From 8ee5c9b07a4957b0a9ba95eb8fd9655d072a98a5 Mon Sep 17 00:00:00 2001 From: duffyduck Date: Tue, 19 May 2026 12:41:39 +0200 Subject: [PATCH] Rollen+Permissions-Sync beim Container-Start MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Folge-Fix für die DSGVO-Menü-Sache. Settings.tsx hatte ich auf audit:read || gdpr:admin erweitert, aber auf bestehenden Installationen läuft der prisma-Seed nicht (nur auf leeren DBs). Wer das System früher installiert hat, hat die DSGVO-Rolle ohne audit:read in der DB – das JWT enthielt die Perm dann nie, und der neue Settings.tsx-Check blieb wirkungslos. Neues Skript prisma/sync-roles.ts läuft idempotent bei jedem Container-Start: upserts Permissions-Katalog + syncRolePermissions für Admin, Developer, DSGVO, Mitarbeiter (R/W + R/O), Kunde. Stammdaten, User und Verträge werden NICHT angefasst – sicher auf prod. Live-verifiziert: nach `DELETE audit:read FROM RolePermission` liefert der nächste Lauf "+1 Permissions an Rolle #27", DSGVO ist wieder komplett. Co-Authored-By: Claude Opus 4.7 (1M context) --- backend/docker-entrypoint.sh | 9 ++ backend/prisma/sync-roles.ts | 162 +++++++++++++++++++++++++++++++++++ docs/todo.md | 14 +++ 3 files changed, 185 insertions(+) create mode 100644 backend/prisma/sync-roles.ts diff --git a/backend/docker-entrypoint.sh b/backend/docker-entrypoint.sh index 9e9e5f5f..a5fbeae4 100755 --- a/backend/docker-entrypoint.sh +++ b/backend/docker-entrypoint.sh @@ -122,6 +122,15 @@ if [ "$RAN_SEED" = "true" ] && [ -d /app/factory-defaults-builtin ] \ || echo "[entrypoint] Factory-Defaults-Seed fehlgeschlagen – ignoriert" fi +# Permissions + Rollen-Sync: Stellt sicher, dass nachträglich hinzugefügte +# Permissions (z.B. audit:read auf der DSGVO-Rolle) auch auf bestehenden +# DBs ankommen. Seed läuft NICHT auf nicht-leeren DBs, daher würden alte +# Installationen sonst mit unvollständigen Role-Perms laufen. Idempotent, +# fasst keine Stammdaten / User / Verträge an. +echo "[entrypoint] Rollen + Permissions synchronisieren…" +npx tsx prisma/sync-roles.ts \ + || echo "[entrypoint] Role-Sync fehlgeschlagen – nicht kritisch" + # Datenbereinigung: XSS-Strings aus Customer/User-Stringfeldern strippen, # nicht-whitelisted AppSettings entfernen, Pentest-Marker melden (Default # nur warnen; CLEANUP_PURGE_PENTEST=true löscht markierte Records). diff --git a/backend/prisma/sync-roles.ts b/backend/prisma/sync-roles.ts new file mode 100644 index 00000000..8909971a --- /dev/null +++ b/backend/prisma/sync-roles.ts @@ -0,0 +1,162 @@ +/** + * Idempotenter Permissions+Rollen-Sync für den Container-Start. + * + * Hintergrund: seed.ts läuft nur auf leeren DBs (USER_COUNT=0). Wer das + * System schon installiert hat, bekommt nachträglich hinzugefügte + * Permissions oder neue Rollenzuordnungen NICHT — die DSGVO-Rolle kann + * dann z.B. ohne audit:read landen, obwohl Settings.tsx das voraussetzt. + * + * Dieses Skript synchronisiert ausschließlich: + * - Permission-Katalog (resource/action-Paare aus dem Code) + * - Roll-Zuordnungen (Admin, Developer, DSGVO, Mitarbeiter, + * Mitarbeiter (Nur-Lesen), Kunde) + * + * KEINE Stammdaten, KEINE User, KEINE Verträge — das Skript ist auf + * laufenden Prod-DBs sicher. + */ + +import { PrismaClient } from '@prisma/client'; + +const prisma = new PrismaClient(); + +const RESOURCE_PERMISSIONS: Record = { + customers: ['create', 'read', 'update', 'delete'], + contracts: ['create', 'read', 'update', 'delete'], + users: ['create', 'read', 'update', 'delete'], + platforms: ['create', 'read', 'update', 'delete'], + providers: ['create', 'read', 'update', 'delete'], + tariffs: ['create', 'read', 'update', 'delete'], + 'cancellation-periods': ['create', 'read', 'update', 'delete'], + 'contract-durations': ['create', 'read', 'update', 'delete'], + 'contract-categories': ['create', 'read', 'update', 'delete'], + 'email-providers': ['create', 'read', 'update', 'delete'], + settings: ['read', 'update'], + developer: ['access'], + emails: ['delete'], + audit: ['read', 'export', 'admin'], + gdpr: ['export', 'delete', 'admin'], +}; + +async function syncRolePermissions(roleId: number, permissionIds: number[]) { + const existing = await prisma.rolePermission.findMany({ + where: { roleId }, + select: { permissionId: true }, + }); + const existingIds = new Set(existing.map((e) => e.permissionId)); + const targetIds = new Set(permissionIds); + + const missing = permissionIds.filter((id) => !existingIds.has(id)); + if (missing.length > 0) { + await prisma.rolePermission.createMany({ + data: missing.map((permissionId) => ({ roleId, permissionId })), + skipDuplicates: true, + }); + console.log(` → +${missing.length} Permissions an Rolle #${roleId}`); + } + + const excess = existing + .filter((e) => !targetIds.has(e.permissionId)) + .map((e) => e.permissionId); + if (excess.length > 0) { + await prisma.rolePermission.deleteMany({ + where: { roleId, permissionId: { in: excess } }, + }); + console.log(` → -${excess.length} Permissions von Rolle #${roleId}`); + } +} + +async function main() { + console.log('[sync-roles] Permissions-Katalog upserten…'); + for (const [resource, actions] of Object.entries(RESOURCE_PERMISSIONS)) { + for (const action of actions) { + await prisma.permission.upsert({ + where: { resource_action: { resource, action } }, + update: {}, + create: { resource, action }, + }); + } + } + const allPermissions = await prisma.permission.findMany(); + console.log(`[sync-roles] ${allPermissions.length} Permissions vorhanden`); + + // Admin: alles AUSSER developer:access und audit/gdpr (DSGVO + Developer + // sind separate hidden roles, über Checkboxen zugewiesen) + const adminPermIds = allPermissions + .filter( + (p) => + !(p.resource === 'developer' && p.action === 'access') && + p.resource !== 'audit' && + p.resource !== 'gdpr' + ) + .map((p) => p.id); + + // Developer: alles + const developerPermIds = allPermissions.map((p) => p.id); + + // DSGVO: audit + gdpr komplett + const gdprPermIds = allPermissions + .filter((p) => p.resource === 'audit' || p.resource === 'gdpr') + .map((p) => p.id); + + // Mitarbeiter: customers + contracts + read auf Stammdaten + const employeePermIds = allPermissions + .filter( + (p) => + p.resource === 'customers' || + p.resource === 'contracts' || + (p.action === 'read' && + [ + 'platforms', + 'providers', + 'tariffs', + 'cancellation-periods', + 'contract-durations', + 'contract-categories', + ].includes(p.resource)) + ) + .map((p) => p.id); + + // Read-only Mitarbeiter + Kunde: nur read auf Haupt-Entities + Stammdaten + const readOnlyResources = [ + 'customers', + 'contracts', + 'platforms', + 'providers', + 'tariffs', + 'cancellation-periods', + 'contract-durations', + 'contract-categories', + ]; + const readOnlyPermIds = allPermissions + .filter((p) => p.action === 'read' && readOnlyResources.includes(p.resource)) + .map((p) => p.id); + + const rolesSpec: Array<{ name: string; description: string; permIds: number[] }> = [ + { name: 'Admin', description: 'Voller Zugriff auf alle Funktionen', permIds: adminPermIds }, + { name: 'Developer', description: 'Voller Zugriff inkl. Entwickler-Tools', permIds: developerPermIds }, + { name: 'DSGVO', description: 'DSGVO-Zugriff: Audit-Logs und Datenschutz-Verwaltung', permIds: gdprPermIds }, + { name: 'Mitarbeiter', description: 'Kann Kunden und Verträge verwalten', permIds: employeePermIds }, + { name: 'Mitarbeiter (Nur-Lesen)', description: 'Kann nur lesen, keine Änderungen', permIds: readOnlyPermIds }, + { name: 'Kunde', description: 'Kann nur eigene Daten lesen', permIds: readOnlyPermIds }, + ]; + + for (const r of rolesSpec) { + const role = await prisma.role.upsert({ + where: { name: r.name }, + update: { description: r.description }, + create: { name: r.name, description: r.description }, + }); + await syncRolePermissions(role.id, r.permIds); + } + + console.log('[sync-roles] fertig.'); +} + +main() + .catch((e) => { + console.error('[sync-roles] Fehler:', e); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + }); diff --git a/docs/todo.md b/docs/todo.md index 930dfd0c..fba2fcb2 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -120,6 +120,20 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung - **Live-verifiziert**: 4867 Datensätze + 1 Datei in 13.2s wiederhergestellt, Log-Modal zeigt den vollständigen Verlauf. +- [x] **🐛 Rollen-Perms-Sync beim Container-Start (Follow-up DSGVO-Fix)** + - Bestehende Installationen liefen weiter mit veraltetem + Permission-Set für die DSGVO-Rolle (audit:read u.a. fehlten), + weil `prisma db seed` per docker-entrypoint nur auf leeren DBs + läuft. Folge: Settings.tsx-Fix vom Vorgänger-Commit half nicht, + weil das JWT die fehlende Perm gar nicht enthielt. + - Neuer Step im Entrypoint: `npx tsx prisma/sync-roles.ts` läuft + bei jedem Start. Idempotent, fasst nur Permission- und + Role-Tabellen an (keine User/Customers/Contracts), führt + `syncRolePermissions` für Admin, Developer, DSGVO, + Mitarbeiter, Mitarbeiter (Nur-Lesen), Kunde aus. + - Live-verifiziert: `audit:read` aus DSGVO-Rolle gelöscht, Script + laufen lassen → "+1 Permission an Rolle #27", wieder vollständig. + - [x] **🐛 DSGVO-Rolle: Menüpunkte in den Einstellungen unsichtbar** - Symptom: User mit ausschließlich DSGVO-Rolle sah keinerlei Karten unter Einstellungen → System (DSGVO-Dashboard,