Skip to main content

Context

DEC-D5 fixed the model: every endpoint declares exactly one permission {module}.{resource}.{action}; permissions bundle into roles; roles attach to users and to machines. This feature is that model. The design decision that makes it trustworthy is that the registry is generated, not maintained. Routes declare their permission in code; the registry is built from those declarations; CI fails if the database and the generated union disagree. A hand-maintained list of permissions drifts from what the code actually checks within weeks, and a drifted permission list is worse than none — it tells an administrator something false about what they just granted. Machines carrying roles is the second load-bearing choice. An API key is not a special case with its own scope language; it is a subject with role assignments, checked identically. That is what lets an OAuth2 consent screen (FS-IDN-0007) render real permissions instead of invented scopes.

Scope (normative)

  • identity.permissions: the generated registry, seeded from route declarations.
  • identity.roles: system roles seeded per organization, plus tenant-defined roles.
  • identity.role_permissions and identity.role_assignments with a polymorphic subject.
  • Authorization resolution: subject → roles → permissions, cached.
  • CI checks: every route declares exactly one permission; no drift between code and database.

Non-scope (normative)

  • Per-record permissions. Permissions are per action, never per row. “Can edit this campaign but not that one” is out, deliberately: it is where authorization models stop being explainable to the administrator who has to configure them.
  • Permission checks inside modules. The API layer resolves the context and passes it down; a module reading these tables would eventually enforce them inconsistently (ADR-022).
  • OAuth scopes — FS-IDN-0007 renders bundles of these permissions; it does not define a second model.

Behaviour (normative)

  1. A permission is {module}.{resource}.{action}, lowercase, dot-separated. It is created by a route declaring it, never by hand and never by a tenant.
  2. Every endpoint in the platform declares exactly one permission. Zero is a defect; two is a defect. CI enforces both.
  3. The registry is generated at build time and seeded. Drift between the code declarations and the database fails CI — the same discipline as the parametric catalogs.
  4. System roles are seeded per organization: owner, admin, member, analyst, support. They are is_system and cannot be edited or deleted by the tenant, only copied as a starting point.
  5. Tenant-defined roles are plan-gated. Lower plans get the system roles; custom roles are an entitlement.
  6. A role_assignment has a polymorphic subject: a membership (a person in an organization) or a machine credential (an API key, an OAuth client). Resolution and checking are the same code path for both — there is no second authorization path to keep in sync.
  7. Resolution is subject → roles → permissions, cached per subject with invalidation on assignment change. A permission check performs zero database queries on the hot path.
  8. Permissions are additive only. There are no deny rules: the union of a subject’s roles is what they may do. Deny rules make an effective-permission set impossible to reason about, and every administrator who has debugged one knows why.
  9. The owner role always holds every permission, including future ones. A permission added by a new module is available to owners immediately.
  10. Sensitive permissions — core.contacts.read_national_id, identity.impersonation.start, identity.roles.update — are never in a seeded role below admin, and granting one is audit-logged with the granting actor.

Data (normative)

API (normative)

Events (normative)

identity.role.assigned and identity.role.revoked, carrying subject, role and actor. Not offered as outgoing webhooks in F1.

Acceptance criteria (normative)

  1. A route declaring zero or two permissions fails CI, each proven by a failing fixture.
  2. Removing a permission from the code declarations while it remains seeded fails the drift check with a message naming the code.
  3. A permission check performs zero database queries once the subject’s cache is warm.
  4. An API key and a user holding the same role are authorized identically across the endpoint set.
  5. Revoking an assignment invalidates the cache and takes effect on the next request, not on the next cache expiry.
  6. A tenant cannot edit or delete a system role.
  7. Granting a sensitive permission writes an audit row naming the granting actor.
  8. Negative: no deny rule exists anywhere in the model, and no endpoint resolves permissions by querying identity tables from inside another module.

Execution

Single slice, synchronous command. The generator and the resolution function live in packages/core; the CI checks land with TS-005. Route declarations are the source — the registry is downstream of the code, never upstream of it.

Open questions

Changelog

Delivery record

Not implemented yet.