docs: record module authorization architecture

Adds ADR 0014 recording the durable architecture Phase 5A established
across both passes: the shared contracts-layer PermissionDeclaration,
manifest version 3 replacing version 2's bare permission strings, the
reserved core.* namespace and the structural guarantee that a
module-vs-Core collision always surfaces as
ModulePermissionCompositionError rather than the generic
PermissionRegistryError duplicate path, deterministic registry
composition, startup ordering, repository-root/package-version
resolution, registry injection into apps/api's enforcement and
explicit-grant HTTP API, the static-snapshot/historical-record
preservation guarantees, and Doctor/modules:validate integration.
Indexes it in the ADR README, cross-references it from ADR 0006 (only
its manifestVersion 2 / permission-string decision is superseded; the
rest of ADR 0006 remains accepted, and its historical text is left
untouched) and ADR 0013 (module permission registration, deferred
there, is what ADR 0014 implements), and links it from the relevant
development documentation.

Corrects two comments discovered during a direct source audit that
overstated where a module-vs-Core permission collision is actually
caught (core/src/modules/permissions.ts,
apps/api/src/platform-permissions.ts) — comment-only, no behavior
change, already covered by existing tests. Also corrects a
documentation claim in docs/development/authorization.md that
incorrectly implied removing a module blocks explicit revocation of
its historical grants; grants-routes.ts's revoke handler never
consults the permission registry, so revocation remains unaffected.
This commit is contained in:
ekklesia
2026-07-24 17:49:42 -05:00
parent edd7148aa8
commit 9fa051be61
8 changed files with 329 additions and 52 deletions
+38 -26
View File
@@ -1,24 +1,29 @@
# Authorization
Phase 4C establishes Core's authorization foundation and, in a second slice, wires it into
`apps/api` over HTTP. This page covers both: the schema/evaluator/store/registry Core owns, the
platform-owner bootstrap operation, and the enforcement/endpoints `apps/api` exposes on top of it.
See [Identity](identity.md) for the identity foundation this builds on, [API](api.md) for the
HTTP transport it is mounted into, and [ADR
0013](../architecture/decisions/0013-core-authorization-and-owner-bootstrap.md) for the durable
architectural decisions this page describes in more implementation detail.
Phase 4C establishes Core's authorization foundation and wires it into `apps/api` over HTTP. Phase
5A extends that same system so an installed module's declared permissions are enforced and
grantable through the identical mechanism as Core's own. This page covers all of it: the
schema/evaluator/store/registry Core owns, the platform-owner bootstrap operation, the
enforcement/endpoints `apps/api` exposes, and how module manifests contribute to the one platform
permission registry. See [Identity](identity.md) for the identity foundation this builds on,
[API](api.md) for the HTTP transport it is mounted into, [Module development](../module-development/README.md)
for the manifest shape that declares module permissions, and
[ADR 0013](../architecture/decisions/0013-core-authorization-and-owner-bootstrap.md) /
[ADR 0014](../architecture/decisions/0014-module-permission-declarations-and-platform-registry.md)
for the durable architectural decisions this page describes in more implementation detail.
## What this is, and is not
After this slice, an operator can bootstrap exactly one platform owner for a deployment, a
platform owner (or anyone they delegate to) can create, list, and revoke *explicit* authorization
grants over HTTP, and every enforced route fails closed on infrastructure trouble instead of
silently allowing or denying. It is **not** possible to manage roles, groups, teams, or tenants;
there is no owner-management HTTP endpoint (revoking or re-granting platform-owner status is not
exposed at all in this slice); there are no service accounts or API tokens, so only
cookie-authenticated browser callers can reach the mutation routes; and relationship-derived
grants (for example, automatic access from a roster assignment) cannot yet be created by anything
in this repository — the generic grants API structurally cannot create or revoke them either.
An operator can bootstrap exactly one platform owner for a deployment; a platform owner (or anyone
they delegate to) can create, list, and revoke *explicit* authorization grants over HTTP for any
registered permission, Core-owned or module-owned; and every enforced route fails closed on
infrastructure trouble instead of silently allowing or denying. It is **not** possible to manage
roles, groups, teams, or tenants; there is no owner-management HTTP endpoint (revoking or
re-granting platform-owner status is not exposed at all); there are no service accounts or API
tokens, so only cookie-authenticated browser callers can reach the mutation routes; a module cannot
yet mount its own HTTP routes; and relationship-derived grants (for example, automatic access from
a roster assignment) cannot yet be created by anything in this repository — the generic grants API
structurally cannot create or revoke them either.
## Core authorization schema and store
@@ -132,11 +137,16 @@ registry Core's own `core.authorization.grants.manage` permission is registered
module-owned permission and Core's own are enforced, and grantable, through the identical
mechanism — there is no module-specific enforcement path. Removing a module from the next
startup's discovered inventory makes its permission unknown to new authorization checks and new
grants (a well-formed but unregistered permission denies exactly as before); it never deletes,
revokes, or otherwise modifies a historical grant or audit row already recorded for that
permission — a previously granted, still-unrevoked explicit grant for a now-uninstalled module's
permission remains visible through `GET /api/v1/grants` and simply stops being something anyone
can newly grant, revoke, or have authorized against.
grants (a well-formed but unregistered permission denies exactly as before, including on
`POST /api/v1/grants`, which checks the registry explicitly before calling the store); it never
deletes, revokes, or otherwise modifies a historical grant or audit row already recorded for that
permission. A previously granted, still-unrevoked explicit grant for a now-uninstalled module's
permission remains visible through `GET /api/v1/grants` and simply stops being something a
`restricted`-baseline authorization check can newly succeed against. Listing and explicit
revocation (`POST /api/v1/grants/:grantId/revoke`) are unaffected either way: neither route
consults the registry for a grant that already exists — revocation is keyed to the grant's own id
and source kind, not to whether its permission is currently registered — so a manager can still
revoke a historical grant for a now-unregistered permission.
Module HTTP route registration (a module owning its own mounted endpoints) and relationship-derived
grant creation both remain deferred, unrelated slices — this work only makes a module's *declared*
@@ -477,8 +487,10 @@ pnpm test:migrations:integration # requires Docker
## Next phase
Phase 5A, pass 3: deferred from this slice — module HTTP route-registration contracts (a module
mounting its own endpoints), a module runtime-context authorization service, relationship-derived
grant creation itself (a roster or similar module operation, not the generic grants API),
roles/groups/teams/tenants, an owner-management HTTP endpoint, and service accounts/API tokens for
non-browser automation.
Deliberately deferred, unrelated slices — see
[ADR 0014](../architecture/decisions/0014-module-permission-declarations-and-platform-registry.md#deferred)
for the complete list: module HTTP route-registration contracts (a module mounting its own
endpoints), a module runtime-context authorization service, relationship-derived grant creation
itself (a roster or similar module operation, not the generic grants API), reinstall/reactivation
policy for a permission whose owning module was previously removed, roles/groups/teams/tenants, an
owner-management HTTP endpoint, and service accounts/API tokens for non-browser automation.