Commit Graph
6 Commits
Author SHA1 Message Date
duffyduckandClaude Opus 5 9f879759d8 CLI-Skripte: DATABASE_URL wieder aufbaubar machen
Die drei neuen Wartungsskripte bauten sich einen eigenen PrismaClient und
gingen damit an src/lib/prisma.ts vorbei. Dort wird DATABASE_URL aus den
DB_*-Teilen zusammengesetzt, falls sie fehlt - und genau das ist bei
`docker compose exec <dienst> npx tsx prisma/<skript>.ts` der Fall: Der
Entrypoint exportiert sie nur in den Serverprozess, eine neue Shell erbt
sie nicht. Ergebnis war "Environment variable not found: DATABASE_URL".

Der Kommentar in src/lib/prisma.ts beschreibt genau diesen Fall; die
Loesung war da, ich habe sie nur umgangen.

Betroffen war auch rolle-zuweisen.ts. Das ist der Weg, den die Startwache
im Klartext nennt, wenn kein Konto mehr die Pflichtrechte haelt - er
haette in dem Moment versagt, fuer den er gebaut ist.

Geprueft mit entzogener DATABASE_URL und nur DB_*-Teilen in der
Umgebung: rechte-report, rolle-zuweisen --liste und sync-roles laufen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-08 14:23:58 +02:00
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
duffyduckandClaude Opus 5 cca242119b R188/R189-01 aus dem Pentest umgesetzt - Klasse statt Instanz
Beide Findings der Pentesterin waren berechtigt. Ihre Patches liessen
sich nicht anwenden (Basis 791711c, seitdem 42 Commits, sync-roles.ts
kollidiert), und an zwei Stellen greifen sie zu kurz.

R188 - ungueltige IDs im Pfad
-----------------------------
Gemeldet: GET /api/users/:id gibt bei nicht-numerischer ID 500 statt 400.
Ihr Fix schliesst nebenbei mehr, als sie beansprucht: parseInt('12abc')
ergibt 12, also lieferte /api/users/12abc bisher Benutzer 12 aus.

Es waren aber 181 ungepruefte Stellen in 19 Controllern, nicht eine. 181
Einzel-Guards waeren genau der Fehler aus R186-01 gewesen - drei
Filterlisten, die dasselbe bedeuten sollten und auseinanderliefen.
Stattdessen router.param(), an einer Stelle fuer alle 33 Router
registriert, ueber einen mounte()-Helfer, der Pruefung und Einhaengen
zusammenbindet.

Antwort ist 404, nicht 400: Ein Pfadsegment, das keine ID sein kann,
benennt keine Ressource. Der bestehende Praezedenzfall in
provider.controller.ts (Pentest Mai 2026) hatte es genauso entschieden.

Dabei eine aeltere Heuristik abgeloest (Pentest Runde 7). Ihr eigener
Kommentar nannte den Grund fuer sie - "app.param() greift nicht auf in
Sub-Router gemounteten Routes" - und genau das loest mounte(). Sie war zu
eng (/users/abc ging durch und endete als 500) und zu weit (ein
Einstellungs-Schluessel 12abc unter :key wurde geblockt, obwohl das keine
ID ist), und sie antwortete 400, wo jetzt 404 steht.

R189-01 - DSGVO-Rechte ohne Traeger
------------------------------------
Gemeldet: gdpr:* und audit:read/export haengen an DSGVO und Developer,
die Admin-Rolle hat sie nicht, und nach einem frischen Seed war DSGVO
keinem Konto zugewiesen. Auskunft nach Art. 15 und Loeschung nach Art. 17
konnte niemand ausfuehren.

Seed weist admin@admin.com jetzt zusaetzlich die DSGVO-Rolle zu; die
Admin-Rolle selbst bleibt ohne diese Rechte, die Trennung aus R186 bleibt
also erhalten. Label ehrlich gemacht.

Beim Pruefen ihres Patches ein eigener Fund: seed.ts vergab an die
DSGVO-Rolle weiterhin audit:* komplett, inklusive audit:admin - die
Buendelung, die fc6f39e aufgeloest hat. Ich hatte damals zwei Listen
gefunden und die dritte uebersehen. Gerettet hat es nur die Reihenfolge
im Containerstart; ein einzelnes `npm run db:seed` brachte sie zurueck.

Der Seed hilft nur bei Neuinstallation (update: {}). Deshalb zusaetzlich
eine Wache beim Start: Gibt es fuer gdpr:export, gdpr:delete oder
audit:read kein aktives Konto, steht das mit Handlungsanweisung im Log -
Erkennung der ABWESENHEIT einer Faehigkeit, wie beim Heartbeat. Bewusst
nur melden, nicht automatisch vergeben.

