import { randomBytes } from "node:crypto"; import { mkdtemp, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { type AuthorizationScope, createUserActor, type GrantId, parseAuthorizationScope, parseEkklesiaUserId, parseGrantId, parsePermissionId, SYSTEM_ACTOR, } from "@ekklesia/contracts"; import { Client, type QueryResultRow } from "pg"; import { afterAll, describe, expect, it } from "vitest"; import { AuthorizationStoreError, createAuthorizationDrizzleDatabase, createAuthorizationStore, } from "../src/authorization/index.js"; import { applyModuleMigrations, createPostgresMigrationDatabase, discoverAllMigrations, } from "../src/migrations/index.js"; const integrationDatabaseUrlEnvironment = "EKKLESIA_MIGRATION_TEST_DATABASE_URL"; const coreMigrationsDirectory = path.join( path.dirname(fileURLToPath(import.meta.url)), "..", "drizzle", ); const trackedDatabases = new Set(); let databaseCounter = 0; class IntegrationHarnessError extends Error { constructor(message: string) { super(message); this.name = "IntegrationHarnessError"; this.stack = `${this.name}: ${this.message}`; } } function harnessError(message: string): IntegrationHarnessError { return new IntegrationHarnessError(message); } function baseDatabaseUrl(): string { const value = process.env[integrationDatabaseUrlEnvironment]; if (typeof value !== "string" || value.trim() === "") { throw harnessError("The authorization integration database is not configured."); } try { const parsed = new URL(value); if (parsed.protocol !== "postgres:" && parsed.protocol !== "postgresql:") { throw new TypeError("Unsupported protocol."); } } catch { throw harnessError("The authorization integration database configuration is invalid."); } return value; } function quoteDatabaseName(databaseName: string): string { if (!/^[a-z][a-z0-9_]{0,62}$/u.test(databaseName)) { throw harnessError("The generated integration database name is invalid."); } return `"${databaseName}"`; } function makeDatabaseName(label: string): string { const safeLabel = label .toLowerCase() .replaceAll(/[^a-z0-9]+/gu, "_") .replaceAll(/^_+|_+$/gu, "") .slice(0, 14); databaseCounter += 1; return `ekk_authz_${process.pid}_${databaseCounter}_${safeLabel || "scenario"}_${randomBytes(4).toString("hex")}`; } function databaseUrl(databaseName: string): string { const parsed = new URL(baseDatabaseUrl()); parsed.pathname = `/${databaseName}`; parsed.searchParams.delete("application_name"); return parsed.toString(); } async function openAdministrativeClient(connectionString: string): Promise { const client = new Client({ connectionString }); client.on("error", () => undefined); try { await client.connect(); return client; } catch { try { await client.end(); } catch { // The public harness failure remains static when connection cleanup also fails. } throw harnessError("The authorization integration database connection failed."); } } async function safeQuery( client: Client, text: string, values: unknown[] = [], ): Promise { try { const result = await client.query(text, values); return result.rows; } catch { throw harnessError("An integration database administration operation failed."); } } async function safeEnd(client: Client): Promise { try { await client.end(); } catch { throw harnessError("An integration database connection could not be closed."); } } async function createDatabase(databaseName: string): Promise { const admin = await openAdministrativeClient(baseDatabaseUrl()); try { await safeQuery(admin, `CREATE DATABASE ${quoteDatabaseName(databaseName)} TEMPLATE template0`); } finally { await safeEnd(admin); } } async function dropDatabase(databaseName: string): Promise { const admin = await openAdministrativeClient(baseDatabaseUrl()); try { await safeQuery( admin, "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = $1 AND pid <> pg_backend_pid()", [databaseName], ); await safeQuery( admin, `DROP DATABASE IF EXISTS ${quoteDatabaseName(databaseName)} WITH (FORCE)`, ); trackedDatabases.delete(databaseName); } finally { await safeEnd(admin); } } async function createDisposableDatabase(label: string): Promise<{ name: string; url: string }> { const name = makeDatabaseName(label); await createDatabase(name); trackedDatabases.add(name); return { name, url: databaseUrl(name) }; } async function applyCoreMigrations(url: string): Promise { const emptyModulesDirectory = await mkdtemp(path.join(tmpdir(), "ekklesia-authz-no-modules-")); try { const aggregate = await discoverAllMigrations({ coreMigrationsDirectory, moduleDiscovery: { moduleSdkVersion: "0.1.0", modulesDirectory: emptyModulesDirectory, platformVersion: "0.1.0", }, }); expect(aggregate.migrations.map((entry) => entry.identity)).toEqual([ "core:0000_identity_core_tables", "core:0001_authorization_core_tables", "core:0002_authorization_integrity", ]); await applyModuleMigrations({ aggregate, database: createPostgresMigrationDatabase(url) }); } finally { await rm(emptyModulesDirectory, { recursive: true, force: true }); } } async function seedUser(client: Client, id: string, email: string): Promise { await safeQuery( client, `insert into "ekklesia_core_user" (id, name, email, email_verified, created_at, updated_at) values ($1, $2, $3, false, now(), now())`, [id, "Integration Test User", email], ); } afterAll(async () => { for (const databaseName of [...trackedDatabases]) { await dropDatabase(databaseName).catch(() => undefined); } }); describe.sequential("live Core authorization store", () => { let database: { name: string; url: string }; let admin: Client; let store: ReturnType; let close: () => Promise; const userOneId = parseEkklesiaUserId("authz_user_one"); const userTwoId = parseEkklesiaUserId("authz_user_two"); const permission = parsePermissionId("calendar.events.read"); const scope: AuthorizationScope = parseAuthorizationScope("calendar-event:123"); const actor = createUserActor("authz_user_two"); // an operator/admin user distinct from the subject it("applies a fresh migration and creates exactly the three authorization tables with their constraints", async () => { database = await createDisposableDatabase("fresh"); await applyCoreMigrations(database.url); admin = await openAdministrativeClient(database.url); const tables = await safeQuery<{ table_name: string }>( admin, `select table_name from information_schema.tables where table_schema = 'public' and table_name like 'ekklesia_core_%' order by table_name`, ); expect(tables.map((row) => row.table_name)).toEqual([ "ekklesia_core_account", "ekklesia_core_authorization_audit", "ekklesia_core_grant", "ekklesia_core_migrations", "ekklesia_core_platform_owner", "ekklesia_core_platform_owner_bootstrap", "ekklesia_core_session", "ekklesia_core_user", "ekklesia_core_verification", ]); await seedUser(admin, userOneId, "authz-one@example.test"); await seedUser(admin, userTwoId, "authz-two@example.test"); const handle = createAuthorizationDrizzleDatabase(database.url); close = handle.close; store = createAuthorizationStore(handle.db); }); it("creates a grant and finds it as active for the matching user, permission, and scope", async () => { const grant = await store.createGrant({ actor, permission, scope, sourceKind: "explicit", userId: userOneId, }); expect(grant.userId).toBe(userOneId); expect(grant.permission).toBe(permission); expect(grant.scope).toBe(scope); expect(grant.sourceKind).toBe("explicit"); expect(grant.revokedAt).toBeUndefined(); const active = await store.hasActiveGrant({ now: new Date(), permission, scope, userId: userOneId, }); expect(active).toBe(true); }); it("does not find the grant for a different user, a different permission, or a different scope", async () => { expect( await store.hasActiveGrant({ now: new Date(), permission, scope, userId: userTwoId }), ).toBe(false); expect( await store.hasActiveGrant({ now: new Date(), permission: parsePermissionId("calendar.events.write"), scope, userId: userOneId, }), ).toBe(false); expect( await store.hasActiveGrant({ now: new Date(), permission, scope: parseAuthorizationScope("calendar-event:456"), userId: userOneId, }), ).toBe(false); }); it("never lets the scoped grant satisfy an unscoped request", async () => { const active = await store.hasActiveGrant({ now: new Date(), permission, userId: userOneId }); expect(active).toBe(false); }); it("lets an unscoped grant satisfy both unscoped and scoped requests for the same permission", async () => { const unscopedPermission = parsePermissionId("calendar.events.list"); await store.createGrant({ actor, permission: unscopedPermission, sourceKind: "relationship", sourceRef: "roster:volunteer:42", userId: userOneId, }); expect( await store.hasActiveGrant({ now: new Date(), permission: unscopedPermission, userId: userOneId, }), ).toBe(true); expect( await store.hasActiveGrant({ now: new Date(), permission: unscopedPermission, scope: parseAuthorizationScope("anything-at-all"), userId: userOneId, }), ).toBe(true); }); it("records exactly one atomic audit entry for the grant it just created", async () => { const rows = await safeQuery<{ action: string; actor_id: string | null; actor_type: string; permission_id: string | null; scope: string | null; subject_user_id: string; }>( admin, `select action, actor_id, actor_type, permission_id, scope, subject_user_id from "ekklesia_core_authorization_audit" where subject_user_id = $1 and permission_id = $2`, [userOneId, permission], ); expect(rows).toHaveLength(1); expect(rows[0]).toMatchObject({ action: "grant.create", actor_id: userTwoId, actor_type: "user", permission_id: permission, scope, subject_user_id: userOneId, }); }); it("refuses to create a grant that is already expired on arrival", async () => { // The store no longer accepts a past expiration at all: such a grant is dead the moment it is // written, which is never what a caller meant. This is validated before any transaction opens. let captured: unknown; try { await store.createGrant({ actor, expiresAt: new Date(Date.now() - 1_000), permission: parsePermissionId("calendar.events.export"), sourceKind: "explicit", userId: userOneId, }); } catch (error) { captured = error; } expect(captured).toMatchObject({ code: "AUTHORIZATION_GRANT_EXPIRATION_INVALID" }); expect( await safeQuery(admin, `select id from "ekklesia_core_grant" where permission_id = $1`, [ "calendar.events.export", ]), ).toHaveLength(0); }); it("denies a grant once its expiry has passed, evaluated against a real elapsed instant", async () => { const expiringPermission = parsePermissionId("calendar.events.expiring"); const expiresAt = new Date(Date.now() + 60_000); await store.createGrant({ actor, expiresAt, permission: expiringPermission, sourceKind: "explicit", userId: userOneId, }); // Deterministic rather than sleeping: the read port evaluates expiration against the instant // it is given, so advancing that instant past the stored expiry is a real elapsed-time check. expect( await store.hasActiveGrant({ now: new Date(expiresAt.getTime() - 1_000), permission: expiringPermission, userId: userOneId, }), ).toBe(true); expect( await store.hasActiveGrant({ now: new Date(expiresAt.getTime() + 1_000), permission: expiringPermission, userId: userOneId, }), ).toBe(false); }); it("allows a grant that has not yet expired", async () => { const notYetExpiredPermission = parsePermissionId("calendar.events.import"); const future = new Date(Date.now() + 86_400_000); await store.createGrant({ actor, expiresAt: future, permission: notYetExpiredPermission, sourceKind: "explicit", userId: userOneId, }); const active = await store.hasActiveGrant({ now: new Date(), permission: notYetExpiredPermission, userId: userOneId, }); expect(active).toBe(true); }); const revocablePermission = parsePermissionId("calendar.events.delete"); let revocableGrantId: GrantId; it("revokes an active grant, records the revocation audit entry, and stops it authorizing", async () => { const grant = await store.createGrant({ actor, permission: revocablePermission, sourceKind: "explicit", userId: userOneId, }); revocableGrantId = grant.id; await store.revokeGrant({ actor, grantId: grant.id }); const active = await store.hasActiveGrant({ now: new Date(), permission: revocablePermission, userId: userOneId, }); expect(active).toBe(false); const audit = await safeQuery<{ action: string }>( admin, `select action from "ekklesia_core_authorization_audit" where grant_id = $1 order by created_at`, [revocableGrantId], ); expect(audit.map((row) => row.action)).toEqual(["grant.create", "grant.revoke"]); }); it("refuses to revoke an already-revoked grant a second time", async () => { let captured: unknown; try { await store.revokeGrant({ actor, grantId: revocableGrantId }); } catch (error) { captured = error; } expect(captured).toBeInstanceOf(AuthorizationStoreError); expect(captured).toMatchObject({ code: "AUTHORIZATION_GRANT_NOT_FOUND" }); }); it("rolls back the whole grant-plus-audit transaction when the audit write violates a real constraint", async () => { const doomedPermission = parsePermissionId("calendar.events.archive"); // The audit table's scope CHECK constraint rejects a non-null empty string; the grant table // has no such constraint. Bypassing the application-layer AuthorizationScope validation (which // never produces an empty string) with a raw cast lets a real Postgres constraint violation // fire on the audit insert specifically, after the grant insert within the same transaction // would otherwise have succeeded. let captured: unknown; try { await store.createGrant({ actor, permission: doomedPermission, scope: "" as AuthorizationScope, sourceKind: "explicit", userId: userOneId, }); } catch (error) { captured = error; } expect(captured).toBeInstanceOf(AuthorizationStoreError); expect(captured).toMatchObject({ code: "AUTHORIZATION_GRANT_CREATE_FAILED" }); const orphanedGrants = await safeQuery( admin, `select id from "ekklesia_core_grant" where permission_id = $1`, [doomedPermission], ); expect(orphanedGrants).toHaveLength(0); const orphanedAudit = await safeQuery( admin, `select id from "ekklesia_core_authorization_audit" where permission_id = $1`, [doomedPermission], ); expect(orphanedAudit).toHaveLength(0); }); it("is not a platform owner before ever being granted ownership", async () => { expect(await store.isPlatformOwner(userOneId)).toBe(false); }); it("grants, records the audit entry for, and looks up platform-owner authority", async () => { await store.grantPlatformOwner(userOneId, actor); expect(await store.isPlatformOwner(userOneId)).toBe(true); expect(await store.isPlatformOwner(userTwoId)).toBe(false); const audit = await safeQuery<{ action: string; subject_user_id: string }>( admin, `select action, subject_user_id from "ekklesia_core_authorization_audit" where action = 'platform_owner.grant'`, ); expect(audit).toEqual([{ action: "platform_owner.grant", subject_user_id: userOneId }]); }); it("revokes platform-owner authority, records the audit entry, and stops future lookups from succeeding", async () => { await store.revokePlatformOwner(userOneId, actor); expect(await store.isPlatformOwner(userOneId)).toBe(false); const audit = await safeQuery<{ action: string }>( admin, `select action from "ekklesia_core_authorization_audit" where action = 'platform_owner.revoke'`, ); expect(audit).toHaveLength(1); }); it("refuses to revoke platform-owner authority a user never held (or already lost)", async () => { let captured: unknown; try { await store.revokePlatformOwner(userTwoId, actor); } catch (error) { captured = error; } expect(captured).toBeInstanceOf(AuthorizationStoreError); expect(captured).toMatchObject({ code: "AUTHORIZATION_PLATFORM_OWNER_NOT_FOUND" }); }); it("re-grants platform-owner authority after a revocation (upsert), reactivating rather than duplicating the row", async () => { await store.grantPlatformOwner(userOneId, SYSTEM_ACTOR); expect(await store.isPlatformOwner(userOneId)).toBe(true); const rows = await safeQuery( admin, `select user_id from "ekklesia_core_platform_owner" where user_id = $1`, [userOneId], ); expect(rows).toHaveLength(1); }); it("shuts down cleanly", async () => { await close(); await safeEnd(admin); await dropDatabase(database.name); }); }); describe.sequential("live Core authorization store — no automatic migration", () => { it("creates no authorization tables merely by constructing the database, the store, or an evaluator against an unmigrated database", async () => { const database = await createDisposableDatabase("unmigrated"); try { const { db, close } = createAuthorizationDrizzleDatabase(database.url); const store = createAuthorizationStore(db); expect(store).toBeDefined(); await close(); const admin = await openAdministrativeClient(database.url); try { const tables = await safeQuery( admin, `select table_name from information_schema.tables where table_schema = 'public'`, ); expect(tables).toHaveLength(0); } finally { await safeEnd(admin); } } finally { await dropDatabase(database.name); } }); }); /** * Pass 3 grant-integrity scenarios, all against real PostgreSQL because every property under test * is a database-concurrency or database-constraint property that a fake cannot establish. * * The administrative client each scenario uses (`openAdministrativeClient`) is **test-only * provisioning and inspection infrastructure**: it seeds users, plants legacy duplicate rows that * the production store would now refuse to create, and reads back the resulting state. It is never * an API pool or a runtime connection, and it is closed in `afterAll` along with its disposable * database. */ describe.sequential("live Core authorization store — grant equivalence and revocation", () => { let database: { name: string; url: string }; let admin: Client; let store: ReturnType; let close: () => Promise; const subject = parseEkklesiaUserId("equiv_subject"); const other = parseEkklesiaUserId("equiv_other"); const actor = createUserActor("equiv_other"); const permission = parsePermissionId("calendar.events.read"); async function effectiveCount(userId: string, permissionId: string): Promise { const rows = await safeQuery<{ n: string }>( admin, `select count(*)::int as n from "ekklesia_core_grant" where user_id = $1 and permission_id = $2 and revoked_at is null and (expires_at is null or expires_at > now())`, [userId, permissionId], ); return Number(rows[0]?.n ?? 0); } it("prepares a disposable database with the three Core migrations applied", async () => { database = await createDisposableDatabase("equivalence"); await applyCoreMigrations(database.url); admin = await openAdministrativeClient(database.url); await seedUser(admin, subject, "equiv-subject@example.test"); await seedUser(admin, other, "equiv-other@example.test"); const handle = createAuthorizationDrizzleDatabase(database.url); close = handle.close; store = createAuthorizationStore(handle.db); expect(await effectiveCount(subject, permission)).toBe(0); }); it("creates the first unscoped explicit grant and writes exactly one create audit row", async () => { const grant = await store.createGrant({ actor, permission, sourceKind: "explicit", userId: subject, }); expect(grant.scope).toBeUndefined(); const audit = await safeQuery<{ action: string }>( admin, `select action from "ekklesia_core_authorization_audit" where grant_id = $1`, [grant.id], ); expect(audit.map((row) => row.action)).toEqual(["grant.create"]); }); it("refuses a sequential equivalent creation, leaving no second grant and no second audit row", async () => { const auditBefore = await safeQuery( admin, `select id from "ekklesia_core_authorization_audit" where subject_user_id = $1 and permission_id = $2`, [subject, permission], ); let captured: unknown; try { await store.createGrant({ actor, permission, sourceKind: "explicit", userId: subject }); } catch (error) { captured = error; } expect(captured).toBeInstanceOf(AuthorizationStoreError); expect(captured).toMatchObject({ code: "AUTHORIZATION_GRANT_ALREADY_EXISTS" }); expect(await effectiveCount(subject, permission)).toBe(1); const auditAfter = await safeQuery( admin, `select id from "ekklesia_core_authorization_audit" where subject_user_id = $1 and permission_id = $2`, [subject, permission], ); expect(auditAfter).toHaveLength(auditBefore.length); }); it("a differing expiration does not make an otherwise identical issuance distinct", async () => { let captured: unknown; try { await store.createGrant({ actor, expiresAt: new Date(Date.now() + 3_600_000), permission, sourceKind: "explicit", userId: subject, }); } catch (error) { captured = error; } expect(captured).toMatchObject({ code: "AUTHORIZATION_GRANT_ALREADY_EXISTS" }); expect(await effectiveCount(subject, permission)).toBe(1); }); it("a different subject, permission, scope, source kind, or source reference is not equivalent", async () => { const distinct = [ { permission, sourceKind: "explicit" as const, userId: other }, { permission: parsePermissionId("calendar.events.write"), sourceKind: "explicit" as const, userId: subject, }, { permission, scope: parseAuthorizationScope("calendar-event:1"), sourceKind: "explicit" as const, userId: subject, }, { permission, sourceKind: "relationship" as const, userId: subject }, { permission, sourceKind: "relationship" as const, sourceRef: "roster:7", userId: subject, }, ]; for (const input of distinct) { const grant = await store.createGrant({ actor, ...input }); expect(grant.id).toBeDefined(); } // A second issuance of one of those distinct tuples is itself a duplicate. let captured: unknown; try { await store.createGrant({ actor, permission, scope: parseAuthorizationScope("calendar-event:1"), sourceKind: "explicit", userId: subject, }); } catch (error) { captured = error; } expect(captured).toMatchObject({ code: "AUTHORIZATION_GRANT_ALREADY_EXISTS" }); }); it("serializes concurrent equivalent creations to exactly one grant and one conflict", async () => { const concurrentPermission = parsePermissionId("calendar.events.concurrent"); // Six genuinely concurrent transactions against the same tuple. The transaction-scoped // advisory lock is what makes this deterministic: no sleeps, no retries, no process-local // mutex — a second API process would be serialized identically. const results = await Promise.allSettled( Array.from({ length: 6 }, () => store.createGrant({ actor, permission: concurrentPermission, sourceKind: "explicit", userId: subject, }), ), ); const fulfilled = results.filter((result) => result.status === "fulfilled"); const rejected = results.filter((result) => result.status === "rejected"); expect(fulfilled).toHaveLength(1); expect(rejected).toHaveLength(5); for (const result of rejected) { expect((result as PromiseRejectedResult).reason).toMatchObject({ code: "AUTHORIZATION_GRANT_ALREADY_EXISTS", }); } expect(await effectiveCount(subject, concurrentPermission)).toBe(1); const audit = await safeQuery( admin, `select id from "ekklesia_core_authorization_audit" where subject_user_id = $1 and permission_id = $2 and action = 'grant.create'`, [subject, concurrentPermission], ); expect(audit).toHaveLength(1); }); it("allows a fresh issuance once the previous equivalent is revoked, and again once one expires", async () => { const cyclePermission = parsePermissionId("calendar.events.cycle"); const first = await store.createGrant({ actor, permission: cyclePermission, sourceKind: "explicit", userId: subject, }); await store.revokeGrant({ actor, grantId: first.id, sourceKind: "explicit" }); // Revoked equivalents do not block a new issuance. const second = await store.createGrant({ actor, expiresAt: new Date(Date.now() + 60_000), permission: cyclePermission, sourceKind: "explicit", userId: subject, }); expect(second.id).not.toBe(first.id); // Force the second grant past its expiry through the administrative client, which is the only // deterministic way to observe "expired equivalent" without sleeping. await safeQuery( admin, `update "ekklesia_core_grant" set expires_at = now() - interval '1 second' where id = $1`, [second.id], ); const third = await store.createGrant({ actor, permission: cyclePermission, sourceKind: "explicit", userId: subject, }); expect(third.id).not.toBe(second.id); expect(await effectiveCount(subject, cyclePermission)).toBe(1); }); it("one revocation revokes every unrevoked equivalent, audits each by its own id, and leaves nothing authorizing", async () => { const legacyPermission = parsePermissionId("calendar.events.legacy"); // Plant the exact state the old store could produce: several equivalent issuances, including // one already expired but never revoked, plus two deliberately non-equivalent neighbours. const legacyIds: string[] = []; for (const expires of ["null", "now() + interval '1 hour'", "now() - interval '1 hour'"]) { const rows = await safeQuery<{ id: string }>( admin, `insert into "ekklesia_core_grant" (user_id, permission_id, scope, source_kind, source_ref, created_by_actor_type, created_by_actor_id, expires_at) values ($1, $2, null, 'explicit', null, 'user', $3, ${expires}) returning id`, [subject, legacyPermission, other], ); legacyIds.push(rows[0]?.id as string); } const scopedNeighbour = await safeQuery<{ id: string }>( admin, `insert into "ekklesia_core_grant" (user_id, permission_id, scope, source_kind, source_ref, created_by_actor_type, created_by_actor_id) values ($1, $2, 'calendar-event:9', 'explicit', null, 'user', $3) returning id`, [subject, legacyPermission, other], ); // Deliberately on its own permission: an unscoped relationship grant for `legacyPermission` // would legitimately keep authorizing after this revocation (it is a different equivalence // tuple), which would make the "nothing authorizes" assertion below meaningless rather than // wrong. The scoped neighbour above stays on the same permission because a scoped grant never // satisfies an unscoped check. const relationshipNeighbour = await safeQuery<{ id: string }>( admin, `insert into "ekklesia_core_grant" (user_id, permission_id, scope, source_kind, source_ref, created_by_actor_type, created_by_actor_id) values ($1, 'calendar.events.legacyrel', null, 'relationship', 'roster:9', 'user', $2) returning id`, [subject, other], ); expect(await effectiveCount(subject, legacyPermission)).toBe(3); await store.revokeGrant({ actor, grantId: parseGrantId(legacyIds[0] as string), sourceKind: "explicit", }); const revoked = await safeQuery<{ id: string; revoked_at: Date | null }>( admin, `select id, revoked_at from "ekklesia_core_grant" where id = any($1::uuid[])`, [legacyIds], ); // Every unrevoked equivalent, including the expired-but-unrevoked one, is now revoked. expect(revoked.every((row) => row.revoked_at !== null)).toBe(true); const auditRows = await safeQuery<{ grant_id: string }>( admin, `select grant_id from "ekklesia_core_authorization_audit" where action = 'grant.revoke' and grant_id = any($1::uuid[])`, [legacyIds], ); // One audit row per grant actually revoked, each carrying that row's own id. expect(auditRows).toHaveLength(legacyIds.length); expect([...new Set(auditRows.map((row) => row.grant_id))].sort()).toEqual( [...legacyIds].sort(), ); // Differently scoped and differently sourced grants are untouched. const untouched = await safeQuery<{ revoked_at: Date | null }>( admin, `select revoked_at from "ekklesia_core_grant" where id = any($1::uuid[])`, [[scopedNeighbour[0]?.id, relationshipNeighbour[0]?.id]], ); expect(untouched.every((row) => row.revoked_at === null)).toBe(true); // No equivalent issuance continues authorizing the user. expect( await store.hasActiveGrant({ now: new Date(), permission: legacyPermission, userId: subject, }), ).toBe(false); }); it("serializes a concurrent create and revoke on the same tuple without leaving an unexplained duplicate", async () => { const racePermission = parsePermissionId("calendar.events.race"); const seed = await store.createGrant({ actor, permission: racePermission, sourceKind: "explicit", userId: subject, }); const [revokeResult, createResult] = await Promise.allSettled([ store.revokeGrant({ actor, grantId: seed.id, sourceKind: "explicit" }), store.createGrant({ actor, permission: racePermission, sourceKind: "explicit", userId: subject, }), ]); // Whichever order the two transactions serialize in, the end state is consistent with that // order: either the create lost to an effective equivalent (0 effective), or it ran after the // revoke (1 effective). Two effective equivalents is the state this pass makes impossible. expect(revokeResult.status).toBe("fulfilled"); const effective = await effectiveCount(subject, racePermission); expect(effective).toBeLessThanOrEqual(1); if (createResult.status === "rejected") { expect(createResult.reason).toMatchObject({ code: "AUTHORIZATION_GRANT_ALREADY_EXISTS" }); expect(effective).toBe(0); } else { expect(effective).toBe(1); } }); it("rolls back every grant and audit change when the multi-row revocation transaction fails", async () => { const doomedPermission = parsePermissionId("calendar.events.doomed"); const rows = await safeQuery<{ id: string }>( admin, `insert into "ekklesia_core_grant" (user_id, permission_id, scope, source_kind, source_ref, created_by_actor_type, created_by_actor_id) values ($1, $2, null, 'explicit', null, 'user', $3), ($1, $2, null, 'explicit', null, 'user', $3) returning id`, [subject, doomedPermission, other], ); // Force the audit insert inside the revoke transaction to violate a real constraint: the audit // action CHECK only permits the four known actions, so a temporary trigger that rewrites the // action makes the whole multi-row revocation fail after the grant updates were staged. await safeQuery( admin, `create function ekklesia_test_break_audit() returns trigger as $$ begin new.action := 'not.a.real.action'; return new; end; $$ language plpgsql`, ); await safeQuery( admin, `create trigger ekklesia_test_break_audit_trigger before insert on "ekklesia_core_authorization_audit" for each row execute function ekklesia_test_break_audit()`, ); try { let captured: unknown; try { await store.revokeGrant({ actor, grantId: parseGrantId(rows[0]?.id as string), sourceKind: "explicit", }); } catch (error) { captured = error; } expect(captured).toMatchObject({ code: "AUTHORIZATION_GRANT_REVOKE_FAILED" }); const after = await safeQuery<{ revoked_at: Date | null }>( admin, `select revoked_at from "ekklesia_core_grant" where id = any($1::uuid[])`, [rows.map((row) => row.id)], ); expect(after.every((row) => row.revoked_at === null)).toBe(true); const audit = await safeQuery( admin, `select id from "ekklesia_core_authorization_audit" where permission_id = $1`, [doomedPermission], ); expect(audit).toHaveLength(0); } finally { // Always restored, even when an assertion above fails. await safeQuery( admin, `drop trigger if exists ekklesia_test_break_audit_trigger on "ekklesia_core_authorization_audit"`, ); await safeQuery(admin, `drop function if exists ekklesia_test_break_audit()`); } }); it("reports duplicate conflict, not-found, and infrastructure failure as three distinct sanitized outcomes", async () => { const codes: string[] = []; try { await store.createGrant({ actor, permission, sourceKind: "explicit", userId: subject }); } catch (error) { codes.push((error as AuthorizationStoreError).code); } try { await store.revokeGrant({ actor, grantId: parseGrantId("00000000-0000-4000-8000-000000000000"), sourceKind: "explicit", }); } catch (error) { codes.push((error as AuthorizationStoreError).code); } await close(); try { await store.hasActiveGrant({ now: new Date(), permission, userId: subject }); } catch (error) { codes.push((error as AuthorizationStoreError).code); } expect(codes).toEqual([ "AUTHORIZATION_GRANT_ALREADY_EXISTS", "AUTHORIZATION_GRANT_NOT_FOUND", "AUTHORIZATION_GRANT_LOOKUP_FAILED", ]); await safeEnd(admin); }); }); describe.sequential("live Core authorization store — bounded keyset pagination", () => { let database: { name: string; url: string }; let admin: Client; let store: ReturnType; let close: () => Promise; const subject = parseEkklesiaUserId("page_subject"); it("seeds explicit and relationship grants, including several sharing one created_at", async () => { database = await createDisposableDatabase("pagination"); await applyCoreMigrations(database.url); admin = await openAdministrativeClient(database.url); await seedUser(admin, subject, "page-subject@example.test"); const handle = createAuthorizationDrizzleDatabase(database.url); close = handle.close; store = createAuthorizationStore(handle.db); // Seven explicit grants all sharing one identical `created_at`, which is exactly the case a // timestamp-only keyset boundary would skip or repeat, plus relationship grants that must // never appear on any page. await safeQuery( admin, `insert into "ekklesia_core_grant" (user_id, permission_id, scope, source_kind, source_ref, created_by_actor_type, created_by_actor_id, created_at) select $1, 'calendar.events.p' || generation, null, 'explicit', null, 'user', $1, timestamptz '2026-01-01 00:00:00Z' from generate_series(1, 7) as generation`, [subject], ); await safeQuery( admin, `insert into "ekklesia_core_grant" (user_id, permission_id, scope, source_kind, source_ref, created_by_actor_type, created_by_actor_id, created_at) select $1, 'calendar.events.r' || generation, null, 'relationship', 'roster:' || generation, 'user', $1, timestamptz '2026-01-01 00:00:00Z' from generate_series(1, 3) as generation`, [subject], ); expect( await safeQuery(admin, `select id from "ekklesia_core_grant" where user_id = $1`, [subject]), ).toHaveLength(10); }); it("traverses every explicit grant exactly once across pages, excluding relationship grants", async () => { const seen: string[] = []; let after: { createdAt: Date; id: GrantId } | undefined; let pages = 0; for (;;) { const page = await store.listGrantsForUser(subject, { ...(after === undefined ? {} : { after }), limit: 3, sourceKind: "explicit", }); pages += 1; seen.push(...page.grants.map((grant) => grant.id)); expect(page.grants.every((grant) => grant.sourceKind === "explicit")).toBe(true); if (page.nextBoundary === undefined) { expect(page.grants.length).toBeLessThanOrEqual(3); break; } after = page.nextBoundary; expect(pages).toBeLessThan(10); } // 7 explicit rows at limit 3 → pages of 3, 3, 1. No duplicates, no omissions, deterministic // despite every row sharing one created_at. expect(pages).toBe(3); expect(seen).toHaveLength(7); expect(new Set(seen).size).toBe(7); // Ordering is created_at DESC, id DESC — with equal timestamps that is a pure descending id // sort, which is exactly what makes traversal total and repeatable. expect([...seen].sort().reverse()).toEqual(seen); }); it("honours the 1 and 100 boundaries and rejects anything outside them", async () => { expect( (await store.listGrantsForUser(subject, { limit: 1, sourceKind: "explicit" })).grants, ).toHaveLength(1); const all = await store.listGrantsForUser(subject, { limit: 100, sourceKind: "explicit" }); expect(all.grants).toHaveLength(7); expect(all.nextBoundary).toBeUndefined(); for (const limit of [0, 101, -1, 1.5]) { await expect(store.listGrantsForUser(subject, { limit })).rejects.toMatchObject({ code: "AUTHORIZATION_GRANT_LIMIT_INVALID", }); } await close(); await safeEnd(admin); }); }); /** * A revocation must never succeed by revoking a *replacement* grant it never addressed. * * `revokeGrant` discovers the addressed grant's equivalence tuple before taking that tuple's * advisory lock, because the tuple columns are immutable once written. The all-equivalent update * that follows matches on the tuple, not on the addressed id — so without an under-lock * revalidation of the target itself, a revoker that paused between its target read and the lock * could wake up after the target was revoked and a fresh equivalent created, and then revoke that * replacement while reporting success. No serial order explains that outcome, and it contradicts * the rule that an already-revoked target keeps not-found behavior. * * Reproducing it needs the interleaving forced, not raced: an ordinary `Promise.all` would depend * on timing luck. The tuple advisory lock is cooperative, so a test-only connection can hold the * exact key the store computes — using the identical SQL expression, not a re-derived constant — * while ordinary `UPDATE`/`INSERT` statements proceed normally. That is the same test-only * administrative provisioning this suite already uses to seed legacy duplicates and force * expirations; no production test hook exists to pause the store. */ describe.sequential("live Core authorization store — stale revocation under the equivalence lock", () => { /** Bounded, condition-based wait — never an arbitrary sleep. Returns once a backend is genuinely * blocked on an advisory lock in this disposable database, which is the only way to know the * stale revocation has passed its target read and reached the lock. */ async function awaitAdvisoryLockWait(client: Client): Promise { const deadline = Date.now() + 20_000; while (Date.now() < deadline) { const rows = await safeQuery<{ waiting: number }>( client, `select count(*)::int as waiting from pg_locks l join pg_stat_activity a on a.pid = l.pid where l.locktype = 'advisory' and not l.granted and a.wait_event_type = 'Lock' and a.wait_event = 'advisory' and l.database = (select oid from pg_database where datname = current_database())`, ); if (Number(rows[0]?.waiting ?? 0) > 0) { return; } await new Promise((resolve) => setTimeout(resolve, 25)); } throw harnessError("The stale revocation never reached the advisory-lock wait."); } /** An independent outer deadline, so a regression can never hang the suite. The timer is always * cleared, including when the awaited promise rejects. */ async function withDeadline(operation: Promise, timeoutMs: number): Promise { let timer: ReturnType | undefined; try { return await Promise.race([ operation, new Promise((_resolve, reject) => { timer = setTimeout( () => reject(harnessError("The stale revocation did not settle before its deadline.")), timeoutMs, ); }), ]); } finally { clearTimeout(timer); } } it("rejects a revocation whose addressed target was revoked and replaced while it waited for the tuple lock", async () => { const database = await createDisposableDatabase("stale_revoke"); await applyCoreMigrations(database.url); const admin = await openAdministrativeClient(database.url); const lockHolder = await openAdministrativeClient(database.url); const handle = createAuthorizationDrizzleDatabase(database.url); let staleSettled: Promise | undefined; let lockHeld = false; try { const subject = parseEkklesiaUserId("stale_subject"); const manager = parseEkklesiaUserId("stale_manager"); await seedUser(admin, subject, "stale-subject@example.test"); await seedUser(admin, manager, "stale-manager@example.test"); const store = createAuthorizationStore(handle.db); const actor = createUserActor(manager); const permission = parsePermissionId("calendar.events.stale"); const firstGrant = await store.createGrant({ actor, permission, sourceKind: "explicit", userId: subject, }); // Hold the exact tuple key the store derives, computed with the identical SQL expression. await safeQuery(lockHolder, "begin"); await safeQuery( lockHolder, `select pg_advisory_xact_lock(hashtextextended(jsonb_build_array( $1::text, $2::text, null::text, 'explicit'::text, null::text )::text, 0))`, [subject, permission], ); lockHeld = true; // The stale revoker reads its target successfully, then blocks on the held lock. const stale = store.revokeGrant({ actor, grantId: firstGrant.id, sourceKind: "explicit" }); staleSettled = stale.then( () => ({ status: "fulfilled" as const }), (error: unknown) => ({ error, status: "rejected" as const }), ); await awaitAdvisoryLockWait(admin); // Committed while the stale revoker waits: the target is revoked and audited, then a fresh // equivalent replacement is created — exactly what a concurrent revoker and creator produce. await safeQuery( admin, `update "ekklesia_core_grant" set revoked_at = now(), revoked_by_actor_type = 'user', revoked_by_actor_id = $2 where id = $1`, [firstGrant.id, manager], ); await safeQuery( admin, `insert into "ekklesia_core_authorization_audit" (action, actor_type, actor_id, subject_user_id, permission_id, grant_id) values ('grant.revoke', 'user', $1, $2, $3, $4)`, [manager, subject, permission, firstGrant.id], ); const replacementRows = await safeQuery<{ id: string }>( admin, `insert into "ekklesia_core_grant" (user_id, permission_id, scope, source_kind, source_ref, created_by_actor_type, created_by_actor_id) values ($1, $2, null, 'explicit', null, 'user', $3) returning id`, [subject, permission, manager], ); const replacementId = replacementRows[0]?.id as string; expect(replacementId).toBeDefined(); // Release the cooperative lock and let the stale revoker resume. await safeQuery(lockHolder, "commit"); lockHeld = false; const outcome = (await withDeadline(staleSettled, 30_000)) as { error?: unknown; status: "fulfilled" | "rejected"; }; // The addressed target no longer exists in the state it was addressed in. expect(outcome.status).toBe("rejected"); expect(outcome.error).toBeInstanceOf(AuthorizationStoreError); expect(outcome.error).toMatchObject({ code: "AUTHORIZATION_GRANT_NOT_FOUND" }); // The replacement it never addressed is untouched, and no audit row claims otherwise. const replacement = await safeQuery<{ revoked_at: Date | null }>( admin, `select revoked_at from "ekklesia_core_grant" where id = $1`, [replacementId], ); expect(replacement[0]?.revoked_at).toBeNull(); expect( await safeQuery( admin, `select id from "ekklesia_core_authorization_audit" where action = 'grant.revoke' and grant_id = $1`, [replacementId], ), ).toHaveLength(0); // The end state is consistent with a serial order: revoke(G1), create(G2), reject(stale). expect(await store.hasActiveGrant({ now: new Date(), permission, userId: subject })).toBe( true, ); const effective = await safeQuery<{ n: number }>( admin, `select count(*)::int as n from "ekklesia_core_grant" where user_id = $1 and permission_id = $2 and revoked_at is null`, [subject, permission], ); expect(Number(effective[0]?.n)).toBe(1); const revokeAudit = await safeQuery<{ grant_id: string }>( admin, `select grant_id from "ekklesia_core_authorization_audit" where action = 'grant.revoke'`, ); expect(revokeAudit.map((row) => row.grant_id)).toEqual([firstGrant.id]); } finally { // The lock is released first, so a failed assertion above can never leave the stale // revocation blocked forever; every client, pool, promise, and database is then cleaned up. if (lockHeld) { await safeQuery(lockHolder, "rollback").catch(() => undefined); } if (staleSettled) { await withDeadline(staleSettled, 30_000).catch(() => undefined); } await safeEnd(lockHolder).catch(() => undefined); await safeEnd(admin).catch(() => undefined); await handle.close().catch(() => undefined); await dropDatabase(database.name).catch(() => undefined); } }); });