diff --git a/apps/api/src/authorization-middleware.ts b/apps/api/src/authorization-middleware.ts index 813d184..9b88124 100644 --- a/apps/api/src/authorization-middleware.ts +++ b/apps/api/src/authorization-middleware.ts @@ -17,9 +17,11 @@ function sanitizedDenial(c: Context, status: 403 | 503, code: string): Response } /** - * Builds a Hono middleware that enforces `permissionId`, resolved exclusively from Core's own - * trusted, server-side permission registry — a route never supplies its own baseline, and no - * request input can influence which baseline is checked. The principal is resolved exclusively + * Builds a Hono middleware that enforces `permissionId`, resolved exclusively from the injected, + * trusted, server-side `PermissionRegistry` — in production, the complete platform registry + * `startApi` composes from Core's own declarations plus every installed module's, never a + * Core-only fallback. A route never supplies its own baseline, and no request input can influence + * which baseline is checked. The principal is resolved exclusively * through the injected identity runtime (`resolvePrincipal`), the same one `/api/v1/principal` * already uses; nothing here ever trusts a client-supplied claim about identity, ownership, or * grants. A permission id that is not registered fails closed (403) exactly like a denied diff --git a/apps/api/tests/api-http.integration.ts b/apps/api/tests/api-http.integration.ts index 812d963..902ae1b 100644 --- a/apps/api/tests/api-http.integration.ts +++ b/apps/api/tests/api-http.integration.ts @@ -891,9 +891,11 @@ describe.sequential("live API — platform owner and delegated grant management" const ownerEmail = "authz-owner@example.test"; const managerEmail = "authz-manager@example.test"; const password = "correct horse battery staple"; - // Module permission registration does not exist yet, so `core.authorization.grants.manage` — - // the one permission `CORE_PERMISSION_REGISTRY` actually declares — stands in for "some - // registered permission" throughout this scenario's explicit-grant lifecycle. + // This scenario intentionally proves out the explicit-grant lifecycle (create, list, use, + // revoke) against Core's own grant-management permission rather than a module's — a module + // permission would exercise the exact same route and evaluator code, adding coverage breadth + // without coverage depth. The "live API — installed module permission end-to-end" scenario + // below is where a real installed module's declared permission is proven end-to-end instead. const testPermission = "core.authorization.grants.manage"; // Well-formed (passes `PermissionId` shape validation) but never registered — used to prove the // live route rejects it even for a caller who otherwise has full grant-management authority. diff --git a/apps/api/tests/grants-routes.test.ts b/apps/api/tests/grants-routes.test.ts index 0b1b37c..07012a5 100644 --- a/apps/api/tests/grants-routes.test.ts +++ b/apps/api/tests/grants-routes.test.ts @@ -25,8 +25,13 @@ const subjectUserId = parseEkklesiaUserId("subject_1"); // (or, for listing, still return) an unregistered permission id rather than assuming registration. const permission = parsePermissionId("calendar.events.read"); // A permission `CORE_PERMISSION_REGISTRY` actually declares, so tests that must reach -// `store.createGrant`/`store.revokeGrant` can do so without also depending on module permission -// registration, which does not exist yet. +// `store.createGrant`/`store.revokeGrant` can do so. This suite exercises `createGrantsRoutes` in +// isolation and intentionally injects `CORE_PERMISSION_REGISTRY` directly rather than a composed +// platform registry — the routes have no notion of "Core" versus "module" permissions, they only +// ever consult whichever registry they are given. Coverage proving a module-declared permission +// works through the exact same route code lives in +// `apps/api/tests/authorization-middleware.test.ts` (unit) and the "installed module permission +// end-to-end" scenario in `apps/api/tests/api-http.integration.ts` (live). const registeredPermission = CORE_AUTHORIZATION_GRANTS_MANAGE; const grantId = parseGrantId("11111111-1111-1111-1111-111111111111"); diff --git a/core/src/authorization/permissions.ts b/core/src/authorization/permissions.ts index f5242c1..e81e56f 100644 --- a/core/src/authorization/permissions.ts +++ b/core/src/authorization/permissions.ts @@ -46,9 +46,10 @@ export class PermissionRegistryError extends Error { * Builds an immutable permission registry from a fixed list of declarations, rejecting any * duplicate permission id outright at composition time. Deliberately the smallest mechanism that * distinguishes "registered" from "well-formed": no module discovery, no dynamic registration, no - * network or filesystem I/O — a future module registration mechanism can merge its own - * declarations into a combined list and still call this same constructor, inheriting duplicate - * detection for free instead of reimplementing it. + * network or filesystem I/O. The module integration layer's composer + * (`composePlatformPermissionRegistry`, `core/src/modules/permissions.ts`) merges Core's own + * declarations with every discovered module's into one combined list and calls this same + * constructor, inheriting duplicate detection for free rather than reimplementing it. */ export function createPermissionRegistry( declarations: readonly PermissionDeclaration[], @@ -98,11 +99,12 @@ export const CORE_PERMISSION_DECLARATIONS: readonly PermissionDeclaration[] = Ob ]); /** - * Core's own permission registry — the trusted source of truth `apps/api`'s authorization - * enforcement consults so an HTTP route can never supply its own baseline. Contains exactly the - * one Core-owned permission this slice needs; a future module registration mechanism is - * deliberately out of scope here and will compose its own declarations through - * `createPermissionRegistry` rather than through this constant. + * Core's own permission registry, containing exactly the Core-owned permissions declared above. + * Production `apps/api` does not consult this constant directly: it enforces requests against the + * composed platform registry (`composePlatformPermissionRegistry`, + * `core/src/modules/permissions.ts`), which merges these same declarations with every installed + * module's. `CORE_PERMISSION_REGISTRY` remains for Core-only callers and tests that need Core's + * permissions in isolation, without composing in any module. */ export const CORE_PERMISSION_REGISTRY: PermissionRegistry = createPermissionRegistry( CORE_PERMISSION_DECLARATIONS,