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,90 @@
|
||||
import { requireSession } from "@/server/auth";
|
||||
import { dbForTenant, prisma } from "@/server/db";
|
||||
import { ForbiddenError, type Permission } from "@/server/rbac";
|
||||
import { assertModuleEnabled } from "@/server/modules";
|
||||
import { writeAuditLog } from "@/server/audit";
|
||||
import { isTokenStillValid } from "@/server/sessions";
|
||||
|
||||
/**
|
||||
* Einheitlicher Einstieg für mutierende Server-Actions eines gegateten Moduls (§3.4).
|
||||
* Reihenfolge: Session → Kontostatus/Rechte (autoritativ aus der DB) →
|
||||
* Modul-Aktivierung (wirft bei Deaktivierung + Audit). Gibt Session und den
|
||||
* mandantengebundenen Prisma-Client zurück.
|
||||
*
|
||||
* `moduleGuard("assets")` erzeugt den Guard eines Moduls; jede Action ruft ihn mit
|
||||
* den benötigten Rechten auf, z. B. `const { session, db } = await guard("asset:write")`.
|
||||
* Mehrere Rechte werden alle geprüft (z. B. `guard("risk:write", "measure:write")`).
|
||||
*
|
||||
* F-06: Kontostatus, Passwortzwang und effektive Rechte werden AUTORITATIV aus der
|
||||
* Datenbank geprüft, NICHT aus dem JWT. Sonst blieben ein deaktiviertes Konto und
|
||||
* ein entzogenes Recht bei schreibenden Aktionen bis zum Token-Ablauf wirksam,
|
||||
* obwohl die UI/Layout-Guards (Lesepfade) bereits sperren. Die Layout-Guards
|
||||
* (`(app)/layout.tsx`) decken die Seitenaufrufe ab, dieser Guard die Mutationen.
|
||||
* Kostet eine zusätzliche Query pro Mutation — vertretbar, da Mutationen ohnehin
|
||||
* mehrere Queries ausführen.
|
||||
*
|
||||
* Der Vollständigkeitscheck (`scripts/check-module-guards.ts`) verlässt sich darauf,
|
||||
* dass jede Action eines gegateten Moduls über einen so erzeugten `guard(...)` läuft.
|
||||
*/
|
||||
export function moduleGuard(moduleKey: string) {
|
||||
return async function guard(...permissions: Permission[]) {
|
||||
const session = await requireSession();
|
||||
const db = dbForTenant(session.user.tenantId);
|
||||
|
||||
// Mitgliedschaft (Status ACTIVE) + effektive Rechte des AKTIVEN Mandanten
|
||||
// autoritativ aus der DB (F-06).
|
||||
const account = await db.user.findFirst({
|
||||
where: { id: session.user.id, status: "ACTIVE" },
|
||||
select: {
|
||||
userRoles: {
|
||||
select: {
|
||||
role: {
|
||||
select: {
|
||||
rolePermissions: { select: { permission: { select: { key: true } } } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
// Option C (WS4): Passwortzwang, Kill-Switch und globaler Sperrstatus gehören der
|
||||
// GLOBALEN Identity — separat und autoritativ aus der DB.
|
||||
const identity = session.user.identityId
|
||||
? await prisma.identity.findUnique({
|
||||
where: { id: session.user.identityId },
|
||||
select: { status: true, mustChangePassword: true, sessionsValidAfter: true },
|
||||
})
|
||||
: null;
|
||||
if (!account || !identity || identity.status !== "ACTIVE") {
|
||||
// Deaktiviertes/entferntes Konto (Membership ODER Identity) versucht eine
|
||||
// Mutation → als Sicherheitsereignis protokollieren (nicht nur ablehnen).
|
||||
await writeAuditLog({
|
||||
tenantId: session.user.tenantId,
|
||||
actorId: session.user.id,
|
||||
action: "denied",
|
||||
entity: "account_inactive",
|
||||
entityId: session.user.id,
|
||||
});
|
||||
throw new Error("Konto ist nicht aktiv.");
|
||||
}
|
||||
// SEC2: entwertete Session (Kill-Switch an der Identity) darf keine Mutation mehr.
|
||||
if (!isTokenStillValid(session.user.tokenIssuedAt, identity.sessionsValidAfter)) {
|
||||
throw new Error("Sitzung ist nicht mehr gueltig. Bitte neu anmelden.");
|
||||
}
|
||||
if (identity.mustChangePassword) throw new Error("Passwortwechsel erforderlich.");
|
||||
|
||||
if (permissions.length > 0) {
|
||||
const effective = new Set(
|
||||
account.userRoles.flatMap((ur) =>
|
||||
ur.role.rolePermissions.map((rp) => rp.permission.key),
|
||||
),
|
||||
);
|
||||
for (const permission of permissions) {
|
||||
if (!effective.has(permission)) throw new ForbiddenError(permission);
|
||||
}
|
||||
}
|
||||
|
||||
await assertModuleEnabled(session, moduleKey);
|
||||
return { session, db };
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user