API-seitige Modul-Durchsetzung vervollständigt (§3.4, Phase-1-Härtung Paket 1)

Der requireModule-Guard war bislang nur exemplarisch in den Policy-Actions
gesetzt. Jetzt läuft jede mutierende Server-Action eines gegateten Moduls über
einen zentralen, modul- und rechteprüfenden Guard — ein für den Mandanten
deaktiviertes Modul weist damit auch Writes serverseitig ab (nicht nur Reads
über die Route-Layouts).

- action-guard.ts: moduleGuard("<key>") erzeugt je Modul einen guard(...perms),
  der Session → Modul-Aktivierung → RBAC prüft und {session, db} zurückgibt.
- modules.ts: assertModuleEnabled(session, key) wirft (statt Redirect) bei
  Deaktivierung und protokolliert die Verweigerung (Audit-Aktion "denied").
- audit.ts: Audit-Aktion "denied" + optionaler scope-Parameter.
- Alle gegateten Action-Dateien umgestellt: assets→assets, processes→bia,
  risks→risk, measures→measures, suppliers/services→suppliers, policies→policies.
  Freigabe/Ablehnung der Richtlinien laufen jetzt ebenfalls über den Modul-Guard.
- Vollständigkeitscheck scripts/check-module-guards.ts (Registry Action→Modul):
  schlägt fehl bei nicht zugeordneter Datei, fehlendem moduleGuard oder einer
  Action ohne guard(...). Als prebuild verdrahtet ⇒ Build failt bei vergessenem
  Endpoint. admin/tenant-settings sind als EXEMPT (eigene Auth) markiert.

Verifiziert: tsc sauber, lint sauber, Guard-Check grün (9 Dateien), Negativ-Probe
(ungemappte Action-Datei) failt den Check wie erwartet.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-20 20:27:13 +02:00
co-authored by Claude Opus 4.8
parent c963ffadea
commit 8348764c99
12 changed files with 233 additions and 161 deletions
+10 -24
View File
@@ -3,11 +3,11 @@
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { z } from "zod";
import { requireSession } from "@/server/auth";
import { dbForTenant } from "@/server/db";
import { requirePermission } from "@/server/rbac";
import { moduleGuard } from "@/server/action-guard";
import { writeAuditLog } from "@/server/audit";
const guard = moduleGuard("bia");
const processSchema = z.object({
name: z.string().trim().min(1).max(200),
description: z.string().trim().max(5000).optional(),
@@ -30,9 +30,7 @@ function parseProcessForm(formData: FormData) {
}
export async function createProcess(formData: FormData) {
const session = await requireSession();
requirePermission(session, "bia:write");
const db = dbForTenant(session.user.tenantId);
const { session, db } = await guard("bia:write");
const data = parseProcessForm(formData);
// tenantId explizit für die Typen, der Guard injiziert ohnehin
@@ -53,9 +51,7 @@ export async function createProcess(formData: FormData) {
}
export async function updateProcess(processId: string, formData: FormData) {
const session = await requireSession();
requirePermission(session, "bia:write");
const db = dbForTenant(session.user.tenantId);
const { session, db } = await guard("bia:write");
const before = await db.process.findUnique({ where: { id: processId } });
if (!before) throw new Error("Prozess nicht gefunden");
@@ -77,9 +73,7 @@ export async function updateProcess(processId: string, formData: FormData) {
}
export async function deleteProcess(processId: string) {
const session = await requireSession();
requirePermission(session, "bia:write");
const db = dbForTenant(session.user.tenantId);
const { session, db } = await guard("bia:write");
const before = await db.process.findUnique({ where: { id: processId } });
if (!before) throw new Error("Prozess nicht gefunden");
@@ -99,9 +93,7 @@ export async function deleteProcess(processId: string) {
}
export async function assignAsset(processId: string, formData: FormData) {
const session = await requireSession();
requirePermission(session, "bia:write");
const db = dbForTenant(session.user.tenantId);
const { session, db } = await guard("bia:write");
const assetId = z.string().min(1).parse(formData.get("assetId"));
const role = z.enum(["PRIMARY", "SECONDARY"]).parse(formData.get("role"));
@@ -128,9 +120,7 @@ export async function assignAsset(processId: string, formData: FormData) {
}
export async function unassignAsset(processId: string, processAssetId: string) {
const session = await requireSession();
requirePermission(session, "bia:write");
const db = dbForTenant(session.user.tenantId);
const { session, db } = await guard("bia:write");
const before = await db.processAsset.findUnique({ where: { id: processAssetId } });
if (!before) return;
@@ -152,9 +142,7 @@ const hours = z
.transform((v) => (v === "" ? null : v));
export async function saveBia(processId: string, formData: FormData) {
const session = await requireSession();
requirePermission(session, "bia:write");
const db = dbForTenant(session.user.tenantId);
const { session, db } = await guard("bia:write");
const processCount = await db.process.count({ where: { id: processId } });
if (processCount !== 1) throw new Error("Prozess nicht gefunden");
@@ -197,9 +185,7 @@ export async function saveBia(processId: string, formData: FormData) {
* weiter über die inkrementellen Aktionen assignAsset/unassignAsset.
*/
export async function saveProcessAll(processId: string, formData: FormData) {
const session = await requireSession();
requirePermission(session, "bia:write");
const db = dbForTenant(session.user.tenantId);
const { session, db } = await guard("bia:write");
const before = await db.process.findUnique({
where: { id: processId },