Rollen+Permissions-Sync beim Container-Start

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) <noreply@anthropic.com>
This commit is contained in:
2026-05-19 12:41:39 +02:00
parent 37df8c0c4a
commit 8ee5c9b07a
3 changed files with 185 additions and 0 deletions
+9
View File
@@ -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).
+162
View File
@@ -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<string, string[]> = {
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();
});
+14
View File
@@ -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,