97 lines
3.4 KiB
Markdown
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.
|