feat(quick-260909-ipc): Fehlerrichtung des Bereichs ldap messen und schriftlich festhalten
Aufgabe 1 der Etappe 2: erweitert das Wegwerf-Werkzeug rls-scratch-check.mjs um fuenf Messungen des forTenant()-Musters gegen die echte, aus der ausgelieferten Migration geschnittene LdapConfig/LdapFieldMapping-Policy unter einer Rolle ohne BYPASSRLS. Belegt insbesondere, dass ein ungebundener Zugriff nach dem Scharfschalten 0 Zeilen liefert, nicht alle -- die Fehlerrichtung dreht sich um. Die neue Kritikschrift docs/mandantentrennung-etappe2-fehlerrichtung.md haelt das schriftlich fest, mit Signaltabelle je Pfad und den vier Stellen, die Leere als Abwesenheit deuten. Kein Dienstcode angefasst. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
This commit is contained in:
@@ -360,6 +360,181 @@ async function runAuthLookupChecks(adminUrl, scratchRoleUrl, results) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Liest die ausgelieferte RLS-Basismigration und schneidet die beiden
|
||||
* `CREATE POLICY`-Anweisungen fuer "LdapConfig" und "LdapFieldMapping" bis
|
||||
* zum abschliessenden Semikolon heraus (Vorbild: readAuthLookupMigrationSql).
|
||||
* Der Dateiname wird ueber ein Suffix gesucht, nicht hartkodiert — aber die
|
||||
* Groups-Migration endet ebenfalls auf "_rls_policies" und wird deshalb
|
||||
* ausdruecklich ausgeschlossen, sonst faende der Filter zwei Verzeichnisse.
|
||||
*/
|
||||
function readRlsPoliciesMigrationSql() {
|
||||
const dirs = readdirSync(MIGRATIONS_DIR, { withFileTypes: true })
|
||||
.filter(
|
||||
(entry) =>
|
||||
entry.isDirectory() &&
|
||||
entry.name.endsWith('_rls_policies') &&
|
||||
!entry.name.endsWith('_groups_rls_policies'),
|
||||
)
|
||||
.map((entry) => entry.name);
|
||||
if (dirs.length !== 1) return null;
|
||||
return readFileSync(join(MIGRATIONS_DIR, dirs[0], 'migration.sql'), 'utf-8');
|
||||
}
|
||||
|
||||
function extractPolicySql(migrationSql, tableName) {
|
||||
const re = new RegExp(
|
||||
`CREATE POLICY tenant_isolation_policy ON "${tableName}"[\\s\\S]*?;`,
|
||||
);
|
||||
const match = migrationSql.match(re);
|
||||
return match ? match[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Aufgabe 1 (260909-ipc) — misst die fuenf im Plan genannten Verhaltensweisen
|
||||
* des Bereichs ldap unter der Rolle ohne BYPASSRLS, mit den beiden Policies
|
||||
* WORTGLEICH aus der ausgelieferten Migration statt im Werkzeug neu getippt
|
||||
* (T-IPC-08). Findet die Extraktion eine der beiden Policies nicht, meldet
|
||||
* dieser Abschnitt eine FEHLGESCHLAGENE Pruefung und bricht ab, statt mit
|
||||
* einer geratenen Policy weiterzumessen.
|
||||
*/
|
||||
async function runLdapAreaChecks(adminUrl, scratchRoleUrl, results) {
|
||||
const migrationSql = readRlsPoliciesMigrationSql();
|
||||
const ldapConfigPolicy = migrationSql
|
||||
? extractPolicySql(migrationSql, 'LdapConfig')
|
||||
: null;
|
||||
const ldapFieldMappingPolicy = migrationSql
|
||||
? extractPolicySql(migrationSql, 'LdapFieldMapping')
|
||||
: null;
|
||||
|
||||
if (!ldapConfigPolicy || !ldapFieldMappingPolicy) {
|
||||
report(
|
||||
results,
|
||||
'ldap-policies-aus-migration-gefunden',
|
||||
false,
|
||||
'CREATE POLICY fuer "LdapConfig" und/oder "LdapFieldMapping" nicht in der ausgelieferten *_rls_policies-Migration gefunden',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await withAdminPrisma(urlForDatabase(adminUrl, SCRATCH_DB_NAME).toString(), async (db) => {
|
||||
await db.$executeRawUnsafe(`
|
||||
CREATE TABLE "LdapConfig" (
|
||||
id text PRIMARY KEY,
|
||||
"tenantId" text NOT NULL,
|
||||
"serverUrl" text NOT NULL
|
||||
);
|
||||
`);
|
||||
await db.$executeRawUnsafe(`
|
||||
CREATE TABLE "LdapFieldMapping" (
|
||||
id text PRIMARY KEY,
|
||||
"ldapConfigId" text NOT NULL REFERENCES "LdapConfig"(id),
|
||||
"ldapField" text NOT NULL,
|
||||
"tesseraField" text NOT NULL
|
||||
);
|
||||
`);
|
||||
await db.$executeRawUnsafe(`ALTER TABLE "LdapConfig" ENABLE ROW LEVEL SECURITY;`);
|
||||
await db.$executeRawUnsafe(`ALTER TABLE "LdapConfig" FORCE ROW LEVEL SECURITY;`);
|
||||
await db.$executeRawUnsafe(`ALTER TABLE "LdapFieldMapping" ENABLE ROW LEVEL SECURITY;`);
|
||||
await db.$executeRawUnsafe(`ALTER TABLE "LdapFieldMapping" FORCE ROW LEVEL SECURITY;`);
|
||||
await db.$executeRawUnsafe(ldapConfigPolicy);
|
||||
await db.$executeRawUnsafe(ldapFieldMappingPolicy);
|
||||
await db.$executeRawUnsafe(
|
||||
`GRANT SELECT, INSERT, UPDATE, DELETE ON "LdapConfig" TO ${SCRATCH_ROLE_NAME}`,
|
||||
);
|
||||
await db.$executeRawUnsafe(
|
||||
`GRANT SELECT, INSERT, UPDATE, DELETE ON "LdapFieldMapping" TO ${SCRATCH_ROLE_NAME}`,
|
||||
);
|
||||
await db.$executeRawUnsafe(`
|
||||
INSERT INTO "LdapConfig" (id, "tenantId", "serverUrl") VALUES
|
||||
('cfg-a', 'TENANT-A', 'ldap://a.example'),
|
||||
('cfg-b', 'TENANT-B', 'ldap://b.example');
|
||||
`);
|
||||
await db.$executeRawUnsafe(`
|
||||
INSERT INTO "LdapFieldMapping" (id, "ldapConfigId", "ldapField", "tesseraField") VALUES
|
||||
('map-a', 'cfg-a', 'sAMAccountName', 'username'),
|
||||
('map-b', 'cfg-b', 'sAMAccountName', 'username');
|
||||
`);
|
||||
});
|
||||
|
||||
const prisma = new PrismaClient({ datasourceUrl: scratchRoleUrl });
|
||||
try {
|
||||
// 1: forTenant(TENANT-A) sieht genau die LdapConfig-Zeile von A.
|
||||
const configRowsForA = await forTenantQuery(prisma, 'TENANT-A', (tx) =>
|
||||
tx.$queryRaw`SELECT "tenantId" FROM "LdapConfig" ORDER BY id`,
|
||||
);
|
||||
report(
|
||||
results,
|
||||
'ldapconfig-gebunden-nur-eigene-zeile',
|
||||
configRowsForA.length === 1 && configRowsForA[0].tenantId === 'TENANT-A',
|
||||
`forTenant(TENANT-A) liefert ${configRowsForA.length} Zeile(n): ${JSON.stringify(configRowsForA.map((r) => r.tenantId))}`,
|
||||
);
|
||||
|
||||
// 2: derselbe SELECT ohne Bindung liefert 0 Zeilen — die Fehlerrichtung,
|
||||
// an der echten Policy gemessen statt an der Hilfstabelle "probe".
|
||||
const unboundConfigRows = await prisma.$queryRaw`SELECT "tenantId" FROM "LdapConfig"`;
|
||||
report(
|
||||
results,
|
||||
'ldapconfig-ungebunden-null-zeilen',
|
||||
unboundConfigRows.length === 0,
|
||||
`ungebundener SELECT auf "LdapConfig" liefert ${unboundConfigRows.length} Zeile(n)`,
|
||||
);
|
||||
|
||||
// 3: forTenant(TENANT-A) sieht ueber den Join genau die Feldzuordnung,
|
||||
// die an A's Konfiguration haengt.
|
||||
const mappingRowsForA = await forTenantQuery(prisma, 'TENANT-A', (tx) =>
|
||||
tx.$queryRaw`SELECT "ldapConfigId" FROM "LdapFieldMapping" ORDER BY id`,
|
||||
);
|
||||
report(
|
||||
results,
|
||||
'fieldmapping-folgt-join-auf-ldapconfig',
|
||||
mappingRowsForA.length === 1 && mappingRowsForA[0].ldapConfigId === 'cfg-a',
|
||||
`forTenant(TENANT-A) liefert ${mappingRowsForA.length} Feldzuordnung(en): ${JSON.stringify(mappingRowsForA.map((r) => r.ldapConfigId))}`,
|
||||
);
|
||||
|
||||
// 4: gebundenes INSERT mit A's eigener ldapConfigId gelingt.
|
||||
let ownInsertOk = false;
|
||||
let ownInsertDetail = '';
|
||||
try {
|
||||
await forTenantQuery(
|
||||
prisma,
|
||||
'TENANT-A',
|
||||
(tx) =>
|
||||
tx.$executeRaw`INSERT INTO "LdapFieldMapping" (id, "ldapConfigId", "ldapField", "tesseraField") VALUES ('map-a-2', 'cfg-a', 'mail', 'email')`,
|
||||
);
|
||||
ownInsertOk = true;
|
||||
ownInsertDetail = 'INSERT mit eigener ldapConfigId erfolgreich';
|
||||
} catch (err) {
|
||||
ownInsertDetail = `INSERT mit eigener ldapConfigId fehlgeschlagen: ${err.message}`;
|
||||
}
|
||||
report(results, 'fieldmapping-schreiben-eigene-konfiguration-erlaubt', ownInsertOk, ownInsertDetail);
|
||||
|
||||
// 5: gebundenes INSERT unter TENANT-A mit B's ldapConfigId wird
|
||||
// abgewiesen — die Abweisung IST das bestandene Ergebnis.
|
||||
let foreignInsertRejected = false;
|
||||
let foreignInsertDetail = '';
|
||||
try {
|
||||
await forTenantQuery(
|
||||
prisma,
|
||||
'TENANT-A',
|
||||
(tx) =>
|
||||
tx.$executeRaw`INSERT INTO "LdapFieldMapping" (id, "ldapConfigId", "ldapField", "tesseraField") VALUES ('map-foreign', 'cfg-b', 'mail', 'email')`,
|
||||
);
|
||||
foreignInsertDetail = 'INSERT mit fremder ldapConfigId ist NICHT fehlgeschlagen';
|
||||
} catch (err) {
|
||||
foreignInsertRejected = true;
|
||||
foreignInsertDetail = `INSERT mit fremder ldapConfigId abgewiesen: ${err.message}`;
|
||||
}
|
||||
report(
|
||||
results,
|
||||
'fieldmapping-schreiben-fremde-konfiguration-abgelehnt',
|
||||
foreignInsertRejected,
|
||||
foreignInsertDetail,
|
||||
);
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const adminUrl = parseAdminUrl();
|
||||
const results = [];
|
||||
@@ -375,6 +550,7 @@ async function main() {
|
||||
|
||||
await runForTenantChecks(scratchRoleUrlString, results);
|
||||
await runAuthLookupChecks(adminUrl, scratchRoleUrlString, results);
|
||||
await runLdapAreaChecks(adminUrl, scratchRoleUrlString, results);
|
||||
} finally {
|
||||
console.log(`Raeume Wegwerf-Datenbank "${SCRATCH_DB_NAME}" ab...`);
|
||||
await teardownScratchDatabase(adminUrl);
|
||||
|
||||
Reference in New Issue
Block a user