Scope grant-form matching for OAuth-style <resource>.<action> scopes.
Attesto does not define which scopes exist - that catalog is your
application's policy. It defines the algebra: given a catalog of
concrete scope strings, what counts as a legal grant form, and whether
a granted set covers a required scope. Build a catalog once with
new_catalog/1 and thread it through the matching functions.
A scope is a dotted string of the form <resource>.<action>
(e.g. trackers.read).
Grant forms
A granted scope (one stored on a credential, or carried in a JWT's
scope claim) may be:
- a concrete catalog entry such as
trackers.read; - the resource-level wildcard
<resource>.*(e.g.webhooks.*), which grants every catalog action under that resource; - the full wildcard
*, which grants every catalog scope. Reserved for system-issued credentials; customer-facing issuance must not surface or accept it.
Two validators encode this asymmetry:
valid_grant_form?/2accepts*and is the right check for system-issued credentials.customer_grant_form?/2rejects*and is the check a public token endpoint or credential changeset must use.
Wildcard grants are parsed strictly: only <resource>.* (exactly one
dot, no other segments) is accepted. Deep forms like trackers.read.*
are rejected so a grant is never silently broadened past a single
resource.
Required scopes
A required scope (one a protected endpoint declares) MUST be a
concrete catalog entry - passing a wildcard form as the requirement
returns false, since a wildcard requirement would be ambiguous. Even
a *-granted credential only authorizes catalog entries, so an
uncatalogued endpoint requirement is never granted.
grants_all?/3 raises ArgumentError on a nil or empty required-scope
list so a misconfigured authorization declaration (an endpoint that
forgot to declare its required scope) fails loudly instead of silently
authorizing every caller.
Why strings, not atoms
Scopes round-trip through HTTP requests, JWT claims, and database columns as strings; they are never coerced to atoms (a denial-of-service vector for externally-influenced values).
Summary
Functions
Returns true iff scope is a legal granted form for a
customer-facing credential: a concrete catalog entry or a
resource-level wildcard <resource>.* whose resource appears in the
catalog. The full wildcard * is rejected.
The concrete scope strings in the catalog, sorted.
Returns true iff the granted scope list covers the required
scope.
Returns true iff granted covers every entry in required.
Returns true iff scope is a concrete catalog entry (no wildcards).
Build a catalog from the list of concrete scope strings your API understands. Computes the distinct resources (left-of-dot segments) once so per-request matching is allocation-light.
The distinct resources present in the catalog, sorted.
Returns the subset of requested scopes that are NOT valid
customer-facing grant forms. Used at a token endpoint to surface
invalid_scope (RFC 6749 §5.2) without leaking which scopes are
catalogued. Rejects the system-only * form.
Returns true iff scope is a legal granted form for a
system-issued credential: a concrete catalog entry, the full
wildcard *, or a resource-level wildcard <resource>.* whose
resource appears in the catalog.
Returns true iff value is a syntactically-valid RFC 6749
scope-token: a non-empty string of printable ASCII excluding space,
double-quote, and backslash. This is a wire-format check independent
of any catalog: it rejects a value like "documents.read positions.read"
that, embedded in a space-delimited scope claim, would be
indistinguishable from two separate grants.
Types
Functions
Returns true iff scope is a legal granted form for a
customer-facing credential: a concrete catalog entry or a
resource-level wildcard <resource>.* whose resource appears in the
catalog. The full wildcard * is rejected.
The concrete scope strings in the catalog, sorted.
Returns true iff the granted scope list covers the required
scope.
required MUST be a concrete catalog entry; passing a wildcard form
returns false. A nil or empty grant list returns false. Granted
entries that are not valid grant forms are ignored - they cannot grant
anything, even by accident. Even the full wildcard * only covers
scopes actually in the catalog, so a typo or uncatalogued endpoint
requirement is never authorized.
Returns true iff granted covers every entry in required.
Raises ArgumentError on a nil or empty required list so a
misconfigured authorization declaration fails loudly instead of
silently authorizing every caller.
Returns true iff scope is a concrete catalog entry (no wildcards).
Build a catalog from the list of concrete scope strings your API understands. Computes the distinct resources (left-of-dot segments) once so per-request matching is allocation-light.
The distinct resources present in the catalog, sorted.
Returns the subset of requested scopes that are NOT valid
customer-facing grant forms. Used at a token endpoint to surface
invalid_scope (RFC 6749 §5.2) without leaking which scopes are
catalogued. Rejects the system-only * form.
Returns true iff scope is a legal granted form for a
system-issued credential: a concrete catalog entry, the full
wildcard *, or a resource-level wildcard <resource>.* whose
resource appears in the catalog.
Customer-facing surfaces MUST use customer_grant_form?/2 instead - it
rejects the system-only * form.
Returns true iff value is a syntactically-valid RFC 6749
scope-token: a non-empty string of printable ASCII excluding space,
double-quote, and backslash. This is a wire-format check independent
of any catalog: it rejects a value like "documents.read positions.read"
that, embedded in a space-delimited scope claim, would be
indistinguishable from two separate grants.