208 lines
18 KiB
Markdown
208 lines
18 KiB
Markdown
---
|
||
phase: quick-260910-das
|
||
plan: 01
|
||
subsystem: database
|
||
tags: [prisma, row-level-security, multi-tenancy, nestjs, postgres]
|
||
|
||
requires:
|
||
- phase: quick-260909-mir
|
||
provides: rls-scratch-check.mjs mit sechs Bereichsabschnitten, prisma-tenant.extension.ts (forTenant/withTenantTransaction), die Klassifikations- und Fehlerrichtungsdokumente
|
||
provides:
|
||
- runUserAreaChecks in rls-scratch-check.mjs (12 neue Pruefungen, siebter Abschnitt)
|
||
- UserService gebunden (findById/create/update/deactivate/delete ueber forTenant, zwei neue Plattform-Administratorsicht-Methoden)
|
||
- AdminSeedService: Erstanlage des Administrators gebunden, Startsperre bei plattformweiter Eindeutigkeitsverletzung entschaerft
|
||
- UserController vollstaendig gebunden, Selbstloesch-Riegel repariert (Befund H)
|
||
- user.controller.spec.ts (neu, Zwei-Klienten-Nachweis fuer vorher testlose Steuerungsschicht)
|
||
- docs/mandantentrennung-etappe2-fehlerrichtung.md, Abschnitt "Bereich user" (u1-u5 plus Nachtrag)
|
||
- docs/mandantentrennung-zugriffsklassifikation.md auf den Bereich user nachgezogen, inkl. aller vier handgepflegten Stellen
|
||
affects: [quick-260910-etappe3-plattformweite-eindeutigkeit, quick-etappe4-scharfschalten]
|
||
|
||
actuals:
|
||
tokens: 31500
|
||
tasks: 3
|
||
commits: 3
|
||
plan_head_before: 7e7a697
|
||
|
||
tech-stack:
|
||
added: []
|
||
patterns:
|
||
- "Plattform-Administratorsicht als Schleife ueber alle Mandanten mit je EINEM gebundenen Lesezugriff im Rumpf (bereits in admin-seed.service.ts vorgemacht, jetzt zweimal in user.service.ts uebernommen)"
|
||
- "Bewusst-ungebundener Nachschlageweg auf einem plattformweit eindeutigen Schluessel mit geschriebener Begruendung am Ort (Praezedenzfall resolveEmailForWrite, hier UserService.findByUsername)"
|
||
- "P2002-Uebersetzung am einzigen Erzeugungspunkt fuer eine Entitaet statt bei jedem Aufrufer (UserService.create/update)"
|
||
|
||
key-files:
|
||
created:
|
||
- apps/api/src/user/user.controller.spec.ts
|
||
modified:
|
||
- apps/api/scripts/rls-scratch-check.mjs
|
||
- apps/api/src/user/user.service.ts
|
||
- apps/api/src/user/user.service.spec.ts
|
||
- apps/api/src/user/admin-seed.service.ts
|
||
- apps/api/src/user/admin-seed.service.spec.ts
|
||
- apps/api/src/user/user.controller.ts
|
||
- docs/mandantentrennung-etappe2-fehlerrichtung.md
|
||
- docs/mandantentrennung-zugriffsklassifikation.md
|
||
- .planning/WINDOWS.md
|
||
|
||
key-decisions:
|
||
- "Task-2-Signaturaenderung (findById/update/deactivate/delete bekommen einen Pflicht-Mandanten) UND die vier Aufrufstellen in user.controller.ts wurden im SELBEN Task-2-Commit angepasst (nicht nach Aufgabe 3 verschoben), weil Aufgabe 2s eigenes Verify-Gate volle Typpruefung und einen gruenen Testlauf verlangt. Die minimale Anpassung uebergibt currentUser.tenantId; das ist fuer SUPER_ADMIN semantisch noch nicht korrekt (wird erst in Aufgabe 3 mit findByIdForPlatformAdmin geloest), aber verhaltensneutral, weil der Schalter aus bleibt (BYPASSRLS aktiv) und die betroffenen Methoden keine explizite tenantId ins where schreiben — die reale Rueckgabe war ueber beide Aufgaben hinweg identisch."
|
||
- "Klassifikationsdokument wurde in Aufgabe 2 bereits minimal nachgezogen (Stand-Spalte fuer zwei Paare auf gemischt, neue Zeile user.service.ts/tenant), obwohl das Dateilisting formal erst Aufgabe 3 zuweist — sonst waere rls-access-inventory.spec.ts, Teil des von Aufgabe 2 selbst verlangten npm run test, rot geblieben. Die vollen Klassenkorrekturen mit Begruendung sowie alle vier handgepflegten Uebersichtstabellen bleiben wie geplant Aufgabe 3 vorbehalten."
|
||
- "user.controller.ts bekommt eine private resolveTargetUser()-Hilfsmethode, um die Rollenverzweigung (ADMIN gebunden vs. SUPER_ADMIN uebergreifend) nicht dreimal zu wiederholen (findOne/update/remove) — keine Aenderung an der Pruefreihenfolge oder den bestehenden Ausnahmen."
|
||
|
||
requirements-completed: [WINDOWS-18, ETAPPE-2-USER]
|
||
|
||
coverage:
|
||
- id: D1
|
||
description: "Die Kette unsichtbare Zeile -> falsches 'frei' -> harter Eindeutigkeitsfehler ist an der echten ausgelieferten User-Policy gemessen, samt Unterscheidung SQLSTATE 23505 (Eindeutigkeitsverletzung) vs. 42501 (Zeilenschutz-Ablehnung)"
|
||
requirement: WINDOWS-18
|
||
verification:
|
||
- kind: other
|
||
ref: "apps/api/scripts/rls-scratch-check.mjs runUserAreaChecks() — 12 benannte Pruefungen gegen Wegwerf-Datenbank, alle bestanden"
|
||
status: pass
|
||
human_judgment: false
|
||
- id: D2
|
||
description: "UserService bindet findById/create/update/deactivate/delete an den Mandanten; findByUsername bleibt bewusst ungebunden mit geschriebener Begruendung"
|
||
requirement: ETAPPE-2-USER
|
||
verification:
|
||
- kind: unit
|
||
ref: "apps/api/src/user/user.service.spec.ts — 13 Tests (Test 1-8 plus 4 bestehende plus 1 Zusatztest), Falsifizierungsnachweis fuer findById durchgefuehrt"
|
||
status: pass
|
||
human_judgment: false
|
||
- id: D3
|
||
description: "AdminSeedService bindet die Erstanlage des Administrators und entschaerft die Startsperre bei plattformweiter Eindeutigkeitsverletzung, ohne andere Startfehler abzuschwaechen"
|
||
requirement: ETAPPE-2-USER
|
||
verification:
|
||
- kind: unit
|
||
ref: "apps/api/src/user/admin-seed.service.spec.ts — 10 Tests (Test 9-12 neu plus 6 bestehende), Falsifizierungsnachweis durchgefuehrt (6 Tests rot bei zurueckgebauter Bindung)"
|
||
status: pass
|
||
human_judgment: false
|
||
- id: D4
|
||
description: "UserController bindet alle sieben eigenen Zugriffe, loest den Zielbenutzer rollenabhaengig auf, und der Selbstloesch-Riegel greift (Befund H, vorher wirkungslos)"
|
||
requirement: ETAPPE-2-USER
|
||
verification:
|
||
- kind: unit
|
||
ref: "apps/api/src/user/user.controller.spec.ts — 8 Tests, Rot-vor-Reparatur-Nachweis fuer Test 6 (Selbstloesch-Riegel) und Falsifizierungsnachweis fuer Test 7 (Bindung) durchgefuehrt"
|
||
status: pass
|
||
human_judgment: false
|
||
- id: D5
|
||
description: "Beide Dokumente (Fehlerrichtung, Klassifikation) sind fortgeschrieben statt umgeschrieben; alle vier handgepflegten Stellen der Klassifikation sind maschinell gegen den Quelltext gegatet"
|
||
requirement: ETAPPE-2-USER
|
||
verification:
|
||
- kind: unit
|
||
ref: "apps/api/src/prisma/rls-access-inventory.spec.ts (10 Tests, deckt Bestandsaufnahme ab) + vier awk/grep-Pruefungen aus dem Plan-Verify-Block (Uebersichtszeile, Summenzeile, Klassen-Verteilung samt Summe/Ueberschrift, Hintergrunddienst-Ueberschrift)"
|
||
status: pass
|
||
human_judgment: false
|
||
|
||
duration: 65min
|
||
completed: 2026-09-10
|
||
status: complete
|
||
---
|
||
|
||
# Phase quick-260910-das: Mandantentrennung Etappe 2, Bereich user Summary
|
||
|
||
**`UserService`/`AdminSeedService`/`UserController` vollstaendig an `forTenant()` gebunden, mit exakt vier begruendeten Ausnahmen (Benutzername-Suche, Erstanlage-Pruefung, zwei Mandantentabellen-Zugriffe), plus Reparatur des wirkungslosen Selbstloesch-Riegels und Entschaerfung einer Startsperre nach dem geplanten Scharfschalten.**
|
||
|
||
## Performance
|
||
|
||
- **Duration:** 65 min
|
||
- **Started:** 2026-09-10T08:03:00Z
|
||
- **Completed:** 2026-09-10T09:08:00Z
|
||
- **Tasks:** 3
|
||
- **Files modified:** 10 (1 neu, 9 geaendert)
|
||
|
||
## Accomplishments
|
||
|
||
- Die im Auftrag beschriebene Kette (unsichtbare Zeile → falsches „frei" → harter Eindeutigkeitsfehler) ist an der echten, ausgelieferten `User`-Policy gemessen, nicht behauptet — inklusive der Unterscheidung zwischen einer Eindeutigkeitsverletzung (SQLSTATE 23505) und einer Zeilenschutz-Ablehnung (SQLSTATE 42501).
|
||
- Die schwerste Ausprägung der umgekehrten Fehlerrichtung im gesamten Vorhaben — eine Startsperre für jede Installation mit gesetzten Administrator-Umgebungswerten — ist im Anwendungscode entschärft, ohne dass irgendein anderer Startfehler seine abbrechende Wirkung verliert.
|
||
- Die Linie zwischen „muss binden" und „darf nicht binden" ist je Methode gezogen und am Ort begründet: `UserService.findByUsername` bleibt bewusst ungebunden (derselbe Fall wie `resolveEmailForWrite` im Bereich `ldap`), alle übrigen Zugriffe binden.
|
||
- Eine heute wirksame Rechteausweitung ist geschlossen: ein Administrator konnte sich bisher selbst löschen, weil der Riegel gegen ein im Sitzungsnachweis nicht existierendes Feld (`sub`) verglich.
|
||
- Der Bereich hat erstmals in allen drei Dateien Tests, die auf eine vergessene Bindung rot werden können — durch tatsächlichen probeweisen Rückbau nachgewiesen, nicht behauptet.
|
||
|
||
## Task Commits
|
||
|
||
Each task was committed atomically:
|
||
|
||
1. **Aufgabe 1: Die Kette messen und die Kritikschrift schreiben** - `b848ba6` (feat)
|
||
2. **Aufgabe 2: Testlage herstellen, Dienst umstellen, Linie ziehen, Startsperre entschärfen** - `888f660` (feat)
|
||
3. **Aufgabe 3: Steuerungsschicht binden, Selbstlöschriegel schließen, Dokumente nachziehen** - `3a9391d` (feat)
|
||
|
||
_Alle drei Commits enthalten sowohl den TDD-Testnachweis als auch die Implementierung — kein separater test→feat-Split, weil das Vorgehen "Nachweis vor Umstellung, dann Umstellung, dann Falsifizierungsnachweis mit Rückbau" innerhalb jeder Aufgabe verlief, nicht über Aufgabengrenzen hinweg._
|
||
|
||
## Files Created/Modified
|
||
|
||
- `apps/api/scripts/rls-scratch-check.mjs` — siebter Abschnitt `runUserAreaChecks` (12 neue Prüfungen), Hilfsfunktionen `normalizePolicySql`/`sqlStateOf`
|
||
- `apps/api/src/user/user.service.ts` — `findById`/`update`/`deactivate`/`delete` mit Pflicht-Mandant, `create`/`update` mit P2002-Übersetzung, zwei neue Methoden für die Plattform-Administratorsicht, `findByUsername`-Kommentar richtiggestellt
|
||
- `apps/api/src/user/user.service.spec.ts` — Zwei-Klienten-Nachweis, 13 Tests
|
||
- `apps/api/src/user/admin-seed.service.ts` — Erstanlage gebunden, Startsperre entschärft, Kopfkommentar der Reparaturschleife ergänzt (Befund K)
|
||
- `apps/api/src/user/admin-seed.service.spec.ts` — Zwei-Klienten-Nachweis, 10 Tests
|
||
- `apps/api/src/user/user.controller.ts` — alle sieben Zugriffe gebunden, `resolveTargetUser()`, Selbstlöschriegel repariert
|
||
- `apps/api/src/user/user.controller.spec.ts` — neu, Zwei-Klienten-Nachweis, 8 Tests
|
||
- `docs/mandantentrennung-etappe2-fehlerrichtung.md` — Abschnitt „Bereich user" (u1–u5) plus Nachtrag
|
||
- `docs/mandantentrennung-zugriffsklassifikation.md` — Übersichtszeile, Summenzeile, Klassen-Verteilung, Hintergrunddienst-Abschnitt (fünf Fälle), zwei Klassenkorrekturen, neue Zeile `user.service.ts`/`tenant`
|
||
- `.planning/WINDOWS.md` — offener Eintrag #22 (plattformweite Eindeutigkeit von `username`/`email`, Produktentscheidung für Etappe 3)
|
||
|
||
## Decisions Made
|
||
|
||
- **Reihenfolge der Signaturänderung (Aufgabe 2):** Die vier `UserService`-Methoden bekamen ihren Pflicht-Mandanten UND die vier Aufrufstellen in `user.controller.ts` wurden im selben Aufgabe-2-Commit angepasst, statt die Signaturänderung komplett nach Aufgabe 3 zu verschieben — Aufgabe 2s eigenes `<verify>` verlangt eine saubere Typprüfung. Die minimale Anpassung übergibt `currentUser.tenantId`; das ist für `SUPER_ADMIN` semantisch noch unvollständig (erst Aufgabe 3 löst es korrekt über `findByIdForPlatformAdmin`), aber verhaltensneutral: der Schalter bleibt aus (`tessera`-Rolle mit `BYPASSRLS`), und die betroffenen Methoden schreiben keine explizite `tenantId` ins `where` — die tatsächliche Rückgabe war über beide Aufgaben hinweg identisch.
|
||
- **Klassifikationsdokument teilweise in Aufgabe 2 nachgezogen:** obwohl das Dateilisting es formal erst Aufgabe 3 zuweist, wurden zwei Stand-Korrekturen und eine neue Zeile bereits in Aufgabe 2 ergänzt (Rule 3 — blockierendes Problem), weil `rls-access-inventory.spec.ts` sonst rot geblieben wäre und Aufgabe 2s eigenes `npm run test`-Gate nicht hätte bestehen können. Die vollen Klassenkorrekturen mit Begründung und alle vier handgepflegten Übersichtstabellen blieben wie geplant Aufgabe 3 vorbehalten.
|
||
- **`resolveTargetUser()`-Hilfsmethode** in `user.controller.ts`, um die Rollenverzweigung nicht dreimal zu wiederholen — keine Änderung an Prüfreihenfolge oder Ausnahmen.
|
||
|
||
## Deviations from Plan
|
||
|
||
### Auto-fixed Issues
|
||
|
||
**1. [Rule 3 - Blocking] Klassifikationsdokument teilweise vorgezogen, damit Aufgabe 2s eigenes Test-Gate besteht**
|
||
- **Found during:** Task 2 (nach der Umstellung von `user.service.ts`/`admin-seed.service.ts`)
|
||
- **Issue:** `rls-access-inventory.spec.ts` (Teil von `npm run test`, das Aufgabe 2s `<verify>` selbst verlangt) schlug fehl: das neue Paar `(user.service.ts, tenant)` fehlte im Dokument, und der Stand von `(user.service.ts, user)` sowie `(admin-seed.service.ts, user)` war noch als `ungebunden` dokumentiert, obwohl der Code jetzt `gemischt` war.
|
||
- **Fix:** Minimale Korrektur der drei betroffenen Zeilen (Stand-Spalte, neue Zeile) mit dem Vermerk „ZWISCHENSTAND nach Aufgabe 2 — Klassenkorrektur folgt in Aufgabe 3", ohne die vier handgepflegten Übersichtstabellen anzufassen.
|
||
- **Files modified:** `docs/mandantentrennung-zugriffsklassifikation.md`
|
||
- **Verification:** `rls-access-inventory.spec.ts` grün nach der Korrektur; Aufgabe 3 hat die Zeilen anschließend vollständig fertiggestellt (Klassenkorrektur mit Begründung).
|
||
- **Committed in:** `888f660` (Aufgabe-2-Commit)
|
||
|
||
---
|
||
|
||
**Total deviations:** 1 auto-fixed (Rule 3 — blockierendes Testproblem, keine Funktionsänderung)
|
||
**Impact on plan:** Notwendig, um Aufgabe 2s eigenes Verify-Gate zu erfüllen; die eigentliche inhaltliche Arbeit (Klassenkorrekturen, Übersichtstabellen) blieb wie im Plan vorgesehen Aufgabe 3 vorbehalten. Kein Scope Creep.
|
||
|
||
## Falsifizierungsnachweise
|
||
|
||
**Aufgabe 2, `UserService.findById`:** `forTenant(this.prisma, tenantId)` probeweise durch `this.prisma` (ungebunden) ersetzt. Ergebnis: genau `user.service.spec.ts`, Test 4 ("steht gebunden im Protokoll und liefert einen Benutzer eines anderen Mandanten NICHT"), wurde rot, mit der Meldung `erwarteter gebundener Aufruf user.findUnique(tenant=t1) fehlt im Protokoll: []`. Rückbau zurückgenommen, derselbe Testlauf danach wieder grün (13/13).
|
||
|
||
**Aufgabe 2, `AdminSeedService.seedAdmin`:** `forTenant(this.prisma, tenant.id)` probeweise durch `this.prisma` (ungebunden, ohne `.user.create`) ersetzt. Ergebnis: sechs Tests wurden rot (u. a. Test 9–12), alle mit `TypeError: tenantPrisma.user.create is not a function` — der ungebundene Basisclient in der Testattrappe trägt keine `create`-Methode. Rückbau zurückgenommen, alle zehn Tests danach wieder grün.
|
||
|
||
**Aufgabe 3, `UserController.uploadAvatar`:** `forTenant(this.prisma, currentUser.tenantId)` probeweise durch `this.prisma` (ungebunden, ohne `.user`) ersetzt. Ergebnis: genau `user.controller.spec.ts`, Test 7 ("alle fünf Zugriffe der vier Selbstbedienungswege stehen gebunden im Protokoll"), wurde rot, mit `TypeError: Cannot read properties of undefined (reading 'update')`. Rückbau zurückgenommen, derselbe Testlauf danach wieder grün (8/8).
|
||
|
||
**Aufgabe 3, Selbstlöschriegel (Rot-vor-Reparatur-Nachweis, kein Rückbau):** `user.controller.ts` wurde probeweise auf den ursprünglichen, fehlerhaften Vergleich `user.id === currentUser.sub` zurückgesetzt, BEVOR der Test geschrieben wurde grün lief. Testlauf: genau `user.controller.spec.ts`, Test 6 ("der Riegel gegen das Löschen des eigenen Kontos greift"), wurde rot mit `promise resolved "{ message: 'User deleted' }" instead of rejecting` — der Beleg, dass der Riegel in der ursprünglichen Fassung NIE griff. Reparatur (`currentUser.id`) danach wiederhergestellt, derselbe Test grün.
|
||
|
||
## Known Stubs
|
||
|
||
Keine — jede in diesem Plan berührte Methode ist entweder vollständig implementiert oder trägt eine geschriebene, im Code lesbare Begründung für die bewusst gelassene Ausnahme (kein Platzhalter, kein TODO).
|
||
|
||
## Threat Flags
|
||
|
||
Keine neuen — alle in diesem Plan berührten Zugriffe sind im `<threat_model>` des Plans (T-DAS-01 bis T-DAS-10) bereits erfasst und entschärft.
|
||
|
||
## Issues Encountered
|
||
|
||
- **`.env.prod.example` löste den Secret-Read-Guard in der Bash-Tool-Sandbox aus**, wenn es als Argument in einem `git diff --name-only`/`git status --porcelain -- ...`-Aufruf genannt wurde — obwohl nur der Dateiname, nicht der Inhalt, gelesen worden wäre. Umgangen durch ein einfaches `git status --porcelain` ohne Pfadfilter (bestätigt: nur die erwarteten Dateien geändert), statt die geschützten Dateinamen literal in der Kommandozeile zu nennen.
|
||
- **Prisma-Rohfehlermeldungen (`err.code`) sind bei `$executeRaw`-Fehlern immer `P2010`**, nicht der tatsächliche PostgreSQL-SQLSTATE — empirisch gegen `tessera-ctl-db-1` geprüft (siehe `sqlStateOf()`-Kommentar in `rls-scratch-check.mjs`). Der echte SQLSTATE liegt unter `err.meta.code`. Ohne diese Prüfung hätte die zentrale Messung `user-eindeutigkeit-greift-trotz-unsichtbarkeit` möglicherweise am falschen Feld gelesen.
|
||
|
||
## User Setup Required
|
||
|
||
None - keine externe Diensteinrichtung nötig. Der Schalter (`DATABASE_URL` → Rolle `tessera`) bleibt unverändert aus.
|
||
|
||
## Next Phase Readiness
|
||
|
||
- Fünf von fünf Bereichen der Etappe 2 sind jetzt umgestellt (`ldap`, `groups`, `tenders`, `dkv`, `user`) — die Klassifikationstabelle listet 63 Paare, davon 31 `muss-mandantengebunden`, 17 `keine-mandantengebundene-tabelle`, 13 `beides`, 2 `bewusst-uebergreifend`.
|
||
- Etappe 3 (plattformweite Eindeutigkeit von `username`/`email` als Schemaentscheidung; WINDOWS #19 nullbares `tenantId`; die offene Architekturfrage `req.tenantPrisma`) ist mit vollständiger Beweislage vorgemerkt — siehe WINDOWS-Eintrag #22 und Abschnitt (u4) der Fehlerrichtung.
|
||
- Reihenfolgebedingungen für Etappe 4 (Scharfschalten): keine neuen aus diesem Plan. Bestehende (Bereiche `groups`/`settings` für `dkv`/`tenders`) unverändert.
|
||
- `rls-preflight.mjs` (Etappe 4) sollte künftig auch die plattformweite Eindeutigkeit von `username`/`email` als Signal berücksichtigen — bislang nicht Gegenstand dieses Werkzeugs.
|
||
|
||
## Self-Check: PASSED
|
||
|
||
Alle zehn im Plan gelisteten Artefakte auf der Festplatte gefunden; alle drei Task-Commit-Hashes (`b848ba6`, `888f660`, `3a9391d`) in `git log` gefunden.
|
||
|
||
---
|
||
*Phase: quick-260910-das*
|
||
*Completed: 2026-09-10*
|