Files
tessera-ctl/.planning/quick/260911-e2s-mandantentrennung-etappe-2-bereich-tenan/260911-e2s-SUMMARY.md
T
schalli 6236b302f4
Tessera CI/CD / Lint & Type Check (push) Successful in 47s
Tessera CI/CD / Tests (push) Successful in 56s
Tessera CI/CD / Build & Publish Images (push) Successful in 29s
docs(quick-260911-e2s): Etappe 2 Bereich tenant abgeschlossen, WINDOWS #27 Relations-Blindstelle
2026-09-11 11:08:33 +02:00

249 lines
19 KiB
Markdown
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.
---
phase: quick-260911-e2s
plan: 01
subsystem: database
tags: [prisma, postgres, row-level-security, nestjs, multi-tenancy]
requires:
- phase: quick-260911-cwh
provides: neunte umgestellte Bereich (calendar), die Etappe-2-Konvention der dienst-internen forTenant()-Bindung
provides:
- runTenantAreaChecks (9 neue Pruefungen im Wegwerf-Werkzeug, 6 davon ueber den generierten Client)
- TenantGuard ohne Prisma-Abhaengigkeit, setzt ausschliesslich req.tenantId
- tenant.middleware.ts geloescht (nie verdrahtet)
- TenantController: drei gebundene Benutzerzaehler (Fan-out je Mandant) statt Relationszaehler
- Testlage fuer Guard und Controller aus dem Nichts (27 neue Testfaelle)
- Architekturfrage req.tenantPrisma fuer ALLE Bereiche der Etappe 2 entschieden
affects: [tenant, user, groups, auth, module-registry]
actuals:
tokens: 22215
tasks: 3
commits: 4
plan_head_before: 6426b18630923a35bfee54c7b211a022adafd3c6
tech-stack:
added: []
patterns:
- "Fan-out je Mandant fuer Plattform-Administratorsichten: ungebundener Treiber (this.prisma.tenant.findMany) plus je Mandant EIN gebundener Zaehler/Lesezugriff (forTenant(this.prisma, tenant.id)), wortgleiche Form wie UserService.findAllForPlatformAdmin"
- "Guard setzt nur die Mandantenkennung (req.tenantId); die Bindung an einen Prisma-Client geschieht ausschliesslich dienst-intern je Methode — settled convention nach zehn Bereichen"
key-files:
created:
- apps/api/src/tenant/tenant.guard.spec.ts
- apps/api/src/tenant/tenant.controller.spec.ts
modified:
- apps/api/scripts/rls-scratch-check.mjs
- apps/api/src/tenant/tenant.guard.ts
- apps/api/src/tenant/tenant.controller.ts
- apps/api/src/prisma/rls-access-inventory.spec.ts
- apps/api/src/app.module.ts
- apps/api/src/module-registry/module.guard.ts
- apps/api/src/dkv/dkv.controller.ts
- docs/mandantentrennung-etappe2-fehlerrichtung.md
- docs/mandantentrennung-zugriffsklassifikation.md
- docs/anleitung-entwicklung.md
deleted:
- apps/api/src/tenant/tenant.middleware.ts
key-decisions:
- "req.tenantPrisma entfernt, fuer ALLE Bereiche der Etappe 2 entschieden: dienst-interne Bindung (ein forTenant()-Client je Methode) ist die Konvention, keine Anfrageobjekt-Eigenschaft. Grund: neunfache Praxis vor diesem Bereich; ein gebundener Klient ohne Leser war Kosten ohne Nutzen und sah wie ein Sicherheitsmechanismus aus, der nicht wirkte."
- "tenant.middleware.ts geloescht statt nur entschaerft — sie war nirgends verdrahtet (kein MiddlewareConsumer, kein configure() in ganz apps/api) und hatte identische Logik wie der Guard."
- "Drei Relationszaehler im TenantController (findAll/findOne/remove) durch gebundene Fan-out-Zaehler ersetzt, weil Prisma include:{_count} als EINE Anweisung mit LEFT JOIN in die geschuetzte Tabelle User laeuft — nach dem Scharfschalten waere das userCount=0 fuer jeden Mandanten gewesen."
requirements-completed: [WINDOWS-18, ETAPPE-2-TENANT]
coverage:
- id: D1
description: "runTenantAreaChecks misst neun Verhaltensweisen des Bereichs tenant gegen die echte Wegwerf-Datenbank: keine Regel in allen 34 Migrationen, gebunden=ungebunden (Roh-SQL und generierter Client), Relationszaehler liefert ungebunden 0/0/0, gebundener Fan-out liefert die richtigen Zahlen, Loeschriegel-Umgehung wird vom Fremdschluessel laut abgefangen"
requirement: WINDOWS-18
verification:
- kind: integration
ref: "apps/api/scripts/rls-scratch-check.mjs — 110/110 Pruefungen bestanden (101 bisherige + 9 neue)"
status: pass
human_judgment: false
- id: D2
description: "TenantGuard setzt ausschliesslich req.tenantId, keine Prisma-Abhaengigkeit mehr; alle fuenf Zweige inklusive x-tenant-id-Wechsel und Abwesenheit der alten Eigenschaft als Test festgenagelt"
requirement: ETAPPE-2-TENANT
verification:
- kind: unit
ref: "apps/api/src/tenant/tenant.guard.spec.ts — 7/7 Faelle"
status: pass
human_judgment: false
- id: D3
description: "TenantController: findAll/findOne/remove zaehlen Benutzer je Mandant ueber drei gebundene Aufrufstellen statt Relationszaehler; Antwortform und Verhalten unveraendert"
requirement: ETAPPE-2-TENANT
verification:
- kind: unit
ref: "apps/api/src/tenant/tenant.controller.spec.ts — 20/20 Faelle (Zwei-Klienten-Nachweis, Rollen-Metadaten, Wachhund)"
status: pass
human_judgment: false
- id: D4
description: "Alle fuenf handgepflegten Klassifikationsstellen plus die Kritikschrift sind nachgezogen und maschinell gegatet (64 Paare, Uebersichtszeile 8/3, Klassen-Verteilung)"
verification:
- kind: unit
ref: "apps/api/src/prisma/rls-access-inventory.spec.ts — 11/11 (inkl. neuer Wachhund gegen veraltete Ausnahmeeintraege)"
status: pass
human_judgment: false
duration: ~28min
completed: 2026-09-11
status: complete
---
# Quick 260911-e2s: Mandantentrennung Etappe 2, Bereich tenant Summary
**`Tenant` selbst braucht keine Bindung (gemessen ueber alle 34 Migrationen), aber drei Relationszaehler im `TenantController` liefen unbemerkt unter der Regel von `User` — jetzt durch einen gebundenen Fan-out ersetzt; die seit Etappe 1 offene Frage zum Anfrageobjekt-Klienten ist fuer alle Bereiche entschieden und der Guard hat keine Prisma-Abhaengigkeit mehr.**
## Performance
- **Duration:** ~28 min
- **Tasks:** 3/3
- **Files modified:** 13 (10 geaendert, 2 neu angelegt, 1 geloescht)
- **Commits:** 4 (3 fachliche Task-Commits + 1 Nachtrag fuer eine fehlerhafte `git add`-Staging)
## Accomplishments
- **Wegwerf-Werkzeug erweitert:** `runTenantAreaChecks` (12. Abschnitt in `rls-scratch-check.mjs`) misst neun benannte Verhaltensweisen, sechs davon ueber den generierten Prisma-Client (nicht nur Roh-SQL) — darunter die tragende Belegzeile, dass der Relationszaehler ungebunden fuer JEDEN Mandanten 0 liefert, und dass der Fremdschluessel `User_tenantId_fkey` (wortgleich aus der Migration geschnitten) ein durch den vakuumen Riegel durchgelassenes Loeschen laut abfaengt. 101 → 110 Pruefungen, alle gruen.
- **Kritikschrift erweitert:** `docs/mandantentrennung-etappe2-fehlerrichtung.md` bekommt den Abschnitt "## Bereich tenant" mit der tatsaechlich beobachteten Werkzeugausgabe, einer Signaltabelle je Pfad, den Frontend-Stellen, die die falsche Zahl unkommentiert durchlassen (`admin/tenants/page.tsx`, `TenantContextSelector.tsx`), und der vollstaendigen Entscheidung zur Anfrageobjekt-Eigenschaft.
- **Architekturfrage entschieden (fuer ALLE Bereiche der Etappe 2, nicht nur `tenant`):** `TenantGuard` setzt nur noch `req.tenantId`, hat keinen Konstruktor-Parameter mehr; `tenant.middleware.ts` (nie verdrahtet, identische Logik) ist geloescht. `FORTENANT_ASSIGNMENT_EXCEPTIONS` in `rls-access-inventory.spec.ts` ist leer und durch einen neuen Wachhund-Test gegen veraltete Eintraege abgesichert.
- **Fan-out im Controller:** `findAll`/`findOne`/`remove` zaehlen Benutzer je Mandant ueber drei gebundene `tenantPrisma.user.count`-Aufrufstellen (Muster `UserService.findAllForPlatformAdmin`); die vier `tenant`-Zugriffe selbst bleiben bewusst ungebunden (keine Regel, gemessen).
- **Testlage aus dem Nichts:** `tenant.guard.spec.ts` (7 Faelle) und `tenant.controller.spec.ts` (20 Faelle, davon 9 in `findAll`/`findOne`/`create`/`update`/`remove`, ein Rollen-Metadaten-Test, fuenf Handler-Metadaten-Tests, drei Wachhund-Tests) — zuvor gab es fuer diesen Bereich nur zwei Faelle in `tenant.service.spec.ts`.
- **Klassifikation nachgezogen:** 63 → 64 (Datei, Modell)-Paare (neu: `tenant.controller.ts`/`user`), Uebersichtszeile `tenant` 8/0 → 8/3, Klassen-Verteilung `muss-mandantengebunden` 31 → 32, "Zwei belegte Befunde" und "Was diese Etappe NICHT entscheidet" (erster Punkt) aufgeloest.
## Task Commits
1. **Aufgabe 1: Fehlerrichtung messen** — `652e762` (feat) — `runTenantAreaChecks` + Kritikschrift-Abschnitt
2. **Aufgabe 2: Guard-Umbau, Middleware geloescht** — `11f5731` (feat) — nur `tenant.middleware.ts` (Loeschung) und `tenant.guard.spec.ts` (neu) tatsaechlich erfasst
3. **Aufgabe 2 nachgetragen** — `17dca0d` (fix) — die restlichen fuenf Dateien des Guard-Umbaus (siehe Deviations unten)
4. **Aufgabe 3: Fan-out binden, Klassifikation nachziehen** — `c8de72e` (feat) — Controller, Controller-Spec, beide Dokumente
**Plan metadata:** wird vom Orchestrator committet (SUMMARY.md/STATE.md nicht Teil dieser Task-Commits)
## Files Created/Modified
- `apps/api/scripts/rls-scratch-check.mjs` — `runTenantAreaChecks`, neun Pruefungen, zwischen `runCalendarAreaChecks` und `runTransactionShapeMeasurement`
- `apps/api/src/tenant/tenant.guard.ts` — nur noch `req.tenantId`, keine Prisma-Abhaengigkeit
- `apps/api/src/tenant/tenant.guard.spec.ts` — NEU, 7 Faelle
- `apps/api/src/tenant/tenant.middleware.ts` — GELOESCHT
- `apps/api/src/tenant/tenant.controller.ts` — Fan-out-Zaehler statt Relationszaehler
- `apps/api/src/tenant/tenant.controller.spec.ts` — NEU, 20 Faelle
- `apps/api/src/prisma/rls-access-inventory.spec.ts` — leere `FORTENANT_ASSIGNMENT_EXCEPTIONS` + Wachhund-Test
- `apps/api/src/app.module.ts` — Kommentarzeile korrigiert (nennt nur noch `req.tenantId`)
- `apps/api/src/module-registry/module.guard.ts` — Kommentarzeile korrigiert (`TenantGuard` statt `TenantMiddleware`)
- `apps/api/src/dkv/dkv.controller.ts` — Kommentarzeile korrigiert (`TenantGuard` statt `TenantMiddleware`)
- `docs/mandantentrennung-etappe2-fehlerrichtung.md` — Abschnitt "## Bereich tenant" (n1)-(n5)
- `docs/mandantentrennung-zugriffsklassifikation.md` — 64 Paare, alle handgepflegten Stellen nachgezogen
- `docs/anleitung-entwicklung.md` — Guard-Beschreibung und drei weitere Stellen ohne `req.tenantPrisma`/`TenantMiddleware` (siehe Deviations)
## Decisions Made
- **req.tenantPrisma entfernt, fuer die gesamte Etappe 2 entschieden.** Gemessen: kein Leser ausserhalb von Guard/Middleware, Middleware nirgends verdrahtet. Entschieden: dienst-interne Bindung ist die Konvention (zehnter Bereich in Folge). Grund: Kosten ohne Nutzen, und tote Verdrahtung, die wie Schutz aussieht, ist schlimmer als keine.
- **tenant.middleware.ts geloescht statt nur entschaerft** — eine nie aufgerufene Kopie des Guards mit identischer Logik ist tote Verdrahtung in Reinform.
- **Fan-out statt Relationszaehler** — der Relationszaehler lief unter der Regel von `User`; der Fan-out (ungebundener Treiber, je Mandant EIN gebundener Zaehler) ist die bereits im Codebestand vorhandene Reparaturform (`UserService.findAllForPlatformAdmin`).
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 1 - Prozessfehler] `git add` mit mehreren Pfaden schlug fatal fehl und liess fünf Dateien unstaged**
- **Found during:** Aufgabe 2, beim Commit
- **Issue:** `git add <7 Pfade>` enthielt den bereits per `git rm` entfernten Pfad `tenant.middleware.ts` — Git quittierte das mit "Pfadspezifikation stimmt mit keinen Dateien überein" und staged dabei GAR KEINEN der sieben Pfade (nicht nur den fehlerhaften). Der darauffolgende Commit (`11f5731`) enthielt deshalb nur die zwei Dateien, die vorher schon separat gestaged waren (`tenant.middleware.ts` per `git rm`, `tenant.guard.spec.ts` per Einzel-`git add`) — der eigentliche Guard-Umbau (`tenant.guard.ts`, `rls-access-inventory.spec.ts`, `app.module.ts`, `module.guard.ts`, `dkv.controller.ts`) blieb im Arbeitsverzeichnis unstaged, unsichtbar in der `git commit`-Ausgabe ("2 files changed"), aber sichtbar in einem nachfolgenden `git status`.
- **Fix:** Fuenf fehlende Dateien einzeln mit `git add` gestaged und in einem separaten Commit (`17dca0d`) nachgetragen, mit expliziter Erklaerung der Ursache in der Commit-Botschaft. Inhaltlich identisch mit dem bereits verifizierten Stand (891 Tests gruen, Typpruefung sauber) — nur die Staging-Reihenfolge war fehlerhaft.
- **Files modified:** apps/api/src/tenant/tenant.guard.ts, apps/api/src/prisma/rls-access-inventory.spec.ts, apps/api/src/app.module.ts, apps/api/src/module-registry/module.guard.ts, apps/api/src/dkv/dkv.controller.ts
- **Verification:** `git diff --name-only f1017fa` listet nach dem Nachtrag exakt die 13 erwarteten Dateien; alle Task-2- und Task-3-Gates liefen danach erneut und bestanden.
- **Committed in:** `17dca0d`
**2. [Rule 1 - Bug] Guard-Kopfkommentar verletzte das eigene Gate (Punkt-Zugriff `.tenantPrisma`, Nennung von `TenantMiddleware`)**
- **Found during:** Aufgabe 2, unmittelbar nach dem ersten Entwurf des Kopfkommentars
- **Issue:** Der erste Entwurf des Kopfkommentars in `tenant.guard.ts` beschrieb die alte Anfrageobjekt-Eigenschaft mit `req.tenantPrisma = forTenant(...)` (Punkt-Zugriff) und nannte den Klassennamen `TenantMiddleware` woertlich — beides verletzt die eigenen Gates dieser Aufgabe (`grep -rn '\.tenantPrisma'`/`grep -rn 'TenantMiddleware'` ueber ganz `apps/api/src` muessen 0 liefern, auch in Kommentaren).
- **Fix:** Umformuliert ohne Punkt-Zugriff ("unter einer Eigenschaft namens `tenantPrisma`") und ohne den Klassennamen ("ein nie registrierter Express-Middleware-Klasse mit derselben Logik").
- **Files modified:** apps/api/src/tenant/tenant.guard.ts
- **Verification:** beide Gates liefern 0 im gesamten `apps/api/src`.
- **Committed in:** `17dca0d`
**3. [Rule 3 - Blocking] Zwei zusaetzliche `req.tenantPrisma`-Stellen in `docs/anleitung-entwicklung.md` ausserhalb des im Auftrag genannten ersten Absatzes**
- **Found during:** Aufgabe 3, TEIL 3
- **Issue:** Der Auftrag beschraenkte die Aenderung auf den ersten Absatz des Abschnitts "## Mandantentrennung" und den Hinweiskasten. Das Gate verlangt aber `test 0 -eq "$(grep -c 'req.tenantPrisma' docs/anleitung-entwicklung.md)"` fuer die GESAMTE Datei — und ein frueherer Abschnitt ("Weg einer Anfrage") nannte `req.tenantPrisma` an zwei weiteren Stellen (Schritt 2 und Schritt 4 der Anfrage-Reihenfolge).
- **Fix:** Beide Stellen ebenfalls korrigiert (Schritt 2: nur noch `req.tenantId`; Schritt 4: "dienst-intern per `forTenant()` gebundener Client" statt `req.tenantPrisma`) — inhaltlich dieselbe Berichtigung wie im Abschnitt "## Mandantentrennung" selbst, nur an zwei zusaetzlichen Stellen noetig, um das datei-weite Gate zu erfuellen.
- **Files modified:** docs/anleitung-entwicklung.md
- **Verification:** `grep -c 'req.tenantPrisma' docs/anleitung-entwicklung.md` liefert 0.
- **Committed in:** `c8de72e`
---
**Total deviations:** 3 auto-fixed (1 Prozessfehler beim Staging, 1 Bug im eigenen Kommentarentwurf, 1 datei-weites Gate erforderte zwei zusaetzliche Korrekturstellen)
**Impact on plan:** Keine inhaltliche Abweichung vom Plan — alle drei Punkte sind Korrekturen innerhalb der bereits verifizierten Aufgaben, kein Scope Creep. Die Endzahlen (13 geaenderte Dateien, 110/110 Werkzeugpruefungen, 911/59 Tests) stimmen mit dem an, was der Plan verlangt.
## Falsifizierungsnachweise (woertlich)
1. **Guard (Aufgabe 2):** probeweise `(req as any).tenantPrisma = 'probe';` nach der `req.tenantId`-Zuweisung im SUPER_ADMIN-Zweig eingefuegt. `tenant.guard.spec.ts` wurde rot: 4 von 7 Faellen fehlgeschlagen, u. a.
```
FAIL src/tenant/tenant.guard.spec.ts > TenantGuard.canActivate > SUPER_ADMIN mit tenantId, ohne Kopfzeile: req.tenantId === die eigene Kennung
AssertionError: expected true to be false
- Expected: false
+ Received: true
❯ expect('tenantPrisma' in req).toBe(false);
```
Zustand danach zurueckgestellt (`cp` aus Sicherung), `tenant.guard.spec.ts` wieder 7/7 gruen.
2. **Controller (Aufgabe 3):** in `findOne` den gebundenen Zaehler probeweise durch `(this.prisma as any).user.count(...)` (ungebundener Basisclient) ersetzt. `tenant.controller.spec.ts` wurde rot: 2 von 20 Faellen fehlgeschlagen, exakt in der erwarteten Form:
```
FAIL src/tenant/tenant.controller.spec.ts > TenantController.findOne > bekannte Kennung: userCount aus dem gebundenen Klienten UNTER DIESER Kennung
TypeError: Cannot read properties of undefined (reading 'count')
❯ TenantController.findOne src/tenant/tenant.controller.ts:102:55
```
(der ungebundene Nachbau hat kein `user`-Modell — die `dkv`-Form der Falsifizierung, nicht nur eine falsche Zahl). Zustand danach zurueckgestellt, 20/20 wieder gruen.
3. **Dokument-Gate (a), Bestandsaufnahme-Zeile (Aufgabe 3):** die neue Zeile `tenant.controller.ts`/`user` probeweise auf `ungebunden` gesetzt. `rls-access-inventory.spec.ts` wurde rot:
```
AssertionError: Fehlende Eintraege im Dokument:
apps/api/src/tenant/tenant.controller.ts::user — dokumentiert=ungebunden, gemessen=gebunden
```
Zurueckgestellt, 11/11 wieder gruen.
4. **Dokument-Gate (b), Uebersichtszeile (Aufgabe 3):** die Zeile `| tenant | 8 | 3 |` probeweise auf `| tenant | 8 | 99 |` gesetzt. Das herleitende Gate (`grep -qE "^\| tenant \| ${DU} \| ${DB} \| ..."`) schlug fehl (kein Treffer mehr). Zurueckgestellt.
## Issues Encountered
Keine ausser der oben dokumentierten Staging-Panne (Deviation 1) — beide Falsifizierungsnachweise und beide Dokument-Falsifizierungen liefen beim ersten Versuch wie erwartet rot.
## User Setup Required
None - keine externe Konfiguration noetig.
## Next Phase Readiness
- Zehn von zwoelf Bereichen der Etappe 2 sind umgestellt (`tenant` war der zehnte). Verbleibend laut Klassen-Verteilung: die uebrigen Bereiche mit `muss-mandantengebunden`/`beides`-Paaren, die noch nicht Stand `gebunden` tragen — die Klassifikationstabelle in `docs/mandantentrennung-zugriffsklassifikation.md` ist die autoritative Quelle fuer den verbleibenden Arbeitsvorrat.
- Die Architekturfrage zu `req.tenantPrisma` ist fuer ALLE verbleibenden Bereiche der Etappe 2 entschieden (dienst-intern, `forTenant()` je Methode) — kein zukuenftiger Plan muss diese Frage erneut stellen.
- Der Schalter (`DATABASE_URL` → Rolle `tessera`, `BYPASSRLS`) ist unveraendert AUS. Etappe 4 (Scharfschalten) bleibt ein separater, spaeterer Schritt.
---
*Phase: quick-260911-e2s*
*Completed: 2026-09-11*
## Self-Check: PASSED
- FOUND: apps/api/scripts/rls-scratch-check.mjs
- FOUND: apps/api/src/tenant/tenant.guard.ts
- FOUND: apps/api/src/tenant/tenant.guard.spec.ts
- FOUND: apps/api/src/tenant/tenant.controller.ts
- FOUND: apps/api/src/tenant/tenant.controller.spec.ts
- FOUND: apps/api/src/prisma/rls-access-inventory.spec.ts
- FOUND: docs/mandantentrennung-etappe2-fehlerrichtung.md
- FOUND: docs/mandantentrennung-zugriffsklassifikation.md
- FOUND: docs/anleitung-entwicklung.md
- CONFIRMED DELETED: apps/api/src/tenant/tenant.middleware.ts
- FOUND COMMIT: 652e762 (Aufgabe 1)
- FOUND COMMIT: 11f5731 (Aufgabe 2, teilweise)
- FOUND COMMIT: 17dca0d (Aufgabe 2, Nachtrag)
- FOUND COMMIT: c8de72e (Aufgabe 3)
- `git diff --name-only f1017fa` listet genau die 13 erwarteten Dateien, keine unerwarteten
- Endlauf `rls-scratch-check.mjs`: 110/110 bestanden, Rückgabewert 0
- Endlauf `npm --prefix apps/api run test`: 911/911 gruen in 59 Dateien
- Endlauf `npm --prefix apps/api run type-check`: sauber
- `git status --short`: sauber (working tree clean) vor SUMMARY-Erstellung