Files
Trusty/docs/scope-grammar.md

97 lines
3.4 KiB
Markdown

# 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
```sql
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:
1. Segments are emitted in the same shape both sides expect. An
exclusion of `board:9f2/calendar/private` protects nothing if rows
carry `board/9f2/calendar/private`.
2. No segment contains a `/` of its own. A segment that does silently
becomes two levels, and a grant one level up now covers it.
3. Ids are not user-controlled free text. `board:<user-supplied>` lets
someone name a board `9f2/calendar` and 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 `class` or `id`
- 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.