3.4 KiB
Scope grammar
The string format trusty_policy.permits() matches against. Two repos
depend on it, so it is written down here rather than inferred from the
implementation.
The rule
CREATE OR REPLACE FUNCTION trusty_policy.covers(scope TEXT, resource TEXT)
RETURNS BOOLEAN
LANGUAGE sql IMMUTABLE PARALLEL SAFE
AS $fn$
SELECT scope IS NOT NULL
AND resource IS NOT NULL
AND (resource = scope OR starts_with(resource, scope || '/'))
$fn$
A scope covers a resource when they are equal, or when the resource
begins with the scope followed by a /.
That is the whole grammar. There are no wildcards, no globs, and no pattern syntax — deliberately.
Shape
<class>:<id>[/<segment>]*
| part | example | rule |
|---|---|---|
| class | board |
opaque to trusty |
| id | 9f2 |
opaque to trusty |
| segment | calendar, private |
separated by / |
Only the / is load-bearing. <class>:<id> is a convention the
consumer chooses; trusty treats everything before the first / as one
opaque string.
What follows from it
/ is the only separator. board:9f2/calendar covers
board:9f2/calendar/private. It does not cover
board:9f2/calendar-private, because the character after the scope is
-, not /.
A near-miss prefix does not match. board:o does not cover
board:other/calendar. Without the separator in the concatenation it
would, which is why the separator is in there and not an afterthought.
No wildcards. A scope of % is a scope literally named %, and a
scope containing _ matches only a literal underscore. starts_with is
used rather than LIKE precisely so an attacker-influenced scope cannot
widen itself into a pattern.
Case and Unicode are exact. Board:9f2 is a different scope from
board:9f2. There is no normalisation, folding, or trimming anywhere in
the path. Emit exactly what you mean to match.
No trailing slash. board:9f2/calendar/ is a different scope from
board:9f2/calendar, and covers board:9f2/calendar//private rather
than anything useful. Do not emit one.
Exclusions use the same rule. An exclusion of
board:9f2/calendar/private shuts that resource and everything beneath
it, checked before coverage and across every live grant of the subject
regardless of tier.
What a producer must guarantee
Anything writing a resource_scope column, or issuing a grant, owes the
following. Trusty cannot check them, because the parts are opaque to it:
- Segments are emitted in the same shape both sides expect. An
exclusion of
board:9f2/calendar/privateprotects nothing if rows carryboard/9f2/calendar/private. - No segment contains a
/of its own. A segment that does silently becomes two levels, and a grant one level up now covers it. - Ids are not user-controlled free text.
board:<user-supplied>lets someone name a board9f2/calendarand inherit a scope they were never granted.
Point 3 is the one that bites. If ids can contain /, the grammar has
no boundary and coverage becomes forgeable.
What trusty deliberately does not do
- Parse, validate or normalise a scope
- Attach meaning to
classorid - Enforce a maximum depth
The engine takes two opaque strings and asks whether one contains the other. Keeping it that narrow is what lets the consumer own the namespace — and is why the guarantees above are the consumer's to keep.