fix: enforce registered authorization permissions
Two related fail-closed gaps let a well-formed but never-declared permission id slip through: - POST /api/v1/grants created explicit grants for any well-formed PermissionId without checking the injected permission registry, allowing dormant grants for invented identifiers that could become effective the moment those identifiers were registered later. - createAuthorizationEvaluator validated the permission id only inside the restricted-baseline branch, so a malformed permission combined with the public or authenticated baseline could still return allowed. Both now fail closed: the grants route checks the target permission against the trusted registry before calling store.createGrant (sanitized 403, no store or audit write), and the evaluator validates permission shape before any baseline is allowed to return an allowed decision, without adding a registry dependency to the evaluator itself. The live integration lifecycle test switched from an unregistered calendar.events.read to the registered core.authorization.grants.manage permission, since module permission registration does not exist yet.
This commit is contained in:
@@ -832,7 +832,13 @@ describe.sequential("live API — platform owner and delegated grant management"
|
||||
const ownerEmail = "[email protected]";
|
||||
const managerEmail = "[email protected]";
|
||||
const password = "correct horse battery staple";
|
||||
const testPermission = "calendar.events.read";
|
||||
// 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.
|
||||
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.
|
||||
const unregisteredPermission = "calendar.events.read";
|
||||
|
||||
it("starts a freshly migrated API and signs up the eventual owner and eventual delegated manager", async () => {
|
||||
database = await createDisposableDatabase("authz_http");
|
||||
@@ -913,6 +919,54 @@ describe.sequential("live API — platform owner and delegated grant management"
|
||||
expect(response.status).toBe(200);
|
||||
});
|
||||
|
||||
it("rejects a well-formed but unregistered permission even from a caller with full grant-management authority, and writes no grant or audit row", async () => {
|
||||
const admin = await openAdministrativeClient(database.url);
|
||||
try {
|
||||
const grantCountBefore = await safeQuery<{ count: string }>(
|
||||
admin,
|
||||
`select count(*)::text as count from "ekklesia_core_grant" where permission_id = $1`,
|
||||
[unregisteredPermission],
|
||||
);
|
||||
const auditCountBefore = await safeQuery<{ count: string }>(
|
||||
admin,
|
||||
`select count(*)::text as count from "ekklesia_core_authorization_audit" where permission_id = $1`,
|
||||
[unregisteredPermission],
|
||||
);
|
||||
expect(grantCountBefore[0]?.count).toBe("0");
|
||||
expect(auditCountBefore[0]?.count).toBe("0");
|
||||
|
||||
const response = await fetch(`${origin}/api/v1/grants`, {
|
||||
body: JSON.stringify({
|
||||
permission: unregisteredPermission,
|
||||
subjectUserId: managerUserId,
|
||||
}),
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
cookie: managerCookie,
|
||||
origin: dashboardOrigin,
|
||||
},
|
||||
method: "POST",
|
||||
});
|
||||
expect(response.status).toBe(403);
|
||||
expect(await response.json()).toEqual({ code: "AUTHORIZATION_DENIED" });
|
||||
|
||||
const grantCountAfter = await safeQuery<{ count: string }>(
|
||||
admin,
|
||||
`select count(*)::text as count from "ekklesia_core_grant" where permission_id = $1`,
|
||||
[unregisteredPermission],
|
||||
);
|
||||
const auditCountAfter = await safeQuery<{ count: string }>(
|
||||
admin,
|
||||
`select count(*)::text as count from "ekklesia_core_authorization_audit" where permission_id = $1`,
|
||||
[unregisteredPermission],
|
||||
);
|
||||
expect(grantCountAfter[0]?.count).toBe("0");
|
||||
expect(auditCountAfter[0]?.count).toBe("0");
|
||||
} finally {
|
||||
await safeEnd(admin);
|
||||
}
|
||||
});
|
||||
|
||||
it("creates an explicit grant via HTTP, deriving sourceKind and actor server-side, and records an atomic audit entry", async () => {
|
||||
const response = await fetch(`${origin}/api/v1/grants`, {
|
||||
body: JSON.stringify({
|
||||
|
||||
Reference in New Issue
Block a user