feat: add authorization bootstrap and HTTP enforcement
Wires Core's identity and authorization domains into apps/api over one shared, validated PostgreSQL pool; adds an explicit, single-use, concurrency-safe platform-owner bootstrap CLI; adds a smallest-possible immutable permission registry with one Core-owned restricted permission (core.authorization.grants.manage); and adds reusable Hono authorization enforcement plus a narrowly scoped /api/v1/grants HTTP API for managing explicit grants only, gated by an exact trusted Origin on every mutation.
This commit is contained in:
@@ -0,0 +1,364 @@
|
||||
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 { parseEkklesiaUserId } from "@ekklesia/contracts";
|
||||
import { Client, type QueryResultRow } from "pg";
|
||||
import { afterAll, describe, expect, it } from "vitest";
|
||||
import {
|
||||
bootstrapPlatformOwner,
|
||||
createAuthorizationDrizzleDatabase,
|
||||
PlatformOwnerBootstrapError,
|
||||
} 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<string>();
|
||||
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 bootstrap 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 bootstrap 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_bootstrap_${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<Client> {
|
||||
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 bootstrap integration database connection failed.");
|
||||
}
|
||||
}
|
||||
|
||||
async function safeQuery<Row extends QueryResultRow>(
|
||||
client: Client,
|
||||
text: string,
|
||||
values: unknown[] = [],
|
||||
): Promise<readonly Row[]> {
|
||||
try {
|
||||
const result = await client.query<Row>(text, values);
|
||||
return result.rows;
|
||||
} catch {
|
||||
throw harnessError("An integration database administration operation failed.");
|
||||
}
|
||||
}
|
||||
|
||||
async function safeEnd(client: Client): Promise<void> {
|
||||
try {
|
||||
await client.end();
|
||||
} catch {
|
||||
throw harnessError("An integration database connection could not be closed.");
|
||||
}
|
||||
}
|
||||
|
||||
async function createDatabase(databaseName: string): Promise<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
const emptyModulesDirectory = await mkdtemp(
|
||||
path.join(tmpdir(), "ekklesia-bootstrap-no-modules-"),
|
||||
);
|
||||
try {
|
||||
const aggregate = await discoverAllMigrations({
|
||||
coreMigrationsDirectory,
|
||||
moduleDiscovery: {
|
||||
moduleSdkVersion: "0.1.0",
|
||||
modulesDirectory: emptyModulesDirectory,
|
||||
platformVersion: "0.1.0",
|
||||
},
|
||||
});
|
||||
await applyModuleMigrations({ aggregate, database: createPostgresMigrationDatabase(url) });
|
||||
} finally {
|
||||
await rm(emptyModulesDirectory, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
async function seedUser(client: Client, id: string, email: string): Promise<void> {
|
||||
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],
|
||||
);
|
||||
}
|
||||
|
||||
async function ownerRowCount(client: Client): Promise<number> {
|
||||
const rows = await safeQuery<{ count: string }>(
|
||||
client,
|
||||
`select count(*)::text as count from "ekklesia_core_platform_owner"`,
|
||||
);
|
||||
return Number(rows[0]?.count ?? "0");
|
||||
}
|
||||
|
||||
afterAll(async () => {
|
||||
for (const databaseName of [...trackedDatabases]) {
|
||||
await dropDatabase(databaseName).catch(() => undefined);
|
||||
}
|
||||
});
|
||||
|
||||
describe.sequential("live platform-owner bootstrap", () => {
|
||||
let database: { name: string; url: string };
|
||||
let admin: Client;
|
||||
let db: ReturnType<typeof createAuthorizationDrizzleDatabase>["db"];
|
||||
let close: () => Promise<void>;
|
||||
|
||||
const firstUserId = parseEkklesiaUserId("bootstrap_user_one");
|
||||
const secondUserId = parseEkklesiaUserId("bootstrap_user_two");
|
||||
|
||||
it("bootstraps successfully for an existing user and records exactly one audit entry with the fixed bootstrap actor identity", async () => {
|
||||
database = await createDisposableDatabase("success");
|
||||
await applyCoreMigrations(database.url);
|
||||
|
||||
admin = await openAdministrativeClient(database.url);
|
||||
await seedUser(admin, firstUserId, "[email protected]");
|
||||
await seedUser(admin, secondUserId, "[email protected]");
|
||||
|
||||
const handle = createAuthorizationDrizzleDatabase(database.url);
|
||||
db = handle.db;
|
||||
close = handle.close;
|
||||
|
||||
await bootstrapPlatformOwner(db, { userId: firstUserId });
|
||||
|
||||
const owners = await safeQuery<{ user_id: string }>(
|
||||
admin,
|
||||
`select user_id from "ekklesia_core_platform_owner"`,
|
||||
);
|
||||
expect(owners).toEqual([{ user_id: firstUserId }]);
|
||||
|
||||
const audit = await safeQuery<{
|
||||
action: string;
|
||||
actor_id: string | null;
|
||||
actor_type: string;
|
||||
subject_user_id: string;
|
||||
}>(
|
||||
admin,
|
||||
`select action, actor_id, actor_type, subject_user_id from "ekklesia_core_authorization_audit"`,
|
||||
);
|
||||
expect(audit).toEqual([
|
||||
{
|
||||
action: "platform_owner.grant",
|
||||
actor_id: "authorization-bootstrap-cli",
|
||||
actor_type: "system",
|
||||
subject_user_id: firstUserId,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("rejects a second bootstrap attempt outright, even for a different user, and leaves no new row or audit entry", async () => {
|
||||
let captured: unknown;
|
||||
try {
|
||||
await bootstrapPlatformOwner(db, { userId: secondUserId });
|
||||
} catch (error) {
|
||||
captured = error;
|
||||
}
|
||||
|
||||
expect(captured).toBeInstanceOf(PlatformOwnerBootstrapError);
|
||||
expect(captured).toMatchObject({ code: "PLATFORM_OWNER_BOOTSTRAP_ALREADY_CONSUMED" });
|
||||
|
||||
expect(await ownerRowCount(admin)).toBe(1);
|
||||
const audit = await safeQuery(admin, `select id from "ekklesia_core_authorization_audit"`);
|
||||
expect(audit).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("still refuses bootstrap after the existing owner is revoked — the historical row alone is enough to have consumed it", async () => {
|
||||
await safeQuery(
|
||||
admin,
|
||||
`update "ekklesia_core_platform_owner" set revoked_at = now(), revoked_by_actor_type = 'system' where user_id = $1`,
|
||||
[firstUserId],
|
||||
);
|
||||
|
||||
let captured: unknown;
|
||||
try {
|
||||
await bootstrapPlatformOwner(db, { userId: secondUserId });
|
||||
} catch (error) {
|
||||
captured = error;
|
||||
}
|
||||
|
||||
expect(captured).toBeInstanceOf(PlatformOwnerBootstrapError);
|
||||
expect(captured).toMatchObject({ code: "PLATFORM_OWNER_BOOTSTRAP_ALREADY_CONSUMED" });
|
||||
expect(await ownerRowCount(admin)).toBe(1);
|
||||
});
|
||||
|
||||
it("shuts down cleanly", async () => {
|
||||
await close();
|
||||
await safeEnd(admin);
|
||||
await dropDatabase(database.name);
|
||||
});
|
||||
});
|
||||
|
||||
describe.sequential("live platform-owner bootstrap — nonexistent user", () => {
|
||||
it("rolls back entirely for a user that does not exist, leaving no owner row and no audit entry", async () => {
|
||||
const database = await createDisposableDatabase("missing_user");
|
||||
await applyCoreMigrations(database.url);
|
||||
const { close, db } = createAuthorizationDrizzleDatabase(database.url);
|
||||
|
||||
try {
|
||||
const missingUserId = parseEkklesiaUserId("bootstrap_user_never_seeded");
|
||||
let captured: unknown;
|
||||
try {
|
||||
await bootstrapPlatformOwner(db, { userId: missingUserId });
|
||||
} catch (error) {
|
||||
captured = error;
|
||||
}
|
||||
|
||||
expect(captured).toBeInstanceOf(PlatformOwnerBootstrapError);
|
||||
expect(captured).toMatchObject({ code: "PLATFORM_OWNER_BOOTSTRAP_USER_NOT_FOUND" });
|
||||
|
||||
const admin = await openAdministrativeClient(database.url);
|
||||
try {
|
||||
expect(await ownerRowCount(admin)).toBe(0);
|
||||
const audit = await safeQuery(admin, `select id from "ekklesia_core_authorization_audit"`);
|
||||
expect(audit).toHaveLength(0);
|
||||
} finally {
|
||||
await safeEnd(admin);
|
||||
}
|
||||
} finally {
|
||||
await close();
|
||||
await dropDatabase(database.name);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe.sequential("live platform-owner bootstrap — concurrency", () => {
|
||||
it("produces exactly one owner when two bootstrap attempts race for two different users", async () => {
|
||||
const database = await createDisposableDatabase("race");
|
||||
await applyCoreMigrations(database.url);
|
||||
|
||||
const admin = await openAdministrativeClient(database.url);
|
||||
const raceUserA = parseEkklesiaUserId("bootstrap_race_user_a");
|
||||
const raceUserB = parseEkklesiaUserId("bootstrap_race_user_b");
|
||||
await seedUser(admin, raceUserA, "[email protected]");
|
||||
await seedUser(admin, raceUserB, "[email protected]");
|
||||
|
||||
const { close, db } = createAuthorizationDrizzleDatabase(database.url);
|
||||
try {
|
||||
const results = await Promise.allSettled([
|
||||
bootstrapPlatformOwner(db, { userId: raceUserA }),
|
||||
bootstrapPlatformOwner(db, { userId: raceUserB }),
|
||||
]);
|
||||
|
||||
const fulfilled = results.filter((result) => result.status === "fulfilled");
|
||||
const rejected = results.filter((result) => result.status === "rejected");
|
||||
expect(fulfilled).toHaveLength(1);
|
||||
expect(rejected).toHaveLength(1);
|
||||
expect((rejected[0] as PromiseRejectedResult).reason).toBeInstanceOf(
|
||||
PlatformOwnerBootstrapError,
|
||||
);
|
||||
expect((rejected[0] as PromiseRejectedResult).reason).toMatchObject({
|
||||
code: "PLATFORM_OWNER_BOOTSTRAP_ALREADY_CONSUMED",
|
||||
});
|
||||
|
||||
const owners = await safeQuery<{ user_id: string }>(
|
||||
admin,
|
||||
`select user_id from "ekklesia_core_platform_owner"`,
|
||||
);
|
||||
expect(owners).toHaveLength(1);
|
||||
expect([raceUserA, raceUserB]).toContain(owners[0]?.user_id);
|
||||
|
||||
const audit = await safeQuery(
|
||||
admin,
|
||||
`select id from "ekklesia_core_authorization_audit" where action = 'platform_owner.grant'`,
|
||||
);
|
||||
expect(audit).toHaveLength(1);
|
||||
} finally {
|
||||
await close();
|
||||
await safeEnd(admin);
|
||||
await dropDatabase(database.name);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user