From 6687ef2e03f55507a6b5b2eba6c47219d17cf681 Mon Sep 17 00:00:00 2001 From: Martin Date: Mon, 14 Sep 2026 18:33:22 +0200 Subject: [PATCH] =?UTF-8?q?Tests:=20vollst=C3=A4ndige=20Suite=20auch=20mit?= =?UTF-8?q?=20RLS=5FENFORCED=3Dtrue=20gr=C3=BCn?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - test-tenant-isolation: Compound-Key mit fremdem Mandanten – im Owner-Betrieb Throw (Tenant-Guard), unter scharfer RLS liefert die DB null; beides = kein Datenabfluss - run-tests.ts: lädt .env und leitet RLS_DATABASE_URL (Rolle craftvia_app) aus DATABASE_URL ab, wenn RLS_ENFORCED=true und keine URL gesetzt ist Nachweis: RLS_ENFORCED=true npm run test → 52/52; npm run gate → 52/52. Co-Authored-By: Claude Opus 5 --- scripts/run-tests.ts | 10 ++++++++++ scripts/test-tenant-isolation.ts | 19 +++++++++++++++++-- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/scripts/run-tests.ts b/scripts/run-tests.ts index e2f6487..b3d2bc7 100644 --- a/scripts/run-tests.ts +++ b/scripts/run-tests.ts @@ -8,6 +8,7 @@ * Lauf: npm run test (alle) * npm run test -- mail tenant (nur Tests, deren Name einen der Filter enthält) */ +import "dotenv/config"; import { spawnSync } from "node:child_process"; import { readdirSync } from "node:fs"; import { join, dirname } from "node:path"; @@ -26,6 +27,15 @@ if (tests.length === 0) { process.exit(1); } +// RLS runs (RLS_ENFORCED=true): db.ts refuses to start without RLS_DATABASE_URL. Default it to the +// same database as DATABASE_URL with the restricted role (as scripts/test-rls-enforcement.ts does). +if (process.env.RLS_ENFORCED === "true" && !process.env.RLS_DATABASE_URL && process.env.DATABASE_URL) { + const url = new URL(process.env.DATABASE_URL); + url.username = "craftvia_app"; + url.password = "craftvia_app_local"; + process.env.RLS_DATABASE_URL = url.toString(); +} + const results: { name: string; ok: boolean; ms: number }[] = []; for (const file of tests) { const started = Date.now(); diff --git a/scripts/test-tenant-isolation.ts b/scripts/test-tenant-isolation.ts index 4db9194..4cc30dc 100644 --- a/scripts/test-tenant-isolation.ts +++ b/scripts/test-tenant-isolation.ts @@ -29,6 +29,21 @@ async function expectThrow(fn: () => Promise, msg: string) { } } +/** + * Compound keys with a foreign tenantId: in owner mode the tenant guard throws (fail-closed); + * with RLS_ENFORCED=true the database already hides the row, so the query returns null. + * Both outcomes mean "no data leak" — anything else fails. + */ +async function expectThrowOrNullUnderRls(fn: () => Promise, msg: string) { + if (process.env.RLS_ENFORCED !== "true") return expectThrow(fn, msg); + try { + const r = await fn(); + ok(r === null, `${msg} (RLS: null)${r === null ? "" : ` — statt null: ${JSON.stringify(r)}`}`); + } catch { + ok(true, `${msg} (RLS: Throw)`); + } +} + /** Erwartet, dass `fn` `null` liefert (kein Datensatz, kein Abfluss). */ async function expectNull(fn: () => Promise, msg: string) { const r = await fn(); @@ -98,7 +113,7 @@ async function main() { ); // (6) Compound-Unique-Key (tenantId_key) mit fremdem tenantId, select ohne tenantId. - await expectThrow( + await expectThrowOrNullUnderRls( () => dbA.role.findUnique({ where: { tenantId_key: { tenantId: tenantB.id, key: roleB.key } }, @@ -108,7 +123,7 @@ async function main() { ); // (7) Compound-Unique-Key mit fremdem tenantId, ohne select. - await expectThrow( + await expectThrowOrNullUnderRls( () => dbA.role.findUnique({ where: { tenantId_key: { tenantId: tenantB.id, key: roleB.key } },