- 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>
133 lines
5.6 KiB
TypeScript
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);
|
|
});
|