Richtlinien Phase 2a: verwaltete Register-Tabellen + Anwender-Handbuch (§7b, §9.7)
Vier neue, in die Bibliothek integrierte Dokumente (öffnen als interaktive Popups):
- Krypto-Register (VA-07): editierbare Tabelle der eingesetzten Verschlüsselung
mit Ablaufüberwachung (läuft-ab/abgelaufen-Markierung), Baseline-Bezug
(BL-CRY-…), voller CRUD (add/edit/delete).
- Risiko-Bewertungsmatrix (R03/VA-09): zentrale Pflegestelle mit FB-80-04-Defaults
— 4×4-Heatmap (Schadensausmaß × EW), Risikoklassen + Akzeptanzinstanz
(Niedrig≤3 / Mittel≤6 / Hoch≤9 / Kritisch≥12, editierbar), EW-Skala,
7 Schadenskategorien. (Rückkopplung in die 5×5-Heatmap der Risikoanalyse folgt.)
- Klassifizierungs-Handhabungsmatrix (R02/VA-08): AA-80-20-Defaults, 4 Schutzklassen
× 14 Aspekte mit eskalierenden Regeln; Zellen inline editierbar, Aspekt/Klasse
hinzufügbar.
- Anwender-Handbuch (§9.7): kuratierte Themen (Passwörter/MFA, Zugriffsrechte,
Klassifizierung, mobiles Arbeiten, E-Mail, Vorfälle) in verständlicher Sprache;
Werte via {{VARIABLE}}-Templating → automatisch synchron zur Baseline; Deep-Links
in die Quellabschnitte (z. B. R08 §4.1.2).
Zusätzlich: Richtlinien/Verfahren, die eine verwaltete Tabelle einbetten
(R09/VA-07, R03/VA-09, R02/VA-08), zeigen im Lesemodus einen „Vollständige Tabelle
öffnen"-Callout auf das jeweilige Register (§7b).
Datenmodell: CryptoEntry, ClassificationClass/HandlingAspect/HandlingRule,
RiskMatrixClass/RiskEwLevel/RiskDamageDimension, HandbookTopic (+ RLS).
Server-Actions mit RBAC (policy:write) + Audit-Log. Seed aus FB-80-04/AA-80-20.
Verifiziert im Browser: alle vier Register gerendert, Ablauf-/Farb-/Deep-Link-Logik,
Krypto-CRUD (add + delete) end-to-end.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
"use server";
|
||||
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { z } from "zod";
|
||||
import { requireSession } from "@/server/auth";
|
||||
import { dbForTenant } from "@/server/db";
|
||||
import { requirePermission } from "@/server/rbac";
|
||||
import { writeAuditLog } from "@/server/audit";
|
||||
|
||||
const optDate = (v: FormDataEntryValue | null) => (v && String(v) ? new Date(String(v)) : null);
|
||||
const str = (v: FormDataEntryValue | null) => (v ? String(v).trim() : "");
|
||||
|
||||
async function guard() {
|
||||
const session = await requireSession();
|
||||
requirePermission(session, "policy:write");
|
||||
return { session, db: dbForTenant(session.user.tenantId) };
|
||||
}
|
||||
|
||||
/* ── Krypto-Register (VA-07) ── */
|
||||
|
||||
export async function addCryptoEntry(formData: FormData) {
|
||||
const { session, db } = await guard();
|
||||
const last = await db.cryptoEntry.aggregate({ _max: { orderIdx: true } });
|
||||
await db.cryptoEntry.create({
|
||||
data: {
|
||||
tenantId: session.user.tenantId,
|
||||
dienst: z.string().trim().min(1).parse(formData.get("dienst")),
|
||||
schluessel: z.string().trim().min(1).parse(formData.get("schluessel")),
|
||||
algorithmus: str(formData.get("algorithmus")) || null,
|
||||
ablaufdatum: optDate(formData.get("ablaufdatum")),
|
||||
verantwortlich: str(formData.get("verantwortlich")) || "—",
|
||||
speicherort: str(formData.get("speicherort")) || null,
|
||||
baselineRef: str(formData.get("baselineRef")) || null,
|
||||
orderIdx: (last._max.orderIdx ?? 0) + 1,
|
||||
},
|
||||
});
|
||||
await writeAuditLog({ tenantId: session.user.tenantId, actorId: session.user.id, action: "create", entity: "crypto_entry" });
|
||||
revalidatePath("/policies");
|
||||
}
|
||||
|
||||
export async function updateCryptoEntry(id: string, formData: FormData) {
|
||||
const { session, db } = await guard();
|
||||
await db.cryptoEntry.update({
|
||||
where: { id },
|
||||
data: {
|
||||
dienst: z.string().trim().min(1).parse(formData.get("dienst")),
|
||||
schluessel: z.string().trim().min(1).parse(formData.get("schluessel")),
|
||||
algorithmus: str(formData.get("algorithmus")) || null,
|
||||
ablaufdatum: optDate(formData.get("ablaufdatum")),
|
||||
verantwortlich: str(formData.get("verantwortlich")) || "—",
|
||||
speicherort: str(formData.get("speicherort")) || null,
|
||||
baselineRef: str(formData.get("baselineRef")) || null,
|
||||
},
|
||||
});
|
||||
await writeAuditLog({ tenantId: session.user.tenantId, actorId: session.user.id, action: "update", entity: "crypto_entry", entityId: id });
|
||||
revalidatePath("/policies");
|
||||
}
|
||||
|
||||
export async function deleteCryptoEntry(id: string) {
|
||||
const { session, db } = await guard();
|
||||
await db.cryptoEntry.delete({ where: { id } });
|
||||
await writeAuditLog({ tenantId: session.user.tenantId, actorId: session.user.id, action: "delete", entity: "crypto_entry", entityId: id });
|
||||
revalidatePath("/policies");
|
||||
}
|
||||
|
||||
/* ── Klassifizierungs-Handhabungsmatrix (R02 / VA-08) ── */
|
||||
|
||||
export async function updateHandlingRule(classId: string, aspectId: string, formData: FormData) {
|
||||
const { session, db } = await guard();
|
||||
const text = str(formData.get("text"));
|
||||
await db.handlingRule.upsert({
|
||||
where: { classId_aspectId: { classId, aspectId } },
|
||||
update: { text },
|
||||
create: { tenantId: session.user.tenantId, classId, aspectId, text },
|
||||
});
|
||||
await writeAuditLog({ tenantId: session.user.tenantId, actorId: session.user.id, action: "update", entity: "handling_rule" });
|
||||
revalidatePath("/policies");
|
||||
}
|
||||
|
||||
export async function addHandlingAspect(formData: FormData) {
|
||||
const { session, db } = await guard();
|
||||
const last = await db.handlingAspect.aggregate({ _max: { orderIdx: true } });
|
||||
await db.handlingAspect.create({
|
||||
data: {
|
||||
tenantId: session.user.tenantId,
|
||||
name: z.string().trim().min(1).parse(formData.get("name")),
|
||||
category: str(formData.get("category")) || null,
|
||||
orderIdx: (last._max.orderIdx ?? 0) + 1,
|
||||
},
|
||||
});
|
||||
await writeAuditLog({ tenantId: session.user.tenantId, actorId: session.user.id, action: "create", entity: "handling_aspect" });
|
||||
revalidatePath("/policies");
|
||||
}
|
||||
|
||||
export async function addClassificationClass(formData: FormData) {
|
||||
const { session, db } = await guard();
|
||||
const last = await db.classificationClass.aggregate({ _max: { orderIdx: true } });
|
||||
await db.classificationClass.create({
|
||||
data: {
|
||||
tenantId: session.user.tenantId,
|
||||
name: z.string().trim().min(1).parse(formData.get("name")),
|
||||
description: str(formData.get("description")) || null,
|
||||
orderIdx: (last._max.orderIdx ?? 0) + 1,
|
||||
},
|
||||
});
|
||||
await writeAuditLog({ tenantId: session.user.tenantId, actorId: session.user.id, action: "create", entity: "classification_class" });
|
||||
revalidatePath("/policies");
|
||||
}
|
||||
|
||||
/* ── Risiko-Bewertungsmatrix (R03 / VA-09) ── */
|
||||
|
||||
export async function updateRiskClass(id: string, formData: FormData) {
|
||||
const { session, db } = await guard();
|
||||
await db.riskMatrixClass.update({
|
||||
where: { id },
|
||||
data: {
|
||||
name: z.string().trim().min(1).parse(formData.get("name")),
|
||||
maxScore: z.coerce.number().int().min(1).max(999).parse(formData.get("maxScore")),
|
||||
acceptance: str(formData.get("acceptance")),
|
||||
},
|
||||
});
|
||||
await writeAuditLog({ tenantId: session.user.tenantId, actorId: session.user.id, action: "update", entity: "risk_matrix_class", entityId: id });
|
||||
revalidatePath("/policies");
|
||||
}
|
||||
@@ -52,6 +52,14 @@ const TENANT_MODELS = new Set<string>([
|
||||
"PolicyVariable",
|
||||
"PolicyBaselineParam",
|
||||
"PolicyEvidence",
|
||||
"CryptoEntry",
|
||||
"ClassificationClass",
|
||||
"HandlingAspect",
|
||||
"HandlingRule",
|
||||
"RiskMatrixClass",
|
||||
"RiskEwLevel",
|
||||
"RiskDamageDimension",
|
||||
"HandbookTopic",
|
||||
]);
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user