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:
@@ -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 },
|
||||
|
||||
Reference in New Issue
Block a user