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>
116 lines
3.8 KiB
TypeScript
116 lines
3.8 KiB
TypeScript
/**
|
|
* Bringt Rechtekatalog und Systemrollen in der Datenbank auf den Stand des
|
|
* Codes. Idempotent - laeuft bei jedem Container-Start.
|
|
*
|
|
* Verbraucher: `prisma/sync-roles.ts` (Container-Start), `prisma/seed.ts`
|
|
* (Erstinstallation) und `factoryReset` in `backup.service.ts`. Alle drei
|
|
* benutzen dieselbe Definition aus `config/rechte-katalog.ts`; vorher hatte
|
|
* jeder seine eigene, und die dritte wich ab.
|
|
*
|
|
* Was hier NICHT passiert: Stammdaten, Benutzer, Vertraege. Das Skript ist
|
|
* auf einer laufenden Produktionsdatenbank sicher.
|
|
*/
|
|
|
|
import type { PrismaClient } from '@prisma/client';
|
|
import {
|
|
RECHTE_KATALOG,
|
|
SYSTEMROLLEN,
|
|
alsRechtString,
|
|
} from '../config/rechte-katalog.js';
|
|
|
|
type Protokoll = (zeile: string) => void;
|
|
|
|
/**
|
|
* Setzt die Rechte einer Rolle exakt auf `permissionIds` - fehlende kommen
|
|
* dazu, ueberzaehlige fliegen raus.
|
|
*
|
|
* Der Vollersatz ist Absicht: Er ist der Grund, warum eine per Adminer an
|
|
* einer Systemrolle vorgenommene Aenderung den naechsten Container-Start
|
|
* nicht ueberlebt.
|
|
*/
|
|
async function synchronisiereRollenrechte(
|
|
prisma: PrismaClient,
|
|
roleId: number,
|
|
permissionIds: number[],
|
|
log: Protokoll,
|
|
): Promise<void> {
|
|
const vorhanden = await prisma.rolePermission.findMany({
|
|
where: { roleId },
|
|
select: { permissionId: true },
|
|
});
|
|
const vorhandenIds = new Set(vorhanden.map((e) => e.permissionId));
|
|
const zielIds = new Set(permissionIds);
|
|
|
|
const fehlend = permissionIds.filter((id) => !vorhandenIds.has(id));
|
|
if (fehlend.length > 0) {
|
|
await prisma.rolePermission.createMany({
|
|
data: fehlend.map((permissionId) => ({ roleId, permissionId })),
|
|
skipDuplicates: true,
|
|
});
|
|
log(` → +${fehlend.length} Rechte an Rolle #${roleId}`);
|
|
}
|
|
|
|
const ueberzaehlig = vorhanden
|
|
.filter((e) => !zielIds.has(e.permissionId))
|
|
.map((e) => e.permissionId);
|
|
if (ueberzaehlig.length > 0) {
|
|
await prisma.rolePermission.deleteMany({
|
|
where: { roleId, permissionId: { in: ueberzaehlig } },
|
|
});
|
|
log(` → -${ueberzaehlig.length} Rechte von Rolle #${roleId}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Legt alle Rechte aus dem Katalog an und bringt die Systemrollen auf Stand.
|
|
* Setzt dabei auch `isSystem` und `isHidden` - das ist die eigentliche
|
|
* Absicherung gegen eine von Hand verstellte Datenbank, die Migration setzt
|
|
* die Flags nur einmalig.
|
|
*/
|
|
export async function synchronisiereRechteUndRollen(
|
|
prisma: PrismaClient,
|
|
log: Protokoll = (z) => console.log(z),
|
|
): Promise<void> {
|
|
log('[rollen-sync] Rechtekatalog upserten…');
|
|
for (const recht of RECHTE_KATALOG) {
|
|
await prisma.permission.upsert({
|
|
where: { resource_action: { resource: recht.resource, action: recht.action } },
|
|
update: {},
|
|
create: { resource: recht.resource, action: recht.action },
|
|
});
|
|
}
|
|
|
|
const alleRechte = await prisma.permission.findMany();
|
|
log(`[rollen-sync] ${alleRechte.length} Rechte in der Datenbank`);
|
|
|
|
// Auflösung Katalog → Datenbank-IDs. Rechte, die in der Datenbank stehen,
|
|
// aber nicht im Katalog, bleiben unangetastet und werden auch keiner Rolle
|
|
// zugeteilt: Der Katalog ist die Wahrheit, nicht der Altbestand.
|
|
const idFuerRecht = new Map<string, number>();
|
|
for (const p of alleRechte) idFuerRecht.set(alsRechtString(p), p.id);
|
|
|
|
for (const spec of SYSTEMROLLEN) {
|
|
const rechteIds = RECHTE_KATALOG.filter(spec.rechte)
|
|
.map((r) => idFuerRecht.get(alsRechtString(r)))
|
|
.filter((id): id is number => id !== undefined);
|
|
|
|
const rolle = await prisma.role.upsert({
|
|
where: { name: spec.name },
|
|
update: {
|
|
description: spec.description,
|
|
isSystem: true,
|
|
isHidden: spec.isHidden,
|
|
},
|
|
create: {
|
|
name: spec.name,
|
|
description: spec.description,
|
|
isSystem: true,
|
|
isHidden: spec.isHidden,
|
|
},
|
|
});
|
|
await synchronisiereRollenrechte(prisma, rolle.id, rechteIds, log);
|
|
}
|
|
|
|
log('[rollen-sync] fertig.');
|
|
}
|