Rechtemodell Etappe 1: toter Katalog begradigt, Selbst-Erhoehung geschlossen

Vorarbeit fuer die Rollen-Oberflaeche. Eine Checkbox-Liste ueber einem
Katalog, der nicht stimmt, waere schlimmer als gar keine.

18 von 50 Rechten bewachten nichts: tariffs:*, cancellation-periods:*,
contract-durations:* und email-providers:* standen im Katalog und waren
anhakbar - die Routen prueften in Wahrheit providers:*, platforms:* und
settings:*. Die Routen gaten jetzt granular; eine rein additive Migration
vererbt jedes neue Recht an jede Rolle, die bisher das Sammelrecht hatte.
Nachgerechnet: keine Rolle hat etwas verloren.

Der Katalog stand dreifach und divergent - seed.ts, sync-roles.ts und,
abweichend, factoryReset. Die dritte Kopie kannte weder audit:* noch
gdpr:* und legte DSGVO, Audit-Betrieb und Gegenbuch gar nicht an: Nach
einem Werksreset konnte niemand mehr eine Auskunft nach Art. 15
ausfuehren, und aufgefallen waere es erst, wenn eine Frist laeuft. Jetzt
eine Quelle: src/config/rechte-katalog.ts.

Im selben Code: factoryReset setzte das Admin-Kennwort fest auf "admin"
mit bcrypt-Cost 10 - genau das, was seed.ts seit Pentest Runde 12
verbietet. Dieselbe Haertung war nur in einer der beiden Kopien
angekommen. Jetzt zufaellig, Cost 12, einmalig im Log.

Selbst-Erhoehung geschlossen: neues Recht roles:manage, getrennt von
users:*, dazu die Teilmengenregel in rechte.service.ts - niemand kann ein
Recht weitergeben, das er selbst nicht haelt. Sie greift auf allen vier
Wegen: Rolle anlegen, Rolle aendern, Rollen zuweisen und die drei Haken.
Der Haken-Weg war der wichtigste: Er brauchte nur users:create, ein
zweites Konto mit "Entwicklerzugriff" anlegen und sich damit anmelden -
die Developer-Rolle traegt alle Rechte. Geprueft wird der Zuwachs, nicht
der Endzustand, damit Entziehen erlaubt bleibt; und die Rechte des
Handelnden kommen frisch aus der Datenbank, nicht aus dem bis zu 15
Minuten alten Token.

Systemrollen gesperrt (Role.isSystem): Admin liess sich bisher umbenennen
oder leeren - und die versteckten Rollen haengen an ihrem Namen.
Role.isHidden ersetzt die im Frontend hartkodierte Namensliste, in der
"Gegenbuch" fehlte.

Rechteaenderung wirkt sofort: updateRole/deleteRole melden alle Traeger
ab. Werksreset und Backup-Restore verlangen zusaetzlich roles:manage -
beide loeschen alle Rollen und legen ein frisches admin@admin.com an.

Rollenpflege war der einzige Eingriff in die Rechtevergabe ohne
SecurityEvent, obwohl sie viele Konten auf einmal trifft. Jetzt
PERMISSION_CHANGED - ebenso fuer die bisher stummen Haken DSGVO und
Entwicklerzugriff.

Aussperr-Ausweg: prisma/rolle-zuweisen.ts. Umgeht die Regel bewusst - wer
Shell-Zugang hat, hat ohnehin die Datenbank; ein gestohlener Web-Zugang
hat ihn nicht. Schreibt ueber die Hash-Kette, nicht roh. Die Startwache
nennt den Befehl im Klartext.

Geprueft gegen Dev-DB und frische Wegwerf-DB: 7 Eskalationswege alle 403,
2 Gegenproben erlaubt; 6 Sperrtests auf Systemrollen alle 403, eigene
Rollen weiter aenderbar; Stammdaten-Lesen unveraendert 200;
Rechteaenderung sofort 401; Migration und sync-roles dreimal identisch;
db:seed auf bestehender DB ohne Wirkung auf die Rollenmatrix; frische
Installation mit allen 8 Systemrollen und ohne Hinweise.

