Owner recovery lived in the identity-only migration path and could clear historical revocations on conflict. Admin layout also rethrew authorization 503s, turning service-unavailable failures into 500 responses.\n\nMove recovery behind the RBAC migration sequence, exclude revoked candidates without mutating their history, and terminate layout redirects at a no-store 503 route.
170 lines
6.5 KiB
TypeScript
170 lines
6.5 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { spawnSync } from "node:child_process";
|
|
import { readFileSync } from "node:fs";
|
|
import test from "node:test";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
import { startPostgresFixture } from "./helpers/postgres-fixture.ts";
|
|
|
|
const runnerPath = fileURLToPath(new URL("../scripts/db-migrate.mjs", import.meta.url));
|
|
const recoveryMigration = readFileSync(
|
|
new URL("../supabase/migrations/20260807010000_recover_initial_admin_owner.sql", import.meta.url),
|
|
"utf8",
|
|
);
|
|
const ids = {
|
|
first: "30000000-0000-4000-8000-000000000001",
|
|
second: "30000000-0000-4000-8000-000000000002",
|
|
third: "30000000-0000-4000-8000-000000000003",
|
|
blocked: "30000000-0000-4000-8000-000000000004",
|
|
unsynced: "30000000-0000-4000-8000-000000000005",
|
|
};
|
|
|
|
const ownerCountSql = `
|
|
select count(*)
|
|
from public.admin_users au
|
|
join public.admin_user_roles aur on aur.admin_user_id = au.user_id
|
|
join public.admin_roles ar on ar.id = aur.role_id
|
|
where au.revoked_at is null and ar.code = 'owner'
|
|
`;
|
|
|
|
test("Owner recovery grants only one currently loginable synced identity admin", () => {
|
|
const fixture = startPostgresFixture();
|
|
const schemaSql = (sql: string) => fixture.psqlAs(
|
|
"schema_owner",
|
|
"schema-owner-test-password",
|
|
sql,
|
|
);
|
|
|
|
try {
|
|
const migration = spawnSync(process.execPath, [runnerPath], {
|
|
encoding: "utf8",
|
|
env: {
|
|
...process.env,
|
|
SCHEMA_DATABASE_URL: fixture.connectionUrl(
|
|
"schema_owner",
|
|
"schema-owner-test-password",
|
|
),
|
|
},
|
|
});
|
|
assert.equal(migration.status, 0, migration.stderr);
|
|
assert.match(migration.stdout, /applied 20260807010000_recover_initial_admin_owner\.sql/);
|
|
assert.equal(fixture.psql(ownerCountSql), "0", "a truly empty database stays empty");
|
|
|
|
schemaSql(`
|
|
alter table identity.users disable trigger identity_user_business_auth_sync;
|
|
insert into identity.users (id, name, email, email_verified, email_verified_at, role)
|
|
values ('${ids.unsynced}', 'Unsynced Admin', 'unsynced-admin@example.com', true, now(), 'admin');
|
|
alter table identity.users enable trigger identity_user_business_auth_sync;
|
|
`);
|
|
assert.equal(
|
|
fixture.psql(`select count(*) from auth.users where id = '${ids.unsynced}'`),
|
|
"0",
|
|
);
|
|
assert.throws(
|
|
() => schemaSql(recoveryMigration),
|
|
/admin_owner_recovery_requires_exactly_one_active_identity_admin: found 0/,
|
|
"an unsynced identity account is not a truly empty database",
|
|
);
|
|
|
|
fixture.psqlAs("identity_runtime", "identity-runtime-test-password", `
|
|
insert into identity.users
|
|
(id, name, email, email_verified, email_verified_at, role, banned, ban_expires)
|
|
values
|
|
('${ids.first}', 'First Admin', 'first-admin@example.com', true, now(), ' user , admin ', true, now() - interval '1 hour'),
|
|
('${ids.blocked}', 'Blocked Admin', 'blocked-admin@example.com', true, now(), 'admin', true, null)
|
|
`);
|
|
|
|
schemaSql(recoveryMigration);
|
|
assert.equal(
|
|
fixture.psql(`
|
|
select count(*)
|
|
from public.admin_users au
|
|
join public.admin_user_roles aur on aur.admin_user_id = au.user_id
|
|
join public.admin_roles ar on ar.id = aur.role_id
|
|
where au.user_id = '${ids.first}' and au.revoked_at is null and ar.code = 'owner'
|
|
`),
|
|
"1",
|
|
);
|
|
assert.equal(fixture.psql(ownerCountSql), "1");
|
|
|
|
fixture.psqlAs("identity_runtime", "identity-runtime-test-password", `
|
|
insert into identity.users (id, name, email, email_verified, email_verified_at, role)
|
|
values ('${ids.second}', 'Second Admin', 'second-admin@example.com', true, now(), 'admin')
|
|
`);
|
|
|
|
schemaSql(recoveryMigration);
|
|
assert.equal(
|
|
fixture.psql(`
|
|
select string_agg(aur.admin_user_id::text, ',' order by aur.admin_user_id)
|
|
from public.admin_users au
|
|
join public.admin_user_roles aur on aur.admin_user_id = au.user_id
|
|
join public.admin_roles ar on ar.id = aur.role_id
|
|
where au.revoked_at is null and ar.code = 'owner'
|
|
`),
|
|
ids.first,
|
|
"an existing active Owner makes recovery a no-op",
|
|
);
|
|
|
|
schemaSql(`
|
|
alter table public.admin_users disable trigger admin_users_require_active_owner;
|
|
update public.admin_users
|
|
set revoked_at = timestamptz '2026-08-07 01:23:45+00',
|
|
revoked_by = '${ids.first}'
|
|
where user_id = '${ids.first}';
|
|
alter table public.admin_users enable trigger admin_users_require_active_owner;
|
|
`);
|
|
|
|
schemaSql(recoveryMigration);
|
|
assert.equal(
|
|
fixture.psql(`
|
|
select string_agg(aur.admin_user_id::text, ',' order by aur.admin_user_id)
|
|
from public.admin_users au
|
|
join public.admin_user_roles aur on aur.admin_user_id = au.user_id
|
|
join public.admin_roles ar on ar.id = aur.role_id
|
|
where au.revoked_at is null and ar.code = 'owner'
|
|
`),
|
|
ids.second,
|
|
"a revoked historical admin is excluded instead of being restored",
|
|
);
|
|
assert.equal(
|
|
fixture.psql(`
|
|
select count(*)
|
|
from public.admin_users
|
|
where user_id = '${ids.first}'
|
|
and revoked_at = timestamptz '2026-08-07 01:23:45+00'
|
|
and revoked_by = '${ids.first}'
|
|
`),
|
|
"1",
|
|
"recovery preserves the historical revocation fields",
|
|
);
|
|
|
|
fixture.psqlAs("identity_runtime", "identity-runtime-test-password", `
|
|
insert into identity.users (id, name, email, email_verified, email_verified_at, role)
|
|
values ('${ids.third}', 'Third Admin', 'third-admin@example.com', true, now(), 'admin')
|
|
`);
|
|
|
|
schemaSql(`
|
|
alter table public.admin_user_roles disable trigger admin_user_roles_require_active_owner;
|
|
delete from public.admin_user_roles
|
|
where role_id = (select id from public.admin_roles where code = 'owner');
|
|
alter table public.admin_user_roles enable trigger admin_user_roles_require_active_owner;
|
|
`);
|
|
assert.throws(
|
|
() => schemaSql(recoveryMigration),
|
|
/admin_owner_recovery_requires_exactly_one_active_identity_admin: found 2/,
|
|
);
|
|
assert.equal(fixture.psql(ownerCountSql), "0");
|
|
|
|
fixture.psqlAs("identity_runtime", "identity-runtime-test-password", `
|
|
update identity.users set role = 'user' where id in ('${ids.second}', '${ids.third}')
|
|
`);
|
|
assert.throws(
|
|
() => schemaSql(recoveryMigration),
|
|
/admin_owner_recovery_requires_exactly_one_active_identity_admin: found 0/,
|
|
"a revoked identity admin remains ineligible even when it is the only admin role",
|
|
);
|
|
} finally {
|
|
fixture.stop();
|
|
}
|
|
});
|