feat(quick-260910-jab): drei zu kurz greifende RLS-Regeln schliessen (T-JTS-02, T-JTS-03, WINDOWS #19)
- Neue, handgeschriebene Migration 20260910120000_rls_widen_membership_grant_and_platform_read: GroupMembership prueft jetzt beide Seiten der Beziehung (Gruppe UND Benutzer), ModuleGrant prueft zusaetzlich die referenzierte Gruppe/den referenzierten Benutzer (mit Leer-Zulassung, D-04), TenderRssFeedSource bekommt vier nach Befehl getrennte Regeln statt einer (Lesen schliesst plattformweite Zeilen ein, Schreiben verlangt weiterhin einen Mandanten). SearchProvider bewusst unveraendert (Befund E: Praemisse widerlegt). Lokal angewandt und gegen den Systemkatalog der lebenden Datenbank gemessen. Der Schalter bleibt aus (Rolle tessera). - rls-scratch-check.mjs: die drei loch-behauptenden Pruefungen umgekehrt (nicht geloescht), Gegenmessungen ueber die Wartungsrolle ergaenzt, vier Befehlsrichtungen fuer TenderRssFeedSource gemessen, neuer Abschnitt fuer SearchProvider, Extraktion auf die neue Migration umgeleitet und um eine mehrfach-treffer-faehige Form ergaenzt (extractAllPolicySql). - migration-sql.spec.ts: neuer Beschreibungsblock fuer die neue Migration. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
This commit is contained in:
@@ -26,6 +26,24 @@ function readMigrationSql(suffix: string): string {
|
||||
return readFileSync(join(MIGRATIONS_DIR, dirs[0], 'migration.sql'), 'utf-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Schneidet eine einzelne `CREATE POLICY <name?> ON "<Tabelle>" ... ;`
|
||||
* -Anweisung aus dem Migrationstext, wortgleich zu `extractPolicySql()` /
|
||||
* `extractAllPolicySql()` in apps/api/scripts/rls-scratch-check.mjs — reiner
|
||||
* Textabgleich, keine Datenbank. Ohne `policyName` wird der erste Treffer
|
||||
* fuer die Tabelle genommen (fuer Tabellen mit genau einer Regel);
|
||||
* `policyName` waehlt gezielt eine von mehreren (TenderRssFeedSource).
|
||||
*/
|
||||
function extractPolicyBlock(sql: string, tableName: string, policyName?: string): string {
|
||||
const name = policyName ?? '\\w+';
|
||||
const re = new RegExp(`CREATE POLICY ${name} ON "${tableName}"[\\s\\S]*?;`);
|
||||
const match = sql.match(re);
|
||||
if (!match) {
|
||||
throw new Error(`CREATE POLICY fuer "${tableName}"${policyName ? ` (${policyName})` : ''} nicht gefunden`);
|
||||
}
|
||||
return match[0];
|
||||
}
|
||||
|
||||
describe('add_groups_and_module_grants migration.sql (D-04, D-06, D-13)', () => {
|
||||
const sql = readMigrationSql('_add_groups_and_module_grants');
|
||||
|
||||
@@ -95,6 +113,66 @@ describe('groups_rls_policies migration.sql (T-15-11)', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('rls_widen_membership_grant_and_platform_read migration.sql (T-JTS-02, T-JTS-03, WINDOWS #19)', () => {
|
||||
const sql = readMigrationSql('_rls_widen_membership_grant_and_platform_read');
|
||||
|
||||
it('loest die abgeloesten Regeln auf GroupMembership und ModuleGrant ab (DROP + CREATE unter demselben Namen)', () => {
|
||||
expect(sql).toContain('DROP POLICY tenant_isolation_policy ON "GroupMembership"');
|
||||
expect(sql).toContain('DROP POLICY tenant_isolation_policy ON "ModuleGrant"');
|
||||
const groupMembershipCreates = (
|
||||
sql.match(/CREATE POLICY tenant_isolation_policy ON "GroupMembership"/g) ?? []
|
||||
).length;
|
||||
const moduleGrantCreates = (
|
||||
sql.match(/CREATE POLICY tenant_isolation_policy ON "ModuleGrant"/g) ?? []
|
||||
).length;
|
||||
expect(groupMembershipCreates).toBe(1);
|
||||
expect(moduleGrantCreates).toBe(1);
|
||||
});
|
||||
|
||||
it('die neue GroupMembership-Regel prueft die Benutzerseite UND (mit UND verknuepft) die Gruppenseite', () => {
|
||||
const policy = extractPolicyBlock(sql, 'GroupMembership');
|
||||
expect(policy).toContain('SELECT "id" FROM "Group" WHERE "tenantId" = current_tenant_id()');
|
||||
expect(policy).toContain('SELECT "id" FROM "User" WHERE "tenantId" = current_tenant_id()');
|
||||
expect(policy).toMatch(/AND\s+"userId"\s+IN/);
|
||||
});
|
||||
|
||||
it('die neue ModuleGrant-Regel prueft die referenzierte Gruppe UND den referenzierten Benutzer, beide mit Leer-Zulassung (D-04)', () => {
|
||||
const policy = extractPolicyBlock(sql, 'ModuleGrant');
|
||||
expect(policy).toContain('"groupId" IS NULL');
|
||||
expect(policy).toContain('"userId" IS NULL');
|
||||
expect(policy).toContain('SELECT "id" FROM "Group" WHERE "tenantId" = current_tenant_id()');
|
||||
expect(policy).toContain('SELECT "id" FROM "User" WHERE "tenantId" = current_tenant_id()');
|
||||
});
|
||||
|
||||
it('loest die abgeloeste Regel auf TenderRssFeedSource ab und legt genau vier nach Befehl getrennte Regeln an', () => {
|
||||
expect(sql).toContain('DROP POLICY tenant_isolation_policy ON "TenderRssFeedSource"');
|
||||
for (const name of [
|
||||
'tenant_platform_read_policy',
|
||||
'tenant_insert_policy',
|
||||
'tenant_update_policy',
|
||||
'tenant_delete_policy',
|
||||
]) {
|
||||
expect(sql).toContain(`CREATE POLICY ${name} ON "TenderRssFeedSource"`);
|
||||
}
|
||||
});
|
||||
|
||||
it('ausschliesslich die Leseregel auf TenderRssFeedSource laesst Zeilen ohne Mandant zu', () => {
|
||||
const readPolicy = extractPolicyBlock(sql, 'TenderRssFeedSource', 'tenant_platform_read_policy');
|
||||
const insertPolicy = extractPolicyBlock(sql, 'TenderRssFeedSource', 'tenant_insert_policy');
|
||||
const updatePolicy = extractPolicyBlock(sql, 'TenderRssFeedSource', 'tenant_update_policy');
|
||||
const deletePolicy = extractPolicyBlock(sql, 'TenderRssFeedSource', 'tenant_delete_policy');
|
||||
|
||||
expect(readPolicy).toContain('IS NULL');
|
||||
for (const writePolicy of [insertPolicy, updatePolicy, deletePolicy]) {
|
||||
expect(writePolicy).not.toContain('IS NULL');
|
||||
}
|
||||
});
|
||||
|
||||
it('fasst SearchProvider nicht an — kein DROP POLICY und kein CREATE POLICY fuer diese Tabelle', () => {
|
||||
expect(sql).not.toMatch(/(DROP|CREATE) POLICY [\w ]*ON "SearchProvider"/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('add_group_internal_name_and_object_guid migration.sql (D-04)', () => {
|
||||
const sql = readMigrationSql('_add_group_internal_name_and_object_guid');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user