Getestet ueber HTTP gegen eine Wegwerf-Instanz: alle ID-Varianten quer
ueber sechs Controller, Nicht-ID-Parameter unveraendert, frischer Seed,
Wache mit und ohne vergebene Rechte.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 21:17:12 +02:00
duffyduckandClaude Opus 5 fc6f39eba0 Aufsicht und Eingriff getrennt: neuer Haken "Audit-Betrieb"
Die DSGVO-Rolle trug audit:* komplett, also auch audit:admin. Ein
DSGVO-Beauftragter konnte damit seal-backlog, rehash und cleanup - seine
eigene Beweisgrundlage ersetzen. Wer das Protokoll beaufsichtigt, darf es
nicht umschreiben koennen. Dieselbe Klasse wie R184-02: falsche Domaene,
zu breit gebuendelt.

Der naheliegende Fix waere falsch gewesen. audit:admin einfach aus der
DSGVO-Rolle zu streichen haette es heimatlos gemacht: Die Admin-Rolle ist
ausdruecklich ohne audit/gdpr gebaut, einzige verbleibende Quelle waere
der Entwicklerzugriff - der alles gibt. Prod versiegeln haette dann
Vollzugriff vorausgesetzt.

Deshalb eine eigene versteckte Rolle "Audit-Betrieb" (audit:read +
audit:admin), zugewiesen ueber eine Checkbox wie DSGVO/Entwickler. DSGVO
behaelt audit:read + audit:export + gdpr:*. Fuer kein bestehendes Konto
weitet sich etwas aus; es wird enger, und wer eingreifen koennen soll,
bekommt es ausdruecklich.

Keine zusaetzliche Rechte-Huerde davor, weil das am Henne-Ei-Problem
scheitert: Nach der Aufteilung haelt zunaechst niemand audit:admin,
koennte ihn also auch niemand vergeben. Stattdessen wird die Vergabe
laut - CRITICAL im Protokoll und PERMISSION_CHANGED/CRITICAL im
Alarmkanal, samt Kennzeichen, ob sich jemand den Haken selbst gesetzt hat.

Nebenbei geschlossen: setUserGdprAccess() legte die DSGVO-Rolle im
Notfallpfad mit audit:* komplett an - eine zweite Liste, die dasselbe
bedeuten sollte und die Buendelung stillschweigend zurueckgebracht haette.

ACHTUNG beim Deploy: Bestehende DSGVO-Konten verlieren audit:admin. Wer
Prod versiegeln will, muss sich vorher "Audit-Betrieb" ankreuzen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 17:16:27 +02:00
duffyduckandClaude Opus 5 d2460fa7c0 Rolle "Gegenbuch": Leserecht aufs Audit-Protokoll ohne audit:admin
Beim Selbst-Nachpruefen eines Deploys auf Staging aufgefallen: Das
Gegenbuch-Dienstkonto meldete beim Login audit:read, audit:export,
audit:admin, gdpr:export, gdpr:delete und gdpr:admin. Es braucht genau
eines davon - audit:read -, denn es ruft nur /audit-logs/checkpoint und
/audit-logs/verify auf.

Das war kein Bedienfehler, sondern ein Konstruktionsfehler: Es gab keine
Rolle, die nur Leserecht aufs Protokoll gibt. Wer das wollte, musste den
Haken "DSGVO-Zugriff" setzen - und der vergibt audit:* komplett, also
auch audit:admin mit seal-backlog, rehash und cleanup. Das Label
("Audit-Logs, Datenschutz") legt Lesen nahe und liefert Vollzugriff.

Warum das ernst ist: Das Passwort des Dienstkontos liegt im Klartext in
tools/audit-notary/.env auf der Gegenbuch-Maschine. Mit audit:admin
haette ein Einbruch dort nicht nur den Waechter gehabt, sondern gleich
die Mittel zur Waesche aus R185-01 - und damit genau die Trennung
aufgehoben, wegen der das Gegenbuch auf einer eigenen Maschine laeuft.

Neue Rolle "Gegenbuch" in sync-roles.ts mit ausschliesslich audit:read.
sync-roles laeuft beim Containerstart mit, die Rolle erscheint danach in
der Benutzerverwaltung. README des Gegenbuchs umgeschrieben: Rolle statt
"selbst anlegen", ausdrueckliche Warnung vor dem DSGVO-Haken, dazu eine
Gegenprobe (checkpoint -> 200, seal-backlog -> 403).

Bewusst NICHT angefasst: dass die DSGVO-Rolle selbst audit:admin traegt,
ist ein Gewaltenteilungs-Problem - wer das Protokoll beaufsichtigt, kann
seine Beweisgrundlage ersetzen. Das zu aendern entzieht bestehenden
DSGVO-Konten Rechte und gehoert entschieden, nicht nebenbei gemacht.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 12:41:34 +02:00
duffyduckandClaude Opus 4.7 8ee5c9b07a 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>
2026-05-19 12:41:39 +02:00