Files
craftvia/scripts/test-action-guard-authz.ts
msolarczekandClaude Opus 5 c8e6f30a27
CI / build-and-check (push) Canceled after 0s
CI / audit (push) Canceled after 0s
CI / sbom (push) Canceled after 0s
Basis: Certvia dev@a48c5fb als Fundament für Craftvia
Unveränderter Stand von certvia/dev (a48c5fb) plus Craftvia-Spezifikation
und Brandbook unter docs/craftvia/. ISMS-Module werden im Folgecommit entfernt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 11:05:39 +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 (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<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("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);
});