fix(admin): allow administrator list reads
Staging Backend Quality Gate / validate (push) Successful in 12m30s
Staging Backend Quality Gate / publish (push) Successful in 7m37s

This commit is contained in:
Jesse_Chen
2026-08-11 00:10:24 +08:00
parent 10724e9dab
commit 8eed0603f9
3 changed files with 43 additions and 0 deletions
@@ -0,0 +1,13 @@
begin;
do $$
begin
if exists (select 1 from pg_roles where rolname = 'admin_runtime') then
drop policy if exists admin_users_admin_read on public.admin_users;
create policy admin_users_admin_read on public.admin_users
for select to admin_runtime using (true);
end if;
end;
$$;
commit;
+9
View File
@@ -15,6 +15,10 @@ const authPolicy = readFileSync(new URL("../src/lib/admin/auth-policy.ts", impor
const authFactory = readFileSync(new URL("../src/modules/identity/auth-factory.ts", import.meta.url), "utf8"); const authFactory = readFileSync(new URL("../src/modules/identity/auth-factory.ts", import.meta.url), "utf8");
const adminHttp = readFileSync(new URL("../src/lib/admin/http.ts", import.meta.url), "utf8"); const adminHttp = readFileSync(new URL("../src/lib/admin/http.ts", import.meta.url), "utf8");
const rbacMigration = readFileSync(new URL("../supabase/migrations/20260806010000_admin_rbac.sql", import.meta.url), "utf8"); const rbacMigration = readFileSync(new URL("../supabase/migrations/20260806010000_admin_rbac.sql", import.meta.url), "utf8");
const adminUsersReadPolicyMigration = readFileSync(
new URL("../supabase/migrations/20260810010000_admin_users_admin_runtime_read_policy.sql", import.meta.url),
"utf8",
);
const ownerRecoveryMigration = readFileSync(new URL("../db/migrations/20260807010000_recover_initial_admin_owner.sql", import.meta.url), "utf8"); const ownerRecoveryMigration = readFileSync(new URL("../db/migrations/20260807010000_recover_initial_admin_owner.sql", import.meta.url), "utf8");
const bootstrapRoles = readFileSync(new URL("../../deploy/postgres/001-bootstrap-roles.sh", import.meta.url), "utf8"); const bootstrapRoles = readFileSync(new URL("../../deploy/postgres/001-bootstrap-roles.sh", import.meta.url), "utf8");
const compatibilityRoles = readFileSync(new URL("../../deploy/postgres/002-ensure-business-compatibility-roles.sql", import.meta.url), "utf8"); const compatibilityRoles = readFileSync(new URL("../../deploy/postgres/002-ensure-business-compatibility-roles.sql", import.meta.url), "utf8");
@@ -281,6 +285,11 @@ test("admin runtime cannot assume service_role and keeps explicit RBAC grants",
assert.match(rbacMigration, /public\.admin_read_customer_birth_data\(uuid, uuid\[\], text\)[\s\S]*to admin_runtime/); assert.match(rbacMigration, /public\.admin_read_customer_birth_data\(uuid, uuid\[\], text\)[\s\S]*to admin_runtime/);
}); });
test("admin runtime can read administrators through RLS", () => {
assert.match(adminUsersReadPolicyMigration, /admin_users_admin_read/);
assert.match(adminUsersReadPolicyMigration, /for select to admin_runtime using \(true\)/);
});
test("last Owner revocations are serialized by one transaction advisory lock", () => { test("last Owner revocations are serialized by one transaction advisory lock", () => {
assert.match(rbacMigration, /pg_advisory_xact_lock\(1096040772, 1\)[\s\S]*v_owner_count/); assert.match(rbacMigration, /pg_advisory_xact_lock\(1096040772, 1\)[\s\S]*v_owner_count/);
}); });
@@ -18,6 +18,7 @@ const compatibilitySql = readFileSync(
"utf8", "utf8",
); );
const unknownUserId = "00000000-0000-4000-8000-000000000001"; const unknownUserId = "00000000-0000-4000-8000-000000000001";
const visibleAdminId = "00000000-0000-4000-8000-000000000002";
test("service and restricted admin database identities stay separated", async () => { test("service and restricted admin database identities stay separated", async () => {
const fixture = startPostgresFixture(); const fixture = startPostgresFixture();
@@ -74,6 +75,26 @@ test("service and restricted admin database identities stay separated", async ()
assert.equal(fixture.psql("select pg_has_role('service_runtime','service_role','MEMBER')"), "t"); assert.equal(fixture.psql("select pg_has_role('service_runtime','service_role','MEMBER')"), "t");
assert.equal(fixture.psql("select pg_has_role('admin_runtime','service_role','MEMBER')"), "f"); assert.equal(fixture.psql("select pg_has_role('admin_runtime','service_role','MEMBER')"), "f");
fixture.psqlAs("identity_runtime", "identity-runtime-test-password", `
insert into identity.users (id, name, email, email_verified, email_verified_at, role)
values ('${visibleAdminId}', 'Visible Admin', 'visible-admin@example.com', true, now(), 'admin')
`);
fixture.psql(`
insert into public.admin_users (user_id, created_by)
values ('${visibleAdminId}', '${visibleAdminId}');
insert into public.admin_user_roles (admin_user_id, role_id, assigned_by)
select '${visibleAdminId}', id, '${visibleAdminId}' from public.admin_roles where code = 'support';
`);
assert.equal(
fixture.psqlAs(
"admin_runtime",
"admin-runtime-test-password",
`select count(*) from public.admin_users where user_id = '${visibleAdminId}' and revoked_at is null`,
),
"1",
"admin list reads must not be hidden by admin_users RLS",
);
const previousAuthProvider = process.env.AUTH_PROVIDER; const previousAuthProvider = process.env.AUTH_PROVIDER;
const previousServiceDatabaseUrl = process.env.SERVICE_DATABASE_URL; const previousServiceDatabaseUrl = process.env.SERVICE_DATABASE_URL;
const previousAdminDatabaseUrl = process.env.ADMIN_DATABASE_URL; const previousAdminDatabaseUrl = process.env.ADMIN_DATABASE_URL;