Files
craftvia/scripts/test-action-guard-authz.ts
T
msolarczekandClaude Opus 5 1701db0a62 Fundament: Doku, Testanpassungen, Restbereinigung
- AGENTS.md und README.md auf Craftvia umgeschrieben (Regeln, Andockpunkte,
  Stack mit Garage, tsx-Tests, Gate, Demo-Logins)
- ISMS-Dokumente entfernt (SPEC, Prototypen, Lane-Prompts, Übergaben, Konzepte
  Incidents/Framework); Fundament-Doku (Deploy, Sicherheit, Backup, Identity) bleibt
- Fundament-Tests an Craftvia-Rollen/Branding angepasst, Resttreffer
  certvia/isms in Skripten und Kommentaren bereinigt

Gate: prisma generate, migrate status, tsc, lint, build, 22/22 Testskripte grün.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 11:44:58 +02:00

133 lines
5.6 KiB
TypeScript

// Nachweis der autoritativen Rechte-/Statusprüfung im moduleGuard (Sicherheitsbefund F-06).
//
// Der moduleGuard prüft Kontostatus, Passwortzwang und effektive Rechte NICHT mehr
// aus dem JWT, sondern autoritativ aus der Datenbank (src/server/action-guard.ts).
// Dieser Test repliziert die exakte autoritative Query und weist nach, dass die
// Guard-Entscheidung korrekt kippt, sobald sich der DB-Zustand ändert — ohne dass
// ein neues Login (Token) nötig wäre. (Ein direkter moduleGuard-Aufruf würde eine
// NextAuth-Session voraussetzen; die sicherheitsrelevante Logik ist die Query.)
//
// Lauf: npx tsx scripts/test-action-guard-authz.ts
// Nutzt die lokale Postgres-DB (Compose-Service postgres); .env liegt im Worktree.
import "dotenv/config";
import { prisma } from "../src/server/db";
let failures = 0;
const ok = (cond: boolean, msg: string) => {
console.log(`${cond ? "✓" : "✗ FEHLER"} ${msg}`);
if (!cond) failures++;
};
const SLUG = "zz-f06-authz-test";
const EMAIL = "f06-user@zz-authz.test";
const PERM = "customer:write"; // existiert im globalen Permissionskatalog
/**
* Repliziert die autoritative Prüfung aus moduleGuard (Option C): Membership-Status +
* effektive Rechte kommen aus `User`, Passwortzwang + globaler Status aus der `Identity`.
* Liefert `null`, wenn Mitgliedschaft ODER Identity nicht (mehr) aktiv ist.
*/
async function authorize(userId: string, identityId: string): Promise<{ mustChangePassword: boolean; perms: Set<string> } | null> {
const account = await prisma.user.findFirst({
where: { id: userId, status: "ACTIVE" },
select: {
userRoles: {
select: {
role: {
select: {
rolePermissions: { select: { permission: { select: { key: true } } } },
},
},
},
},
},
});
const identity = await prisma.identity.findUnique({
where: { id: identityId },
select: { status: true, mustChangePassword: true },
});
if (!account || !identity || identity.status !== "ACTIVE") return null;
return {
mustChangePassword: identity.mustChangePassword,
perms: new Set(account.userRoles.flatMap((ur) => ur.role.rolePermissions.map((rp) => rp.permission.key))),
};
}
async function cleanup() {
const t = await prisma.tenant.findUnique({ where: { slug: SLUG } });
if (!t) return;
await prisma.user.deleteMany({ where: { tenantId: t.id } });
await prisma.role.deleteMany({ where: { tenantId: t.id } });
await prisma.tenant.delete({ where: { id: t.id } });
// Verwaiste Test-Identity(s) entfernen (Membership wurde eben gelöscht).
await prisma.identity.deleteMany({ where: { email: EMAIL, memberships: { none: {} } } });
}
async function main() {
await cleanup();
const tenant = await prisma.tenant.create({ data: { name: "F06 AuthZ Test", slug: SLUG } });
const perm = await prisma.permission.findFirstOrThrow({ where: { key: PERM } });
const role = await prisma.role.create({
data: {
tenantId: tenant.id,
key: "f06-role",
name: "F06 Rolle",
rolePermissions: { create: [{ permissionId: perm.id }] },
},
});
// Option C: Mitgliedschaft braucht eine globale Identity (Anmeldung).
const identity = await prisma.identity.create({ data: { email: EMAIL, passwordHash: "x" } });
const user = await prisma.user.create({
data: {
tenantId: tenant.id,
identityId: identity.id,
email: EMAIL,
name: "F06 User",
status: "ACTIVE",
userRoles: { create: [{ roleId: role.id }] },
},
});
// (1) Aktives Konto mit Recht → Guard ließe die Mutation zu.
const a1 = await authorize(user.id, identity.id);
ok(a1 !== null, "(1) aktives Konto wird gefunden");
ok(a1?.perms.has(PERM) === true, `(1) effektive Rechte enthalten ${PERM}`);
ok(a1?.perms.has("customer:merge") === false, "(1) nicht vergebenes Recht fehlt korrekt");
// (2) Membership deaktiviert → Query liefert null → Guard wirft „Konto ist nicht aktiv".
await prisma.user.update({ where: { id: user.id }, data: { status: "DEACTIVATED" } });
const a2 = await authorize(user.id, identity.id);
ok(a2 === null, "(2) deaktivierte Mitgliedschaft → null (Mutation sofort geblockt)");
// (2b) Membership aktiv, aber IDENTITY deaktiviert → ebenfalls null (globale Sperre).
await prisma.user.update({ where: { id: user.id }, data: { status: "ACTIVE" } });
await prisma.identity.update({ where: { id: identity.id }, data: { status: "DISABLED" } });
ok((await authorize(user.id, identity.id)) === null, "(2b) deaktivierte Identity → null (globale Sperre wirkt)");
await prisma.identity.update({ where: { id: identity.id }, data: { status: "ACTIVE" } });
// (3) Recht entzogen → Recht fehlt → Guard wirft ForbiddenError.
await prisma.rolePermission.delete({ where: { roleId_permissionId: { roleId: role.id, permissionId: perm.id } } });
const a3 = await authorize(user.id, identity.id);
ok(a3 !== null, "(3) Konto aktiv");
ok(a3?.perms.has(PERM) === false, `(3) entzogenes Recht ${PERM} sofort weg (kein Warten auf neues Login)`);
// (4) Passwortzwang an der Identity → Guard wirft „Passwortwechsel erforderlich".
await prisma.identity.update({ where: { id: identity.id }, data: { mustChangePassword: true } });
const a4 = await authorize(user.id, identity.id);
ok(a4?.mustChangePassword === true, "(4) mustChangePassword (Identity) wird autoritativ erkannt");
await cleanup();
if (failures === 0) console.log("\nOK — alle F-06-Nachweise (1)-(4) erfüllt.");
else console.log(`\n${failures} FEHLER.`);
process.exit(failures === 0 ? 0 : 1);
}
main().catch(async (e) => {
console.error(e);
await cleanup().catch(() => {});
process.exit(1);
});