Files
opencrm/backend/prisma/rechte-report.ts
T
duffyduckandClaude Opus 5 77aeb69aeb 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>
2026-09-08 13:49:30 +02:00

158 lines
5.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Bestandsaufnahme des Rechtesystems. Liest nur, aendert nichts.
*
* npx tsx prisma/rechte-report.ts → Bericht auf stdout
* npx tsx prisma/rechte-report.ts > vorher.txt
*
* Zweck: Vor und nach dem Rechte-Umbau denselben Bericht erzeugen und
* vergleichen. Die Zusage lautet "niemand verliert Zugriff" - und eine
* Zusage, die niemand nachrechnen kann, ist keine.
*
* Der Bericht ist bewusst zeilenweise und sortiert, damit `diff` darauf
* etwas Lesbares ausgibt.
*/
import { PrismaClient } from '@prisma/client';
import { ALLE_RECHTE, SYSTEMROLLEN_NAMEN } from '../src/config/rechte-katalog.js';
const prisma = new PrismaClient();
/**
* Liest isSystem/isHidden, sofern die Spalten existieren.
*
* Der Bericht muss VOR und NACH der Migration laufen - das ist sein Zweck.
* Vorher gibt es die Spalten noch nicht, und ein Absturz an dieser Stelle
* haette genau die Baseline verhindert, gegen die spaeter verglichen wird.
*/
async function leseKennzeichen(): Promise<Map<number, { isSystem: boolean; isHidden: boolean }>> {
const karte = new Map<number, { isSystem: boolean; isHidden: boolean }>();
try {
const zeilen = await prisma.$queryRawUnsafe<
Array<{ id: number; isSystem: number | boolean; isHidden: number | boolean }>
>('SELECT id, isSystem, isHidden FROM `Role`');
for (const z of zeilen) {
karte.set(Number(z.id), { isSystem: Boolean(z.isSystem), isHidden: Boolean(z.isHidden) });
}
} catch {
// Spalten noch nicht vorhanden - das ist der Zustand vor der Migration.
}
return karte;
}
async function main(): Promise<void> {
const kennzeichen = await leseKennzeichen();
const rollen = await prisma.role.findMany({
orderBy: { name: 'asc' },
select: {
id: true,
name: true,
permissions: { include: { permission: true } },
_count: { select: { users: true } },
},
});
console.log('=== ROLLEN ===');
for (const r of rollen) {
const rechte = r.permissions
.map((rp) => `${rp.permission.resource}:${rp.permission.action}`)
.sort();
const k = kennzeichen.get(r.id);
const etikett = [k?.isSystem ? 'System' : null, k?.isHidden ? 'versteckt' : null]
.filter(Boolean)
.join(', ');
console.log(
`\n[${r.name}] #${r.id}` +
(etikett ? ` (${etikett})` : '') +
` ${r._count.users} Konten, ${rechte.length} Rechte`,
);
for (const recht of rechte) console.log(` ${recht}`);
}
console.log('\n=== KONTEN (aktiv) ===');
const konten = await prisma.user.findMany({
where: { isActive: true },
orderBy: { email: 'asc' },
select: {
email: true,
roles: {
select: {
role: {
// Ausdrueckliche Spaltenauswahl, damit der Bericht auch auf einer
// noch nicht migrierten Datenbank laeuft.
select: {
name: true,
permissions: { include: { permission: true } },
},
},
},
},
},
});
for (const k of konten) {
const rechte = new Set<string>();
for (const ur of k.roles) {
for (const rp of ur.role.permissions) {
rechte.add(`${rp.permission.resource}:${rp.permission.action}`);
}
}
const rollenNamen = k.roles.map((ur) => ur.role.name).sort().join(', ') || '(keine)';
console.log(`\n[${k.email}] Rollen: ${rollenNamen} ${rechte.size} Rechte`);
for (const recht of [...rechte].sort()) console.log(` ${recht}`);
}
// --- Auffaelligkeiten -----------------------------------------------------
// Nicht als Alarm gemeint, sondern als das, was man vor einem Deploy
// wissen will. Eine umbenannte Systemrolle etwa wird vom Backfill der
// Migration nicht getroffen.
console.log('\n=== HINWEISE ===');
const hinweise: string[] = [];
const vorhandeneNamen = new Set(rollen.map((r) => r.name));
for (const erwartet of SYSTEMROLLEN_NAMEN) {
if (!vorhandeneNamen.has(erwartet)) {
hinweise.push(`Systemrolle "${erwartet}" fehlt in der Datenbank.`);
}
}
for (const r of rollen) {
const k = kennzeichen.get(r.id);
// Vor der Migration gibt es die Spalte nicht - dann ist "nicht
// gekennzeichnet" kein Befund, sondern der erwartete Zustand.
if (kennzeichen.size === 0) continue;
if (SYSTEMROLLEN_NAMEN.includes(r.name) && !k?.isSystem) {
hinweise.push(`Rolle "${r.name}" ist eine Systemrolle, aber isSystem ist nicht gesetzt.`);
}
if (!SYSTEMROLLEN_NAMEN.includes(r.name) && k?.isSystem) {
hinweise.push(`Rolle "${r.name}" traegt isSystem, gehoert aber nicht zum Katalog.`);
}
}
const alleRechteDb = await prisma.permission.findMany();
const imKatalog = new Set(ALLE_RECHTE);
for (const p of alleRechteDb) {
const s = `${p.resource}:${p.action}`;
if (!imKatalog.has(s)) hinweise.push(`Recht "${s}" steht in der Datenbank, aber nicht im Katalog.`);
}
const inDb = new Set(alleRechteDb.map((p) => `${p.resource}:${p.action}`));
for (const s of ALLE_RECHTE) {
if (!inDb.has(s)) hinweise.push(`Recht "${s}" steht im Katalog, aber nicht in der Datenbank.`);
}
// Konten ohne jede Rolle koennen sich anmelden und sehen nichts - eine
// haeufige Ursache fuer "bei mir ist alles leer".
for (const k of konten) {
if (k.roles.length === 0) hinweise.push(`Konto "${k.email}" hat keine Rolle.`);
}
if (hinweise.length === 0) console.log(' keine');
else for (const h of hinweise) console.log(` ! ${h}`);
}
main()
.catch((e) => {
console.error('[rechte-report] Fehler:', e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});