fix(admin): allow administrator list reads
This commit is contained in:
@@ -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;
|
||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user