Beim Deploy: Die Migration beendet alle Sitzungen einmalig. Noetig, weil
die Rechte im Token stehen - sonst liefen bis zu 15 Minuten 403er.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-08 13:49:30 +02:00
co-authored by Claude Opus 5
parent cca242119b
commit 77aeb69aeb
31 changed files with 1790 additions and 627 deletions
@@ -1,4 +1,5 @@
import prisma from '../lib/prisma.js';
import { SYSTEMROLLEN } from '../config/rechte-katalog.js';
/**
* Prueft beim Start, ob die gesetzlich gebundenen Rechte ueberhaupt jemand
@@ -25,6 +26,11 @@ const PFLICHTRECHTE: Array<{ resource: string; action: string; wofuer: string }>
{ resource: 'gdpr', action: 'export', wofuer: 'Auskunft nach Art. 15 DSGVO' },
{ resource: 'gdpr', action: 'delete', wofuer: 'Löschung nach Art. 17 DSGVO' },
{ resource: 'audit', action: 'read', wofuer: 'Prüfung des Audit-Protokolls' },
// Ohne dieses Recht laesst sich keine Rolle mehr anlegen oder aendern - die
// Rechtevergabe waere eingefroren. Kein Rechtsproblem wie die beiden
// darueber, aber dieselbe Bauart: eine Faehigkeit, deren Fehlen erst
// auffaellt, wenn man sie braucht.
{ resource: 'roles', action: 'manage', wofuer: 'Pflege der Rollen und Rechte' },
];
export async function pruefePflichtrechte(): Promise<void> {
@@ -53,6 +59,37 @@ export async function pruefePflichtrechte(): Promise<void> {
}
}
// Zweite Wache: Stimmen die Systemrollen noch?
//
// Die versteckten Rollen werden ueber ihren NAMEN gefunden
// (`findFirst({ where: { name: 'DSGVO' } })`). Fehlt eine, oder ist ihr
// isSystem-Flag von Hand entfernt worden, laeuft der Notfallpfad ins
// Leere - lautlos. Das hier ist die Meldung, die es dann geben soll.
const rollenHinweise: string[] = [];
for (const spec of SYSTEMROLLEN) {
const rolle = await prisma.role.findUnique({ where: { name: spec.name } });
if (!rolle) {
rollenHinweise.push(`Systemrolle „${spec.name}" fehlt.`);
} else if (!rolle.isSystem) {
rollenHinweise.push(
`Systemrolle „${spec.name}" ist nicht als Systemrolle gekennzeichnet – ` +
'sie ist damit über die Rollenverwaltung änderbar.',
);
}
}
if (rollenHinweise.length > 0) {
console.warn(
'\n' +
'========================================================================\n' +
' ACHTUNG: Die Systemrollen stimmen nicht mit dem Katalog überein:\n' +
rollenHinweise.map((h) => ` – ${h}\n`).join('') +
'\n' +
' Beheben: npx tsx prisma/sync-roles.ts\n' +
'========================================================================\n',
);
}
if (fehlend.length === 0) return;
console.warn(
@@ -68,6 +105,13 @@ export async function pruefePflichtrechte(): Promise<void> {
' den Haken „DSGVO-Zugriff" setzen (Audit-Protokoll lesen und\n' +
' Datenschutz-Verwaltung). Für Eingriffe am Protokoll – versiegeln,\n' +
' aufräumen, Aufbewahrung ändern – zusätzlich „Audit-Betrieb".\n' +
'\n' +
' Geht das nicht, weil niemand mehr die nötigen Rechte hat: Rechte\n' +
' lassen sich über die Oberfläche nur weitergeben, nicht erschaffen.\n' +
' Für die Erstvergabe gibt es den Weg über die Kommandozeile:\n' +
' docker compose exec backend \\\n' +
' npx tsx prisma/rolle-zuweisen.ts <e-mail> DSGVO\n' +
' („--liste" zeigt die vorhandenen Rollen.)\n' +
'========================================================================\n',
);
} catch (err) {