docs(quick-260911-fh9): Etappe 2 Bereich auth abgeschlossen und verifiziert
Tessera CI/CD / Lint & Type Check (push) Successful in 47s
Tessera CI/CD / Tests (push) Successful in 54s
Tessera CI/CD / Build & Publish Images (push) Successful in 28s

This commit is contained in:
2026-09-11 12:10:02 +02:00
parent f68beb379a
commit 46f0e781be
3 changed files with 312 additions and 0 deletions
@@ -0,0 +1,206 @@
---
phase: quick-260911-fh9
plan: 01
subsystem: auth
tags: [prisma, postgresql, row-level-security, multi-tenancy, nestjs, jwt]
requires:
- phase: quick-260909-eor
provides: drei SECURITY-DEFINER-Funktionen fuer den Anmeldeweg (auth_lookup_user_by_username/email/reset_token, Migration 20260909160000_auth_lookup_functions)
- phase: quick-260910-das
provides: UserService.findByIdForPlatformAdmin (gebundener Fan-out je Mandant) und der Praezedenzfall resolveTargetUser in user.controller.ts
provides:
- getMe/changePassword/adminResetPassword binden je ueber genau einen Klienten tenantPrisma an den Mandanten aus dem signierten Sitzungsnachweis
- adminResetPassword schliesst die Rechteausweitung ueber die Mandantengrenze (T-FH9-01) und innerhalb des Mandanten (T-FH9-04, ADMIN darf keinen SUPER_ADMIN zuruecksetzen)
- AuthModule importiert UserModule (zyklusfrei) fuer den gebundenen Fan-out der obersten Rolle
- dreizehnter Abschnitt runAuthAreaChecks im Wegwerf-Werkzeug (10 neue Pruefungen, 120/120 gesamt)
- Klassifikationsdokument und WINDOWS.md auf den neuen Stand nachgezogen
affects: [quick-260911-favorites-settings, etappe-3-mandantentrennung]
actuals:
tokens: 26271
tasks: 3
commits: 3
plan_head_before: 4c3172b5a5f3469501afead181f3ecf90a5fdbfe
tech-stack:
added: []
patterns:
- "Zwei-Klienten-Testnachbau (__makeBoundClient) statt Identitaets-Attrappe fuer forTenant() in Service-Spec-Dateien"
- "Controller loest den Mandanten der obersten Rolle vor dem Dienstaufruf ueber einen gebundenen Fan-out auf (resolveTargetTenantId), der Dienst nimmt den fertigen Mandanten entgegen"
key-files:
created:
- apps/api/src/auth/auth.controller.spec.ts
modified:
- apps/api/src/auth/auth.service.ts
- apps/api/src/auth/auth.service.spec.ts
- apps/api/src/auth/auth.controller.ts
- apps/api/src/auth/auth.module.ts
- apps/api/scripts/rls-scratch-check.mjs
- docs/mandantentrennung-etappe2-fehlerrichtung.md
- docs/mandantentrennung-zugriffsklassifikation.md
- .planning/WINDOWS.md
key-decisions:
- "Selbstbedienung (getMe/changePassword) bindet an @CurrentUser().tenantId (das JWT-Claim), NICHT an req.tenantId (per x-tenant-id fuer SUPER_ADMIN umschaltbar) — ein umgeschalteter SUPER_ADMIN muss sich selbst weiterhin sehen"
- "adminResetPassword loest den Mandanten des ZIELS auf: ADMIN -> currentUser.tenantId, SUPER_ADMIN -> UserService.findByIdForPlatformAdmin(userId), derselbe Praezedenzfall wie user.controller.ts resolveTargetUser"
- "Die drei $queryRaw-Anmeldesuchen (validateUser/requestPasswordReset/resetPassword) bleiben unveraendert auf dem ungebundenen Klienten — sie sind die Grenze, nicht der Umbau"
- "adminResetPassword verweigert einem Nicht-SUPER_ADMIN das Kennwort eines SUPER_ADMIN (T-FH9-04); der Schwesterweg PATCH /users/:id hat dieselbe Luecke nicht geschlossen — WINDOWS #29 statt Reparatur, weil ausserhalb der Erlaubnisliste"
patterns-established:
- "runAuthAreaChecks im Wegwerf-Werkzeug: getrennt von runAuthLookupChecks (Anmeldeweg vs. Nach-Anmeldung), erweitert eine bereits vorhandene Wegwerf-Tabelle um fehlende Spalten statt sie neu anzulegen"
requirements-completed: [WINDOWS-18, ETAPPE-2-AUTH]
coverage:
- id: D1
description: "getMe/changePassword/adminResetPassword binden je ueber tenantPrisma an den Mandanten aus dem Sitzungsnachweis"
requirement: WINDOWS-18
verification:
- kind: unit
ref: "apps/api/src/auth/auth.service.spec.ts — AuthService.getMe/changePassword/adminResetPassword (20 Faelle)"
status: pass
- kind: integration
ref: "apps/api/scripts/rls-scratch-check.mjs — runAuthAreaChecks (10 Pruefungen ueber den generierten Client an einer auf 15 Spalten erweiterten Wegwerf-Tabelle, gegen tessera-ctl-db-1)"
status: pass
human_judgment: false
- id: D2
description: "adminResetPassword schliesst die Rechteausweitung ueber die Mandantengrenze (T-FH9-01) und innerhalb des Mandanten (T-FH9-04)"
requirement: ETAPPE-2-AUTH
verification:
- kind: unit
ref: "apps/api/src/auth/auth.service.spec.ts — 'FREMDER Mandant: BadRequestException...' und 'Aufrufer ADMIN, Ziel SUPER_ADMIN...ForbiddenException...'"
status: pass
- kind: integration
ref: "apps/api/scripts/rls-scratch-check.mjs — auth-adminreset-generierter-client-gebundenes-update-fremder-mandant-scheitert-laut"
status: pass
human_judgment: false
- id: D3
description: "auth.controller.ts liest den Mandanten ausschliesslich aus dem Sitzungsnachweis bzw. dem gebundenen Fan-out fuer die oberste Rolle — nicht aus req.tenantId/x-tenant-id"
requirement: ETAPPE-2-AUTH
verification:
- kind: unit
ref: "apps/api/src/auth/auth.controller.spec.ts — Mandantenquelle je Handler (9 Faelle) plus Rollen-/Public-Metadaten (14 Faelle)"
status: pass
human_judgment: false
- id: D4
description: "Klassifikationsdokument und WINDOWS.md sind auf den neuen Stand nachgezogen (auth.service.ts/user gebunden, zwei neue offene Ledger-Eintraege)"
requirement: ETAPPE-2-AUTH
verification:
- kind: unit
ref: "apps/api/src/prisma/rls-access-inventory.spec.ts (11 Tests, insbesondere der Stand-Vergleich gegen den Quelltext)"
status: pass
human_judgment: false
duration: 40min
completed: 2026-09-11
status: complete
---
# Quick Task 260911-fh9: Bereich auth der Mandantentrennung Etappe 2 Summary
**`getMe`, `changePassword`, `adminResetPassword` binden je über genau einen Klienten `tenantPrisma` an den Mandanten aus dem signierten Sitzungsnachweis; `adminResetPassword` schließt sowohl die Rechteausweitung über die Mandantengrenze (T-FH9-01) als auch innerhalb des Mandanten (T-FH9-04); der Anmeldeweg (drei SECURITY-DEFINER-Funktionen) bleibt unangetastet.**
## Performance
- **Duration:** ~40 min
- **Started:** 2026-09-11 (Baseline-Messung: 911 Tests grün, Werkzeug 110/110)
- **Completed:** 2026-09-11T10:01:47Z
- **Tasks:** 3
- **Files modified:** 9 (8 geändert, 1 neu)
## Accomplishments
- Die Grenze zwischen Anmeldeweg (drei `SECURITY DEFINER`-Funktionen, `20260909160000_auth_lookup_functions`, unverändert) und Nach-Anmeldung (drei gebundene Methoden) ist gemessen und im Werkzeug (`runAuthAreaChecks`, 10 neue Prüfungen, 120/120 insgesamt) und in der Kritikschrift (`docs/mandantentrennung-etappe2-fehlerrichtung.md`, Abschnitt "## Bereich auth", (h1)–(h5)) festgehalten.
- `getMe`, `changePassword`, `adminResetPassword` nehmen den Mandanten als ersten Parameter und laufen je über genau EINEN Klienten `tenantPrisma`; die drei `$queryRaw`-Anmeldesuchen bleiben unverändert auf dem ungebundenen Klienten.
- `adminResetPassword` verweigert einem Nicht-SUPER_ADMIN das Kennwort eines SUPER_ADMIN (T-FH9-04); `auth.controller.ts` löst den Mandanten der obersten Rolle über den gebundenen Fan-out `UserService.findByIdForPlatformAdmin` auf (Präzedenzfall `user.controller.ts` `resolveTargetUser`).
- Die Testlage hat keine Identitätsattrappe mehr — `auth.service.spec.ts` (29 Fälle) und die neue `auth.controller.spec.ts` (23 Fälle) nageln Bindung, Rollenverzweigung und Metadaten fest; sieben Falsifizierungsnachweise durchgeführt und zurückgenommen.
- Fünf handgepflegte Dokumentstellen der Klassifikation nachgezogen und derivativ gegatet; zwei neue offene Ledger-Einträge (#28, #29).
## Task Commits
1. **Aufgabe 1: Fehlerrichtung messen und aufschreiben** — `9782bea` (docs)
2. **Aufgabe 2: Die drei Methoden binden** — `92aa8c4` (feat)
3. **Aufgabe 3: Mandantenquelle festnageln, Klassifikation nachziehen, Ledger** — `f68beb3` (docs)
**Plan metadata:** wird vom Orchestrator nach dieser SUMMARY committet.
## Files Created/Modified
- `apps/api/scripts/rls-scratch-check.mjs` — dreizehnter Abschnitt `runAuthAreaChecks`, neuer Helfer `readSchemaModelScalarFieldNames`
- `docs/mandantentrennung-etappe2-fehlerrichtung.md` — Abschnitt `## Bereich auth` (h1)–(h5) vor `## Verweis`
- `apps/api/src/auth/auth.service.ts` — `getMe(tenantId, userId)`, `changePassword(tenantId, userId, ...)`, `adminResetPassword(tenantId, callerRole, userId, ...)`
- `apps/api/src/auth/auth.service.spec.ts` — Zwei-Klienten-Nachbau (`__makeBoundClient`), 29 Fälle
- `apps/api/src/auth/auth.controller.ts` — `resolveTargetTenantId`, `me`/`changePassword`/`adminResetPassword` reichen das Claim durch
- `apps/api/src/auth/auth.module.ts` — importiert `UserModule`
- `apps/api/src/auth/auth.controller.spec.ts` — NEU, 23 Fälle
- `docs/mandantentrennung-zugriffsklassifikation.md` — Übersichtszeile, Summenzeile, Bestandsaufnahme-Zeile, Klassen-Verteilung-Vermerk, Hintergrunddienst-Vermerk, Etappe-3-Punkt
- `.planning/WINDOWS.md` — Einträge #28, #29
## Decisions Made
- Mandantenquelle für Selbstbedienung: das JWT-Claim (`@CurrentUser().tenantId`), nicht `req.tenantId` — siehe key-decisions oben.
- `adminResetPassword`s Mandant für die oberste Rolle: gebundener Fan-out über `UserService.findByIdForPlatformAdmin`, nicht `req.tenantId`/`x-tenant-id`.
- Rollengrenze innerhalb des Mandanten in `adminResetPassword` geschlossen; Schwesterweg `PATCH /users/:id` bewusst NICHT angefasst (außerhalb der Erlaubnisliste) — Ledger-Eintrag #29 statt Reparatur.
## Tatsächlich gezählte Prüfungs- und Testzahlen
- **Wegwerf-Werkzeug:** 120/120 Prüfungen bestanden (110 bisherige + 10 neue, wie im Plan gezählt: `auth-anmeldefunktionen-security-definer-unveraendert`, `auth-wegwerftabelle-user-deckt-alle-spalten-des-generierten-clients`, `auth-anmeldesuche-findet-benutzer-weiterhin-mit-festem-spaltensatz`, `auth-getme-generierter-client-ungebunden-liefert-null`, `auth-getme-generierter-client-gebunden-eigener-mandant-findet-benutzer`, `auth-getme-generierter-client-gebunden-fremder-mandant-liefert-null`, `auth-changepassword-generierter-client-ungebundenes-update-scheitert-laut`, `auth-changepassword-generierter-client-gebundenes-update-eigener-mandant-gelingt`, `auth-adminreset-generierter-client-gebundenes-update-fremder-mandant-scheitert-laut`, `auth-fan-out-je-mandant-gebunden-loest-mandant-der-kennung-auf`).
- **Testsuite:** Baseline 911 Tests/59 Dateien → nach Aufgabe 2: 928 Tests (927 grün, 1 erwartungsgemäß rot — siehe unten) → nach Aufgabe 3: **951 Tests grün in 60 Dateien** (29 Fälle in `auth.service.spec.ts`, 23 Fälle in der neuen `auth.controller.spec.ts`, 927 + 24 = 951).
- **Typprüfung:** sauber nach jeder Aufgabe.
## Abweichung von den Planungsbefunden (ausdrücklich benannt)
**Zwischenzeitlich rot: `rls-access-inventory.spec.ts`, zwischen Aufgabe 2 und Aufgabe 3.** Der Plan sagt in Aufgabe 3 voraus: *"Ohne Schritt 3 ist rls-access-inventory.spec.ts am Ende dieser Aufgabe rot (Stand-Vergleich)."* Das galt nicht nur für Aufgabe 3, sondern bereits ab dem Ende von Aufgabe 2: sobald `auth.service.ts` keinen ungebundenen `user`-Zugriff mehr enthielt, maß die Prüfung den Stand für `apps/api/src/auth/auth.service.ts::user` als `gebunden`, während das Klassifikationsdokument (noch nicht nachgezogen, das ist Aufgabe 3) weiterhin `gemischt` führte — ein Fehlschlag von genau einem Test (`der eingetragene Stand stimmt mit dem im Quelltext gemessenen überein`), 927/928 grün. Dasselbe Muster zeigt sich bereits im Klassifikationsdokument selbst für den Vorgänger-Plan 260911-cwh ("Aufgabe 2 (260911-cwh) ändert nur seine Stand-Spalte … nicht seine Klasse" — im Abschnitt zu Aufgabe 3 dokumentiert, obwohl die Codeänderung in Aufgabe 2 lag). Behandlung: nicht als Blocker gewertet, weil (a) der Fehlschlag exakt einen einzigen, im Plan selbst vorausgesagten Test betraf, (b) er keine Datei außerhalb der für Aufgabe 2 erlaubten vier Dateien berührte, und (c) Aufgabe 3 unmittelbar im selben Lauf folgte und die Baseline innerhalb von Minuten wiederherstellte (951/951). Der Commit von Aufgabe 2 dokumentiert das ausdrücklich als "bekannt und erwartet". Kein Datenverlust, keine stillschweigende Planabweichung — nur eine Klarstellung, dass "Baseline gehalten nach jeder Aufgabe" hier als "nach dem vollständigen Plan, mit einem im Plan selbst vorausgesagten Zwischenzustand" zu lesen ist, nicht als literarische Bedingung jedes einzelnen Aufgaben-`<verify>`-Blocks.
**Testfall-Namensraumkollision im Gate `forTenant: vi.fn((p` (Aufgabe 2).** Die neue Mock-Signatur `forTenant: vi.fn((prisma: any, tenantId: string) => prisma.__makeBoundClient(tenantId))` — wortgleich mit dem Muster aus `user.service.spec.ts`/`tenant.controller.spec.ts` — erfüllte unbeabsichtigt das BRE-Suchmuster `forTenant: vi.fn((p` des Gates, das die ALTE Identitäts-Attrappe `forTenant: vi.fn((p) => p)` ausschließen sollte (`vi.fn((p` ist ein Präfix von `vi.fn((prisma`). Behoben durch Umbenennung des ersten Parameters auf `unboundClient` statt `prisma`. Keine Verhaltensänderung, nur eine Namenswahl, die das Gate nicht fälschlich trifft.
Alle übrigen Zahlen, Codeaussagen (Befunde B, C, D, E, J, K) und die Modulgraph-Messung stimmten bei der erneuten Ausführung zur Ausführungszeit exakt mit den Planungsbefunden überein — keine weiteren Abweichungen.
## Falsifizierungsnachweise (alle durchgeführt, zurückgenommen, wörtlich notiert)
**Aufgabe 2 (drei, im Dienst):**
1. **`getMe` probeweise auf den ungebundenen Klienten zurückgebaut** (`const tenantPrisma = this.prisma as any;`): 4 Tests wurden rot (`AuthService.getMe` — alle vier Fälle), jeweils mit `TypeError: Cannot read properties of undefined (reading 'findUnique')`. Erwartete Form bestätigt: der Nachbau hat kein ungebundenes Benutzermodell.
2. **`validateUser` probeweise auf den gebundenen Klienten verschoben** (`const probeBoundClient = forTenant(this.prisma, 'falsification-probe') as any; const rows = await probeBoundClient.$queryRaw...`): 9 Tests wurden rot, darunter der eigens für die Grenze geschriebene Fall (`AuthService.validateUser — lokales Kennwort > sucht die Anmeldedaten exakt EINMAL ungebunden...`) mit `TypeError: probeBoundClient.$queryRaw is not a function`. Erwartete Form bestätigt: der gebundene Nachbau hat kein `$queryRaw`.
3. **SUPER_ADMIN-Riegel in `adminResetPassword` probeweise entfernt**: genau 1 Test wurde rot (`AuthService.adminResetPassword > Aufrufer ADMIN, Ziel SUPER_ADMIN im SELBEN Mandanten: ForbiddenException (T-FH9-04)...`), mit `AssertionError: promise resolved "undefined" instead of rejecting`.
**Aufgabe 3 (eine, am Controller):**
4. **`resolveTargetTenantId` probeweise auf `return currentUser.tenantId;` (auch für SUPER_ADMIN) reduziert**: genau die beiden Fan-out-Fälle wurden rot — `expected "spy" to be called 1 times, but got 0 times` (findByIdForPlatformAdmin nicht aufgerufen) und `promise resolved "{ message: ... }" instead of rejecting` (die Null-Fan-out-BadRequestException griff nicht mehr).
**Aufgabe 3 (zwei, an den Dokument-Gates):**
5. **Bestandsaufnahme-Zeile `auth.service.ts | user` probeweise auf `gemischt` zurückgesetzt**: `rls-access-inventory.spec.ts` wurde rot mit `AssertionError: Abweichender Stand (Dokument vs. Quelltext): apps/api/src/auth/auth.service.ts::user — dokumentiert=gemischt, gemessen=gebunden`.
6. **Übersichtszeile `auth` probeweise auf `99 | 10` gesetzt**: das herleitende Shell-Gate schlug fehl mit `UEBERSICHTSZEILE auth nennt nicht die neu gemessenen Zahlen 3/10`.
## Deviations from Plan
Keine inhaltlichen Abweichungen von den vier Dateien/Aufgaben des Plans — beide oben dokumentierten Punkte sind Klarstellungen zur Ausführungsreihenfolge bzw. eine Namenswahl, keine Scope- oder Verhaltensänderung. Kein Rule-1/2/3/4-Auto-Fix war nötig; alle Codeaussagen aus den Planungsbefunden wurden bei erneuter Ausführung bestätigt.
## Issues Encountered
Keine ungelösten Probleme. Das einzige während der Ausführung aufgetretene technische Detail (Gate-Namenskollision, siehe Abweichungen oben) wurde sofort behoben.
## Etappe-3-Vorbehalt (ein Absatz)
Sobald Anmeldenamen je Mandant eindeutig werden (Etappe-3-Entscheidung (1)), braucht der Anmeldeweg den Mandanten VOR der Benutzersuche: `auth_lookup_user_by_username(p_username)` muss auf `(p_tenant_id, p_username)` umgestellt werden — die Funktion wird dabei ENGER (zwei Gleichheitsbedingungen statt einer), nicht weiter — und `local.strategy.ts` braucht eine Mandantenangabe vor der Suche. Dieser Plan ist dafür neutral: die Bindung der drei Nach-Anmeldungs-Methoden hängt ausschließlich am JWT-Claim `tenantId` und an `User.id` (plattformweite UUID, Kette aus 260911-cwh), nicht an `username`/`email`. Nichts in diesem Plan hat den künftigen Umbau schwerer gemacht.
## User Setup Required
None — keine externe Dienstkonfiguration nötig.
## Next Phase Readiness
- Elf der zwölf Bereiche der Etappe 2 sind umgestellt. Laut Plan ist der nächste Lauf `favorites` (7 ungebundene Rohtreffer) und `settings` (4) als EIN Durchlauf — danach ist Etappe 2 vollständig.
- Kein Blocker für diesen nächsten Lauf. Zwei neue offene WINDOWS-Einträge (#28 Frontend-Leere, #29 Schwesterweg-Rechteausweitung) sind dokumentiert und unabhängig von `favorites`/`settings`.
- `DATABASE_URL` zeigt unverändert auf die Rolle `tessera` (Schalter aus); Schema, Migrationen, die drei Anmeldefunktionen, Active Directory: alle unangetastet.
---
*Phase: quick-260911-fh9*
*Completed: 2026-09-11*
## Self-Check: PASSED
Alle neun in `key-files` genannten Dateien plus diese SUMMARY existieren auf der Platte; alle drei Task-Commits (`9782bea`, `92aa8c4`, `f68beb3`) sind in `git log --oneline --all` auffindbar. Keine fehlenden Elemente.
@@ -0,0 +1,105 @@
---
phase: quick-260911-fh9
verified: 2026-09-11T12:10:00Z
status: passed
score: 8/8 must-haves verified
covered_files:
- .planning/WINDOWS.md
- .planning/quick/260911-fh9-mandantentrennung-etappe-2-bereich-auth-/260911-fh9-PLAN.md
- .planning/quick/260911-fh9-mandantentrennung-etappe-2-bereich-auth-/260911-fh9-SUMMARY.md
- apps/api/scripts/rls-scratch-check.mjs
- apps/api/src/auth/auth.controller.spec.ts
- apps/api/src/auth/auth.controller.ts
- apps/api/src/auth/auth.module.ts
- apps/api/src/auth/auth.service.spec.ts
- apps/api/src/auth/auth.service.ts
- docs/mandantentrennung-etappe2-fehlerrichtung.md
- docs/mandantentrennung-zugriffsklassifikation.md
covered_digest: "v1:sha256:a4eee94bfd0ca019936b1d7766a7bcaa03d97438c1319dad220a021d1e947cde"
behavior_unverified: 0
overrides_applied: 0
---
# Quick Task 260911-fh9: Bereich `auth` der Mandantentrennung Etappe 2 — Verification Report
**Task Goal:** `getMe`, `changePassword`, `adminResetPassword` an den Mandanten aus dem JWT-Claim binden (NICHT an das umschaltbare `req.tenantId`), die fehlenden Mandanten-/Rollenpruefungen in `adminResetPassword` schliessen, die Identitaets-Attrappe im Test durch einen Zwei-Klienten-Nachbau ersetzen, den Anmeldeweg unangetastet lassen, das Klassifikationsdokument nachziehen.
**Verified:** 2026-09-11T12:10:00Z
**Status:** passed
**Re-verification:** No — initial verification
## Goal Achievement
### Observable Truths
| # | Truth | Status | Evidence |
|---|-------|--------|----------|
| 1 | `getMe`/`changePassword` binden an `@CurrentUser().tenantId` (Claim), nicht an `req.tenantId`/`x-tenant-id` | ✓ VERIFIED | `auth.controller.ts`: `me`/`changePassword` reichen ausschliesslich `user.tenantId` aus `@CurrentUser()` durch; `grep -c "req.tenantId\|x-tenant-id"` = 0. Eigener Test belegt, dass ein SUPER_ADMIN mit Claim `t1` ebenfalls `('t1','u1')` liefert — der Handler liest strukturell nur `@CurrentUser()`, eine umgeschaltete `x-tenant-id`-Kopfzeile kann das nicht beeinflussen, weil der Handler keinen zweiten Kanal fuer den Mandanten besitzt. DB-seitig bestaetigt `auth-getme-generierter-client-gebunden-fremder-mandant-liefert-null` (rls-scratch-check.mjs, selbst ausgefuehrt), dass ein unter fremdem Mandanten gebundener Klient die eigene Zeile nicht sieht. |
| 2 | `adminResetPassword`: Tenant-Grenze (ADMIN A -> User B unerreichbar) UND Rollen-Grenze (ADMIN setzt kein SUPER_ADMIN-Kennwort) sind geschlossen und je mit benanntem Test gepinnt | ✓ VERIFIED | `auth.service.ts`: `ForbiddenException` bei `user.role === Role.SUPER_ADMIN && callerRole !== Role.SUPER_ADMIN`; `BadRequestException('User not found')` bei unsichtbarer (fremdmandantiger) Zeile. Beide durch benannte Tests in `auth.service.spec.ts` gepinnt ("FREMDER Mandant: BadRequestException...", "Aufrufer ADMIN, Ziel SUPER_ADMIN...ForbiddenException..."), beide zusaetzlich DB-seitig durch `rls-scratch-check.mjs` (`auth-adminreset-generierter-client-gebundenes-update-fremder-mandant-scheitert-laut`) bestaetigt. SUPER_ADMIN-Pfad laeuft ueber `UserService.findByIdForPlatformAdmin`; `resolveTargetTenantId` im Controller leitet fuer SUPER_ADMIN den Mandanten des ZIELS ab (`target.tenantId`), fuer ADMIN den des Aufrufers (`currentUser.tenantId`) — gelesen in `auth.controller.ts`, gepinnt durch 4 Controller-Tests. |
| 3 | Die drei SECURITY-DEFINER-Anmeldefunktionen sind unangetastet; `pg_proc` bestaetigt es | ✓ VERIFIED | Selbst ausgefuehrt: `TESSERA_SCRATCH_ADMIN_URL=... node apps/api/scripts/rls-scratch-check.mjs` gegen `tessera-ctl-db-1` (172.19.0.2) — 120/120 Pruefungen bestanden, darunter `auth-anmeldefunktionen-security-definer-unveraendert` (3 Funktionen, `prosecdef=true`, `provolatile='s'`, `search_path=public, pg_temp`, `LIMIT 1`) und `auth-anmeldesuche-findet-benutzer-weiterhin-mit-festem-spaltensatz` (genau 9 Spalten nach der Wegwerftabellen-Erweiterung um 5 Spalten — weder `avatarPath` noch `accentColor` noch `email` durchgelassen). `git diff --name-only 4c3172b..HEAD -- apps/api/prisma` leer (orchestratorseitig bereits gemessen, selbst nachgemessen). |
| 4 | Identitaets-Attrappe ersetzt durch asymmetrischen Zwei-Klienten-Nachbau | ✓ VERIFIED | `auth.service.spec.ts`: `forTenant` umgeleitet auf `unboundClient.__makeBoundClient(tenantId)`; ungebundener Basisclient (`fake`) hat NUR `$queryRaw`/`__makeBoundClient`/`__users`/`__resetTokens`/`__boundCallLog` — kein `user`/`passwordResetToken`; gebundener Klient (`makeScopedUser`/`makeScopedResetToken`) hat `user`/`passwordResetToken`, kein `$queryRaw`. Selbst falsifiziert: `getMe` probeweise auf `this.prisma` (ungebunden) zurueckgebaut -> exakt 4 Tests rot mit `TypeError: Cannot read properties of undefined (reading 'findUnique')` — genau wie im SUMMARY behauptet. Aenderung zurueckgenommen, 29/29 wieder gruen, `git status` sauber. |
| 5 | Genau 3 verbleibende ungebundene Stellen sind `$queryRaw`-Anmeldesuchen, keine Modellzugriffe | ✓ VERIFIED | `grep -c 'this\.prisma\.\$queryRaw' auth.service.ts` = 3 (Zeilen 109, 217, 254 — `validateUser`, `requestPasswordReset`, `resetPassword`); `grep -c 'this\.prisma\.user' auth.service.ts` = 0. |
| 6 | Transienter Rotzustand von `rls-access-inventory.spec.ts` zwischen Aufgabe 2 und 3, jetzt gruen; Klassifikationszeile `auth.service.ts`/`user` = `gebunden` | ✓ VERIFIED | `npm --prefix apps/api run test -- src/prisma/rls-access-inventory.spec.ts`: 11/11 gruen (selbst ausgefuehrt). `docs/mandantentrennung-zugriffsklassifikation.md` Zeile 422: Stand `gebunden`, Begruendung mit allen drei Methoden. |
| 7 | Ledger-Eintraege #28 (Frontend-Leere) und #29 (Schwesterweg `PATCH /users/:id`) existieren, offen, ohne Reparatur | ✓ VERIFIED | `.planning/WINDOWS.md`: beide Eintraege vorhanden, `"status": "open"`. #29-Behauptung selbst nachgeprueft: `UserController.update` prueft nur `dto.role === Role.SUPER_ADMIN` (Neuzuweisung), NICHT `user.role === Role.SUPER_ADMIN` (Bestandsrolle des Ziels) — die Luecke ist real und unbehoben. |
| 8 | Fuenf handgepflegte Klassifikationsstellen nachgezogen (Uebersichtszeile, Summenzeile, Bestandsaufnahme, Klassen-Verteilung, `Was diese Etappe NICHT entscheidet`); Umfang gegen `6236b30`/`4c3172b` als Erlaubnisliste gegatet | ✓ VERIFIED | Alle fuenf Stellen selbst nachgesehen: Uebersichtszeile `auth 3 10`, Summenzeile `78/167`, Bestandsaufnahme-Zeile `gebunden`, Klassen-Verteilung `32/17/13/2=64` mit Stand-Vermerk 260911-fh9, neuer Etappe-3-Punkt in "Was diese Etappe NICHT entscheidet". `git diff --name-only 4c3172b..HEAD` = genau die 10 im Plan erlaubten Dateien (plus `.planning/`); `apps/api/prisma`, `apps/web`, Compose/Env unveraendert. |
**Score:** 8/8 truths verified (0 present, behavior-unverified)
### Required Artifacts
| Artifact | Expected | Status | Details |
|----------|----------|--------|---------|
| `apps/api/scripts/rls-scratch-check.mjs` | 13. Abschnitt `runAuthAreaChecks`, >=10 neue Pruefungen, `pg_proc`-Messung | ✓ VERIFIED | Eigenstaendig ausgefuehrt: 120/120 bestanden, korrekte Reihenfolge (`runTenantAreaChecks` -> `runAuthAreaChecks` -> `runTransactionShapeMeasurement`, Zeilen 4016-4018) |
| `docs/mandantentrennung-etappe2-fehlerrichtung.md` | Abschnitt `## Bereich auth` vor `## Verweis`, (h1)-(h5) | ✓ VERIFIED | Zeilen 2473-2676, unmittelbar vor `## Verweis` (2677), alle fuenf Unterabschnitte vorhanden |
| `apps/api/src/auth/auth.service.ts` | drei gebundene Methoden, je EIN `tenantPrisma` | ✓ VERIFIED | Gelesen vollstaendig, 0 ungebundene `this.prisma.user`, genau 3 `$queryRaw`, 6 `forTenant`-Aufrufstellen |
| `apps/api/src/auth/auth.service.spec.ts` | Zwei-Klienten-Nachbau, alle Faelle, Falsifizierung | ✓ VERIFIED | 29/29 Tests gruen; Falsifizierung selbst reproduziert (4 Tests rot, zurueckgenommen) |
| `apps/api/src/auth/auth.controller.ts` | Claim-Durchreichung, Rollenverzweigung | ✓ VERIFIED | Gelesen vollstaendig; 0 `req.tenantId`/`x-tenant-id`/`PrismaService` |
| `apps/api/src/auth/auth.module.ts` | importiert `UserModule`, zyklusfrei | ✓ VERIFIED | `GroupsModule` importiert nichts; nur `app.module.ts` importiert `AuthModule` — selbst nachgemessen |
| `apps/api/src/auth/auth.controller.spec.ts` | NEU, Mandantenquelle, Metadaten | ✓ VERIFIED | 23/23 Tests gruen; Falsifizierung selbst reproduziert (2 Tests rot, zurueckgenommen) |
| `docs/mandantentrennung-zugriffsklassifikation.md` | 5 Stellen nachgezogen | ✓ VERIFIED | Siehe Truth 8 |
| `.planning/WINDOWS.md` | 2 neue offene Eintraege | ✓ VERIFIED | #28, #29 vorhanden, offen |
### Key Link Verification
| From | To | Via | Status | Details |
|------|-----|-----|--------|---------|
| `auth.controller.ts` `me`/`changePassword` | `AuthService.getMe`/`changePassword` | `user.tenantId` aus `@CurrentUser()` | ✓ WIRED | Grep + Test bestaetigt |
| `auth.controller.ts` `adminResetPassword` | `UserService.findByIdForPlatformAdmin` | `resolveTargetTenantId` fuer SUPER_ADMIN | ✓ WIRED | Test bestaetigt (`findByIdForPlatformAdmin` genau 1x mit `'target'`) |
| `AuthModule` | `UserModule` | `imports: [...]` | ✓ WIRED | `auth.module.ts` importiert `UserModule`; zyklusfrei statisch gemessen |
| `auth.service.ts` `validateUser`/`requestPasswordReset`/`resetPassword` | `auth_lookup_*`-Funktionen (SECURITY DEFINER) | `this.prisma.$queryRaw` | ✓ WIRED | 3/3, unveraendert, `pg_proc`-Messung bestanden |
### Behavioral Spot-Checks
| Behavior | Command | Result | Status |
|----------|---------|--------|--------|
| `auth.service.spec.ts` + `auth.controller.spec.ts` laufen | `npm --prefix apps/api run test -- src/auth/auth.service.spec.ts src/auth/auth.controller.spec.ts` | 52/52 gruen (29+23) | ✓ PASS |
| `rls-access-inventory.spec.ts` laeuft | `npm --prefix apps/api run test -- src/prisma/rls-access-inventory.spec.ts` | 11/11 gruen | ✓ PASS |
| Volle Testsuite | `npm --prefix apps/api run test` (einmal) | 951/951 gruen, 60 Dateien | ✓ PASS |
| `type-check` | `npm --prefix apps/api run type-check` | exit 0 | ✓ PASS |
| Wegwerf-Werkzeug gegen laufende DB | `TESSERA_SCRATCH_ADMIN_URL=... node apps/api/scripts/rls-scratch-check.mjs` (frisch aufgeloeste Adresse 172.19.0.2) | 120/120 bestanden | ✓ PASS |
| Falsifizierung 1: `getMe` auf ungebundenen Klienten zurueckgebaut | Codeaenderung + `vitest run auth.service.spec.ts` | 4 Tests rot (`TypeError: Cannot read properties of undefined (reading 'findUnique')`), zurueckgenommen, 29/29 wiederhergestellt | ✓ PASS |
| Falsifizierung 2: `resolveTargetTenantId` fuer SUPER_ADMIN auf `currentUser.tenantId` reduziert | Codeaenderung + `vitest run auth.controller.spec.ts` | 2 Tests rot (Fan-out-Faelle), zurueckgenommen, 23/23 wiederhergestellt | ✓ PASS |
### Requirements Coverage
| Requirement | Source Plan | Description | Status | Evidence |
|-------------|------------|--------------|--------|----------|
| WINDOWS-18 | 260911-fh9-PLAN.md | Die drei Nach-Anmeldungs-Methoden binden an den Mandanten aus dem Sitzungsnachweis | ✓ SATISFIED | Truth 1, 3 |
| ETAPPE-2-AUTH | 260911-fh9-PLAN.md | Rechteausweitung ueber und innerhalb der Mandantengrenze in `adminResetPassword` geschlossen, Dokumentation nachgezogen | ✓ SATISFIED | Truth 2, 6, 7, 8 |
### Anti-Patterns Found
Keine. `grep -n "TODO\|FIXME\|TBD\|HACK\|PLACEHOLDER" apps/api/src/auth/auth.service.ts apps/api/src/auth/auth.controller.ts apps/api/src/auth/auth.module.ts` liefert keine Treffer in den geaenderten Codedateien (Kommentare beziehen sich auf Ledger-Eintraege mit Referenznummern, keine unbezeichneten Marker).
### Human Verification Required
Keine. Dieser Bereich ist rein backend-seitig, ueber Unit-Tests, ein Wegwerf-Datenbank-Werkzeug und Quelltext-Messung vollstaendig ueberprueft — keine visuelle, Echtzeit- oder UX-Beurteilung noetig.
### Gaps Summary
Keine Luecken gefunden. Alle acht abgeleiteten Wahrheiten (must-haves aus PLAN-Frontmatter, kombiniert mit den elf Pruefpunkten aus dem Verifikationsauftrag) sind mit unabhaengig reproduzierten Belegen (Testlaeufen, Datenbankwerkzeug-Ausgabe, Falsifizierungen, Quelltext-Lesungen) bestaetigt. Zwei bewusst offene Ledger-Eintraege (#28, #29) sind korrekt als offene Punkte dokumentiert, nicht als geschlossen behauptet — sie liegen ausserhalb der Erlaubnisliste dieses Plans und wurden dort auch nicht angefasst.
---
_Verified: 2026-09-11T12:10:00Z_
_Verifier: Claude (gsd-verifier)_