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, 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("denies a grant once it expires, evaluated against a real elapsed instant", async () => { const expiringPermission = parsePermissionId("calendar.events.export"); const past = new Date(Date.now() - 1_000); await store.createGrant({ actor, expiresAt: past, permission: expiringPermission, sourceKind: "explicit", userId: userOneId, }); const active = await store.hasActiveGrant({ now: new Date(), permission: expiringPermission, userId: userOneId, }); expect(active).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); } }); });