Tests: vollständige Suite auch mit RLS_ENFORCED=true grün
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@
|
|||||||
* Lauf: npm run test (alle)
|
* Lauf: npm run test (alle)
|
||||||
* npm run test -- mail tenant (nur Tests, deren Name einen der Filter enthält)
|
* npm run test -- mail tenant (nur Tests, deren Name einen der Filter enthält)
|
||||||
*/
|
*/
|
||||||
|
import "dotenv/config";
|
||||||
import { spawnSync } from "node:child_process";
|
import { spawnSync } from "node:child_process";
|
||||||
import { readdirSync } from "node:fs";
|
import { readdirSync } from "node:fs";
|
||||||
import { join, dirname } from "node:path";
|
import { join, dirname } from "node:path";
|
||||||
@@ -26,6 +27,15 @@ if (tests.length === 0) {
|
|||||||
process.exit(1);
|
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 }[] = [];
|
const results: { name: string; ok: boolean; ms: number }[] = [];
|
||||||
for (const file of tests) {
|
for (const file of tests) {
|
||||||
const started = Date.now();
|
const started = Date.now();
|
||||||
|
|||||||
@@ -29,6 +29,21 @@ async function expectThrow(fn: () => Promise<unknown>, 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<unknown>, 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). */
|
/** Erwartet, dass `fn` `null` liefert (kein Datensatz, kein Abfluss). */
|
||||||
async function expectNull(fn: () => Promise<unknown>, msg: string) {
|
async function expectNull(fn: () => Promise<unknown>, msg: string) {
|
||||||
const r = await fn();
|
const r = await fn();
|
||||||
@@ -98,7 +113,7 @@ async function main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// (6) Compound-Unique-Key (tenantId_key) mit fremdem tenantId, select ohne tenantId.
|
// (6) Compound-Unique-Key (tenantId_key) mit fremdem tenantId, select ohne tenantId.
|
||||||
await expectThrow(
|
await expectThrowOrNullUnderRls(
|
||||||
() =>
|
() =>
|
||||||
dbA.role.findUnique({
|
dbA.role.findUnique({
|
||||||
where: { tenantId_key: { tenantId: tenantB.id, key: roleB.key } },
|
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.
|
// (7) Compound-Unique-Key mit fremdem tenantId, ohne select.
|
||||||
await expectThrow(
|
await expectThrowOrNullUnderRls(
|
||||||
() =>
|
() =>
|
||||||
dbA.role.findUnique({
|
dbA.role.findUnique({
|
||||||
where: { tenantId_key: { tenantId: tenantB.id, key: roleB.key } },
|
where: { tenantId_key: { tenantId: tenantB.id, key: roleB.key } },
|
||||||
|
|||||||
Reference in New Issue
Block a user