Files
tessera-ctl/.planning/quick/260911-fh9-mandantentrennung-etappe-2-bereich-auth-/260911-fh9-VERIFICATION.md
T
schalli 46f0e781be
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
docs(quick-260911-fh9): Etappe 2 Bereich auth abgeschlossen und verifiziert
2026-09-11 12:10:02 +02:00

106 lines
12 KiB
Markdown

---
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)_