Authorization
Flametrench authorization is built on a single primitive: relational tuples. Every grant is a row (subject, relation, object); every permission check matches tuples exactly.
This chapter is normative. Rationale and alternatives are in ADR 0001 — Authorization model.
The tuple primitive
Entity shape
id— UUIDv7;tup_<hex>.subject_type— the type prefix of the subject. Currently MUST beusr.subject_id— the UUIDv7 of the subject.relation— a string matching^[a-z_]{2,32}$. Either a registered relation (below) or an application-custom relation.object_type— the type prefix of the object. MAY be any valid type prefix (2–6 lowercase ASCII characters, matching^[a-z]{2,6}$). Registered object types includeorg,mem,inv,ses,cred,usr. Applications MAY introduce custom object types (project,doc, etc.).object_id— the UUIDv7 of the object.created_at,created_by(nullable).
The natural key of a tuple is the 5-tuple (subject_type, subject_id, relation, object_type, object_id). This combination MUST be unique; duplicate grants are prohibited.
Registered relations
Six built-in relations. The spec pins their semantic intent; applications SHOULD use them when the intent matches.
| Relation | Semantic intent |
|---|---|
owner | Full control, including ownership transfer. Typically one per org. |
admin | Manage members, settings, and other admins; cannot transfer ownership. |
member | Default org participant; general-purpose access to the org's features. |
guest | Minimal, typically scoped; limited access. |
viewer | Read-only on the object. |
editor | Read and write on the object. |
Org-scoped vs. object-scoped
Typical usage:
owner,admin,member,guestare applied at the org level (object_type = org). These are membership relations.viewer,editorare applied at any object level (object_type = project,doc, etc.).
The spec does not prohibit other combinations; applications MAY use editor on an org to mean "can edit any org-level configuration," if that matches their model.
Custom relations
Applications MAY register relation names not in the built-in registry. Custom relations follow the same format (^[a-z_]{2,32}$) and carry no spec-defined semantics — the application defines meaning.
The check() primitive
check(subject, relation | [relations], object) → bool
Returns true iff a tuple exists matching the subject, the object, and one of the given relations.
Single-relation form
check(
subject_type = "usr",
subject_id = "0190f2a8-1b3c-7abc-8123-456789abcdef",
relation = "admin",
object_type = "org",
object_id = "01abcdef-..."
) → bool
Set form
check(
subject_type = "usr",
subject_id = "...",
relations = ["owner", "admin"],
object_type = "org",
object_id = "..."
) → bool
Returns true if a tuple exists for subject on object with any of the listed relations. Equivalent to the logical OR of individual checks, provided as a single call for ergonomics and atomicity.
Implementations MUST accept both forms. The relation set in the set-form MUST be non-empty.
Exact-match semantics (default)
By default, check() performs exact match only — no derivations, no inheritance. When no rewrite rules are registered, there is:
- No relation implication.
admindoes not implyeditor.editordoes not implyviewer. - No parent-child inheritance.
vieweroforg_acmedoes not implyviewerof any project inorg_acme. - No group expansion. Group subjects remain deferred.
If an application's authorization policy requires any of these derivations, three options are available:
- Materialize implied tuples at state-change time (write all implied grants explicitly).
- Compose checks at call sites using the set form of
check(). - Register rewrite rules (v0.2) — declarative derivation with depth and fan-out caps. See below.
Rewrite rules (v0.2 / v0.3)
Rewrite rules let you declare authorization policies that check() evaluates automatically — role implication, parent-child inheritance, and similar derivations. Rules are registered at store-construction time; when no rules are registered, check() behavior is byte-identical to pure exact-match (full backward compatibility with any v0.1 call site).
Rationale and the full evaluation model live in ADR 0007 — Authorization rewrite rules.
v0.2: In-memory tuple store supports rewrite rules.
v0.3: PostgresTupleStore.check() accepts the same rules option and evaluates via iterative async expansion (ADR 0017). Adopters who need Postgres durability with rules no longer need an in-memory shadow store.
Three node types
A rule is keyed on (object_type, relation) and contains a union of one or more nodes:
this— the explicit-tuple set. Identical to v0.1 exact-match semantics; always implicitly part of every rule's union. Listingthisexplicitly is documentation, not behavior change.computed_userset { relation: <name> }— anyone holding<name>on the same object. Used for role implication:editorimpliesviewer.tuple_to_userset { tupleset: { relation: <ttu> }, computed_userset: { relation: <target> } }— anyone holding<target>on the object pointed to by the<ttu>relation from the current object. Used for parent-child inheritance.
Not included in v0.2 rules: intersection, exclusion, and transitive closure. See ADR 0007 for rationale.
Example: role implication and parent-child inheritance
rules:
proj:
viewer:
union:
- this
- computed_userset: { relation: editor }
- tuple_to_userset:
tupleset: { relation: parent_org }
computed_userset: { relation: member }
editor:
union:
- this
- computed_userset: { relation: admin }
org:
member:
thisThis expresses:
- Anyone with
editoron a project also hasviewer. - Anyone with
adminon a project also haseditor(and transitivelyviewer). - Any org
memberis implicitly aviewerof org-owned projects via theparent_orghop.
Registering rules in code
from flametrench_authz import InMemoryTupleStore, ComputedUserset, TupleToUserset
store = InMemoryTupleStore(
rules={
"proj": {
"viewer": [
ComputedUserset(relation="editor"),
TupleToUserset(
tupleset_relation="parent_org",
computed_userset_relation="member",
),
],
},
},
)For Postgres (v0.3):
store = PostgresTupleStore(pool=pool, rules=rules)Depth and fan-out limits
- Maximum evaluation depth: 8 hops.
- Maximum fan-out per step: 1024 subjects.
Graphs that exceed either limit raise EvaluationLimitExceededError. Rules are SDK/application configuration, not row-level data — store them in code or in an app-managed config table.
Patterns for working without rewrite rules
Applications on pure exact-match semantics face two patterns for expressing implied grants. Both are spec-supported; neither is favored.
Pattern A — Materialize at state-change time
When the state that implies a grant changes, the application writes the grant as an explicit tuple.
Example: "every org member can view every project in that org"
- When Alice joins
org_acme, the application writes one additionaltup_(alice, viewer, project_X)for every existing project inorg_acme. - When a new project is created in
org_acme, the application writes one additionaltup_(member, viewer, project)for every active member of the org. - When Alice leaves, the application deletes her membership tuple AND all the implied project-viewer tuples.
When to prefer Pattern A:
- When "who can view X?" must be a trivial SQL query.
- When membership churn is low relative to resource churn.
- When audit must show every grant as an explicit row.
Pattern B — Combined checks at query time
The application does not materialize implied grants. Instead, check() calls pass a relation set and the application inspects multiple tuples.
Example: same policy, different implementation
check(user, [viewer, editor, owner, admin], project)— first, check direct grants on the project.- If that returns false,
check(user, [owner, admin, member], project.parent_org)— check org-level membership.
When to prefer Pattern B:
- When state-change volume would otherwise cause tuple explosions.
- When derivation logic is narrow and localizable in the application.
- When audit can tolerate "who can view X?" being a computed query rather than a row lookup.
Mixing patterns
Most real applications use both. Use Pattern A for default grants (org membership implies certain resource grants) and Pattern B for exceptional cases (Carol-the-contractor's per-project scope). Rewrite rules (v0.2) provide a third path that handles both without bespoke application code.
Supporting operations
createTuple(subject_type, subject_id, relation, object_type, object_id, created_by?) → tup_id.deleteTuple(tup_id).check(...)— as defined above.listTuplesBySubject(subject, cursor, limit) → [tup]— enumerate what a subject holds.listTuplesByObject(object, relation?, cursor, limit) → [tup]— enumerate who has grants on an object.cascadeRevokeSubject(subject)— delete all tuples with the given subject. Used on user revocation and membership termination.
Enumeration operations MUST paginate with UUIDv7-ordered cursors (seek-based). Applications SHOULD authz-gate enumeration operations via check() with appropriate relations; the spec does not mandate default policy.
Conformance fixtures
-
Exact-match check. Given
tup_(alice, editor, project_42)and no other tuples for that subject/object,check(alice, editor, project_42)MUST return true;check(alice, viewer, project_42)MUST return false. -
Set-form check. Given
tup_(alice, editor, project_42),check(alice, [viewer, editor], project_42)MUST return true. -
No implicit derivation. Given only
tup_(alice, admin, org_acme)and no other tuples,check(alice, editor, org_acme)MUST return false. The spec does NOT implyeditorfromadminwithout rewrite rules. -
Empty rules equals v0.1. A store constructed with no rules MUST produce byte-identical results to a v0.1 exact-match store on the same tuple set.
-
Uniqueness of tuples. Attempting to create a second tuple with identical
(subject_type, subject_id, relation, object_type, object_id)MUST return an error or be idempotent (returning the existing tuple's ID). Implementations MUST NOT store duplicate rows. -
Invalid UUID in subject or object. Any tuple operation that would store
subject_id = ffffffff-ffff-ffff-ffff-ffffffffffff(Max UUID) or00000000-0000-0000-0000-000000000000(Nil UUID) MUST fail; these are not valid Flametrench identifiers perdocs/ids.md.
More fixtures are in spec/conformance/ across three rewrite-rule fixture files covering computed_userset, tuple_to_userset, and empty-rules-equals-v0.1 cases.