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>
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
// 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);
|
||||
});
|
||||
Reference in New Issue
Block a user