L10a Qualität & Abnahmetests: Sicherheitstests (Rollen-Matrix, schädliche Uploads, Login-Sperre/Rate-Limit/Sessions) und Audit-Log append-only für craftvia_app
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -149,6 +149,15 @@ async function threw(fn: () => Promise<unknown>): Promise<boolean> {
|
||||
}
|
||||
}
|
||||
|
||||
/** Bulk write without effect: count 0, or refused by the database (append-only audit_logs under RLS). */
|
||||
async function noEffect(fn: () => Promise<unknown>): Promise<boolean> {
|
||||
try {
|
||||
return ((await fn()) as { count: number }).count === 0;
|
||||
} catch (err) {
|
||||
return /permission denied/i.test((err as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
function defaultRlsUrl(): string {
|
||||
const base = new URL(process.env.DATABASE_URL ?? "postgresql://localhost:5432/craftvia?schema=public");
|
||||
base.username = "craftvia_app";
|
||||
@@ -178,8 +187,8 @@ runSuite("E2E Mandantentrennung (systematisch)", [SLUG_A, SLUG_B], async () => {
|
||||
findFirst: (await d.findFirst({ where })) === null,
|
||||
findUnique: (await d.findUnique({ where }).catch(() => null)) === null,
|
||||
count: (await d.count({ where })) === 0,
|
||||
updateMany: ((await d.updateMany({ where, data: row.update })) as { count: number }).count === 0,
|
||||
deleteMany: ((await d.deleteMany({ where })) as { count: number }).count === 0,
|
||||
updateMany: await noEffect(() => d.updateMany({ where, data: row.update })),
|
||||
deleteMany: await noEffect(() => d.deleteMany({ where })),
|
||||
update: await threw(() => d.update({ where, data: row.update })),
|
||||
delete: await threw(() => d.delete({ where })),
|
||||
};
|
||||
@@ -202,13 +211,26 @@ runSuite("E2E Mandantentrennung (systematisch)", [SLUG_A, SLUG_B], async () => {
|
||||
const row = rows[model];
|
||||
if (!row) continue;
|
||||
const table = topo.nodes.get(model)!.table;
|
||||
const res = await app.$transaction(async (tx) => {
|
||||
await tx.$executeRaw`SELECT set_config('app.tenant_id', ${B.tenantId}, true)`;
|
||||
const seen = await tx.$queryRawUnsafe<{ n: number }[]>(`SELECT count(*)::int AS n FROM "${table}" WHERE id = $1`, row.id);
|
||||
const updated = await tx.$executeRawUnsafe(`UPDATE "${table}" SET id = id WHERE id = $1`, row.id);
|
||||
const deleted = await tx.$executeRawUnsafe(`DELETE FROM "${table}" WHERE id = $1`, row.id);
|
||||
return { seen: seen[0].n, updated, deleted };
|
||||
});
|
||||
/** Affected rows under tenant context B; a missing privilege (append-only audit_logs) counts as 0. */
|
||||
const asB = async (sql: string): Promise<number> => {
|
||||
try {
|
||||
return await app.$transaction(async (tx) => {
|
||||
await tx.$executeRaw`SELECT set_config('app.tenant_id', ${B.tenantId}, true)`;
|
||||
return tx.$executeRawUnsafe(sql, row.id);
|
||||
});
|
||||
} catch (err) {
|
||||
if (/permission denied/i.test((err as Error).message)) return 0;
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
const res = {
|
||||
seen: await app.$transaction(async (tx) => {
|
||||
await tx.$executeRaw`SELECT set_config('app.tenant_id', ${B.tenantId}, true)`;
|
||||
return (await tx.$queryRawUnsafe<{ n: number }[]>(`SELECT count(*)::int AS n FROM "${table}" WHERE id = $1`, row.id))[0].n;
|
||||
}),
|
||||
updated: await asB(`UPDATE "${table}" SET id = id WHERE id = $1`),
|
||||
deleted: await asB(`DELETE FROM "${table}" WHERE id = $1`),
|
||||
};
|
||||
const own = await app.$transaction(async (tx) => {
|
||||
await tx.$executeRaw`SELECT set_config('app.tenant_id', ${A.tenantId}, true)`;
|
||||
return (await tx.$queryRawUnsafe<{ n: number }[]>(`SELECT count(*)::int AS n FROM "${table}" WHERE id = $1`, row.id))[0].n;
|
||||
|
||||
Reference in New Issue
Block a user