feat(15-01): RLS policies for Group/GroupMembership/ModuleGrant (T-15-11)
- Second, deliberately separate migration (pure hand-SQL, no Prisma- generated DDL): ENABLE/FORCE ROW LEVEL SECURITY plus a tenant_isolation_policy for each of the three new tables, following the pattern of 20260618112133_rls_policies (Auth-Kerntabellen) rather than the RLS-exempt Tender* app-layer tables - Group/ModuleGrant compare tenantId directly against current_tenant_id(); GroupMembership has no own tenantId and follows the PasswordResetToken join pattern (groupId IN (SELECT id FROM Group WHERE tenantId = ...)) - migration-sql.spec.ts extended with a second describe block covering both migration files (6x ROW LEVEL SECURITY, 3x CREATE POLICY, the join vs. direct-comparison shape) - Re-ran the Task-2 end-to-end proof after applying this migration: identical result (USER without grant 403 + empty list, USER with direct grant 200 + slug present, ADMIN 200) — the app's DB role (tessera) is a Postgres superuser with rolbypassrls=true, so it bypasses RLS as documented as an acceptable outcome by the plan; RLS remains the defense-in-depth net for any future non-superuser connection
This commit is contained in:
@@ -66,3 +66,31 @@ describe('add_groups_and_module_grants migration.sql (D-04, D-06, D-13)', () =>
|
||||
expect(membershipIdx).toBeLessThan(grantIdx);
|
||||
});
|
||||
});
|
||||
|
||||
describe('groups_rls_policies migration.sql (T-15-11)', () => {
|
||||
const sql = readMigrationSql('_groups_rls_policies');
|
||||
|
||||
it('aktiviert ENABLE und FORCE ROW LEVEL SECURITY für alle drei Tabellen (6 Anweisungen)', () => {
|
||||
const occurrences = sql.match(/ROW LEVEL SECURITY/g) ?? [];
|
||||
expect(occurrences.length).toBe(6);
|
||||
for (const table of ['"Group"', '"GroupMembership"', '"ModuleGrant"']) {
|
||||
expect(sql).toContain(`ALTER TABLE ${table} ENABLE ROW LEVEL SECURITY`);
|
||||
expect(sql).toContain(`ALTER TABLE ${table} FORCE ROW LEVEL SECURITY`);
|
||||
}
|
||||
});
|
||||
|
||||
it('legt für jede der drei Tabellen eine tenant_isolation_policy an (3 CREATE POLICY)', () => {
|
||||
const occurrences = sql.match(/CREATE POLICY tenant_isolation_policy/g) ?? [];
|
||||
expect(occurrences.length).toBe(3);
|
||||
});
|
||||
|
||||
it('GroupMembership folgt dem Join-Muster (kein direktes tenantId, Join über Group)', () => {
|
||||
expect(sql).toContain('"groupId" IN (');
|
||||
expect(sql).toContain('SELECT "id" FROM "Group" WHERE "tenantId" = current_tenant_id()');
|
||||
});
|
||||
|
||||
it('Group und ModuleGrant vergleichen direkt gegen current_tenant_id() (eigene tenantId-Spalte)', () => {
|
||||
const directPolicyCount = (sql.match(/USING \("tenantId" = current_tenant_id\(\)\)/g) ?? []).length;
|
||||
expect(directPolicyCount).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user