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:
@@ -26,28 +26,28 @@ export default function Settings() {
|
||||
icon: Clock,
|
||||
title: 'Kündigungsfristen',
|
||||
description: 'Konfigurieren Sie die verfügbaren Kündigungsfristen für Verträge.',
|
||||
show: hasPermission('platforms:read'),
|
||||
show: hasPermission('cancellation-periods:read'),
|
||||
},
|
||||
{
|
||||
to: '/settings/contract-durations',
|
||||
icon: Calendar,
|
||||
title: 'Vertragslaufzeiten',
|
||||
description: 'Konfigurieren Sie die verfügbaren Laufzeiten für Verträge.',
|
||||
show: hasPermission('platforms:read'),
|
||||
show: hasPermission('contract-durations:read'),
|
||||
},
|
||||
{
|
||||
to: '/settings/providers',
|
||||
icon: Building2,
|
||||
title: 'Anbieter & Tarife',
|
||||
description: 'Verwalten Sie Anbieter und deren Tarife für Verträge.',
|
||||
show: hasPermission('providers:read') || hasPermission('platforms:read'),
|
||||
show: hasPermission('providers:read') || hasPermission('tariffs:read'),
|
||||
},
|
||||
{
|
||||
to: '/settings/contract-categories',
|
||||
icon: FileType,
|
||||
title: 'Vertragstypen',
|
||||
description: 'Konfigurieren Sie die verfügbaren Vertragstypen (Strom, Gas, Mobilfunk, etc.).',
|
||||
show: hasPermission('platforms:read'),
|
||||
show: hasPermission('contract-categories:read'),
|
||||
},
|
||||
{
|
||||
to: '/settings/credit-note-number-range',
|
||||
@@ -180,6 +180,7 @@ export default function Settings() {
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
{hasPermission('email-providers:read') && (
|
||||
<Link
|
||||
to="/settings/email-providers"
|
||||
className="block p-4 bg-white border border-gray-200 rounded-lg shadow-sm hover:shadow-md hover:border-blue-300 transition-all group"
|
||||
@@ -197,6 +198,7 @@ export default function Settings() {
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
)}
|
||||
<Link
|
||||
to="/settings/database-backup"
|
||||
className="block p-4 bg-white border border-gray-200 rounded-lg shadow-sm hover:shadow-md hover:border-blue-300 transition-all group"
|
||||
|
||||
@@ -49,7 +49,7 @@ export default function CancellationPeriodList() {
|
||||
</Button>
|
||||
</Link>
|
||||
<h1 className="text-2xl font-bold flex-1">Kündigungsfristen</h1>
|
||||
{hasPermission('platforms:create') && (
|
||||
{hasPermission('cancellation-periods:create') && (
|
||||
<Button onClick={() => setShowModal(true)}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Neue Frist
|
||||
@@ -101,12 +101,12 @@ export default function CancellationPeriodList() {
|
||||
</td>
|
||||
<td className="py-3 px-4 text-right">
|
||||
<div className="flex justify-end gap-2">
|
||||
{hasPermission('platforms:update') && (
|
||||
{hasPermission('cancellation-periods:update') && (
|
||||
<Button variant="ghost" size="sm" onClick={() => handleEdit(period)}>
|
||||
<Edit className="w-4 h-4" />
|
||||
</Button>
|
||||
)}
|
||||
{hasPermission('platforms:delete') && (
|
||||
{hasPermission('cancellation-periods:delete') && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
|
||||
@@ -90,7 +90,7 @@ export default function ContractCategoryList() {
|
||||
</Button>
|
||||
</Link>
|
||||
<h1 className="text-2xl font-bold flex-1">Vertragstypen</h1>
|
||||
{hasPermission('developer:access') && (
|
||||
{hasPermission('contract-categories:create') && (
|
||||
<Button onClick={() => setShowModal(true)}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Neuer Vertragstyp
|
||||
@@ -144,12 +144,12 @@ export default function ContractCategoryList() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2 ml-4">
|
||||
{hasPermission('developer:access') && (
|
||||
{hasPermission('contract-categories:update') && (
|
||||
<Button variant="ghost" size="sm" onClick={() => handleEdit(category)} title="Bearbeiten">
|
||||
<Edit className="w-4 h-4" />
|
||||
</Button>
|
||||
)}
|
||||
{hasPermission('developer:access') && (
|
||||
{hasPermission('contract-categories:delete') && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
|
||||
@@ -49,7 +49,7 @@ export default function ContractDurationList() {
|
||||
</Button>
|
||||
</Link>
|
||||
<h1 className="text-2xl font-bold flex-1">Vertragslaufzeiten</h1>
|
||||
{hasPermission('platforms:create') && (
|
||||
{hasPermission('contract-durations:create') && (
|
||||
<Button onClick={() => setShowModal(true)}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Neue Laufzeit
|
||||
@@ -101,12 +101,12 @@ export default function ContractDurationList() {
|
||||
</td>
|
||||
<td className="py-3 px-4 text-right">
|
||||
<div className="flex justify-end gap-2">
|
||||
{hasPermission('platforms:update') && (
|
||||
{hasPermission('contract-durations:update') && (
|
||||
<Button variant="ghost" size="sm" onClick={() => handleEdit(duration)}>
|
||||
<Edit className="w-4 h-4" />
|
||||
</Button>
|
||||
)}
|
||||
{hasPermission('platforms:delete') && (
|
||||
{hasPermission('contract-durations:delete') && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
|
||||
@@ -204,7 +204,7 @@ function ProviderRow({
|
||||
<div className="border-t bg-gray-50 p-4">
|
||||
<div className="flex justify-between items-center mb-3">
|
||||
<h4 className="font-medium text-gray-700">Tarife</h4>
|
||||
{hasPermission('providers:create') && (
|
||||
{hasPermission('tariffs:create') && (
|
||||
<Button size="sm" onClick={() => setShowTariffModal(true)}>
|
||||
<Plus className="w-4 h-4 mr-1" />
|
||||
Tarif hinzufügen
|
||||
@@ -227,7 +227,7 @@ function ProviderRow({
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
{hasPermission('providers:update') && (
|
||||
{hasPermission('tariffs:update') && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@@ -240,7 +240,7 @@ function ProviderRow({
|
||||
<Edit className="w-3 h-3" />
|
||||
</Button>
|
||||
)}
|
||||
{hasPermission('providers:delete') && (
|
||||
{hasPermission('tariffs:delete') && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
|
||||
Reference in New Issue
Block a user