# 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 ``` :[/]* ``` | part | example | rule | |---|---|---| | class | `board` | opaque to trusty | | id | `9f2` | opaque to trusty | | segment | `calendar`, `private` | separated by `/` | Only the `/` is load-bearing. `:` 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:` 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.