// 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 (Container isms-tool-postgres-1); .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 = "asset: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 } | 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("risk:accept") === 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); });