From 46f0e781be96d53a758b6f4615bd3262a8df5bc9 Mon Sep 17 00:00:00 2001 From: Schalli Date: Fri, 11 Sep 2026 12:10:02 +0200 Subject: [PATCH] docs(quick-260911-fh9): Etappe 2 Bereich auth abgeschlossen und verifiziert --- .planning/STATE.md | 1 + .../260911-fh9-SUMMARY.md | 206 ++++++++++++++++++ .../260911-fh9-VERIFICATION.md | 105 +++++++++ 3 files changed, 312 insertions(+) create mode 100644 .planning/quick/260911-fh9-mandantentrennung-etappe-2-bereich-auth-/260911-fh9-SUMMARY.md create mode 100644 .planning/quick/260911-fh9-mandantentrennung-etappe-2-bereich-auth-/260911-fh9-VERIFICATION.md diff --git a/.planning/STATE.md b/.planning/STATE.md index 5909b2d..549e34d 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -388,6 +388,7 @@ None yet. | 260910-krx | Mandantentrennung Etappe 2, Bereich dashboard — 12 von 13 Zugriffen gebunden, der Modulkatalog bleibt bewusst ungebunden (Messung und Bedingung getrennt: heute ohne Zeilenschutz, daher wirkungslos, katastrophal erst wenn Etappe 3 eine Regel setzt). **Erster Bereich, in dem die Besitzpruefungen von Anfang an richtig waren:** dieselbe Bauform, die in ldap und dkv je eine Luecke riss (nachschlagen, dann loeschen), vergleicht hier dazwischen gegen die angemeldete Person — nichts zu reparieren, nur zu bestaetigen und durch Tests festzunageln. **Beweisvernichtungs-Schleife belegt, nicht vermutet (WINDOWS #25, offen):** nach dem Scharfschalten liefert `getLayout` bei unsichtbarer Zeile die Vorgabe, die Oberflaeche uebernimmt sie ohne Fehlerzustand, und das Verlassen des Bearbeitungsmodus schreibt AUTOMATISCH zurueck — der Nutzer ueberschreibt seine urspruengliche Anordnung selbst, ohne es zu merken; dazu haeufen sich Widget-Dubletten, weil es keine Eindeutigkeit ueber (userId, widgetType) gibt. Gehoert in die Etappe-4-Vorabpruefung, nicht in diesen Umbau. Suchleiste: der Rueckfallzweig feuert nie leer, weil drei Vorgaben immer vorangestellt sind — die eigenen Suchmaschinen verschwinden schlicht. `DashboardLayout.userId` ist plattformweit eindeutig ohne Mandantenanteil (Familie WINDOWS #22). **Der Verifizierer fand eine Luecke der bekannten Art:** die Behauptung, ein gebundener Konfliktschreibvorgang werfe `PrismaClientUnknownRequestError` (nicht den P2002-Fall von tenders/user), stuetzte sich auf eine NICHT committete Ad-hoc-Messung — Pruefung 5 mass nur Roh-SQL, kein Test uebte den catch-Zweig. Nachgereicht (6e71206): Messung ueber den GENERIERTEN Client (Konstruktorname geprueft), dabei die Wegwerf-Tabelle korrigiert, der Roh-SQL nie aufgefallen war (createdAt/updatedAt fehlten, der echte Client scheiterte sofort mit P2022); zwei Tests fuer den catch-Zweig, durch Rueckbau falsifiziert. Klassifikation: fremde Datei `groups.service.ts` mit ungenauem Kopfkommentar bewusst NICHT angefasst, Ungenauigkeit in (w5) festgehalten. **Verifiziert 10/11, Luecke behoben** (860/860 Tests, Typpruefung sauber, 88/88 Live-Pruefungen; alle drei Falsifizierungsnachweise vom Pruefer eigenhaendig reproduziert) | 2026-09-11 | 6744918,e0ce594,67b5024,6e71206 | [260910-krx-mandantentrennung-etappe-2-bereich-dashb](./quick/260910-krx-mandantentrennung-etappe-2-bereich-dashb/) | | 260911-cwh | Mandantentrennung Etappe 2, Bereich calendar — alle 12 Zugriffe gebunden, ein Klient je Methode, sechs Methoden. Bereich hatte KEINE Testdatei (dkv-Form); `calendar.service.spec.ts` neu mit 23 Tests. **Gespeicherte Zugangsdaten zu fremden Kalender-Servern** — ein Fremdzugriff waere hier der Schluessel zu einem fremden Exchange/CalDAV. **Der dkv-Passwortverlust-Fall existiert hier NICHT**, in beiden Haelften belegt: der Dienst schreibt `encryptedPassword` nur bei `dto.password !== undefined`, und `calendar-source-form.tsx` laesst ein leeres Feld WEG statt einen leeren Text zu schicken; als drei Tests festgenagelt, weil ein nicht festgenagelter Freispruch still aufhoeren kann zu gelten. **Zwischenspeicher-Schluessel `userId:from:to` ohne Mandantenanteil ist sicher:** `User.id` ist `@default(uuid())`, Kette Schema -> `auth.service.ts sub: user.id` -> `JwtStrategy.validate` -> `extractContext` Glied fuer Glied belegt; die Etappe-3-Entscheidung (Anmeldenamen pro Mandant) beruehrt `username`/`email`, nicht `id`. **Eigener Gefahrenfall — halb gebundene Aggregationsschleife:** `fetchAndCacheEvents` liest Quellen und schreibt den Synchronstatus auf Erfolgs- UND Fehlerpfad zurueck, innerhalb von `Promise.allSettled`; gebundene Lesung mit ungebundenem Rueckschreiben haette Ereignisse still fallen lassen — beide Rueckschreibungen gebunden und als Tests festgenagelt, eines davon vom Pruefer eigenhaendig zurueckgebaut (genau 1 von 23 rot, exakte Meldung). **Frontend macht aus lauten Fehlern stille:** `calendar-widget.tsx` und `calendar-settings-panel.tsx` fangen jeden Fehler in denselben leeren Zustand — ein 403 sieht aus wie ein leerer Kalender; NICHT angefasst, als WINDOWS #26 offen festgehalten. Besitzpruefungen in allen drei Pfaden echt (403, nicht 404). Keine Eindeutigkeitskette, daher keine Konfliktuebersetzung, die nichts uebersetzt. **Lehre aus dashboard angewandt:** 4 der 13 neuen Pruefungen laufen ueber den GENERIERTEN Client, mit Laufzeitvergleich der Wegwerf-Tabelle gegen `schema.prisma` (17 = 17 Spalten). **Verifiziert 12/12** (883/883 Tests, 57 Dateien, Typpruefung sauber, 101/101 Live-Pruefungen; Klassenverteilung 31/17/13/2 = 63 und Ledger-Zaehler vom Pruefer nachgerechnet). Ein Selbstwiderspruch in der Zusammenfassung ('keine Abweichung' vs. 'kein TDD-Zyklus') berichtigt | 2026-09-11 | bf5fc4d,77cb124,e0e163e | [260911-cwh-mandantentrennung-etappe-2-bereich-calen](./quick/260911-cwh-mandantentrennung-etappe-2-bereich-calen/) | | 260911-e2s | Mandantentrennung Etappe 2, Bereich tenant — von anderer Art: alle 8 Zugriffe gehen auf die Mandantentabelle SELBST, die per Definition keinen Mandanten hat. **'Nichts zu binden' war trotzdem falsch, und der Grund ist der wichtigste Fund seit dem kaputten Helfer in Etappe 1:** drei der acht Zugriffe (`findAll`, `findOne`, `remove` im Controller) zaehlen ueber `include: { _count: { select: { users } } }` in die GESCHUETZTE Tabelle `User` hinein — Prisma 6.19 rendert das als `LEFT JOIN (SELECT tenantId, COUNT(*) FROM User ...)`, das unter DEREN Regel laeuft. Nach dem Scharfschalten haette die Mandantenliste des Plattform-Admins fuer jeden Mandanten 0 Benutzer gezeigt, und der Loeschriegel T-02-09 waere vakuum geworden (der Fremdschluessel faengt es noch, aber als 500 statt 400). Behoben per Fan-out je Mandant ueber gebundenen Client, Muster aus `UserService.findAllForPlatformAdmin`. **Die Bestandsaufnahme ist fuer Relationszugriffe strukturell blind** — sie sieht nur `this.prisma.`, nicht was ein `include:` in eine zweite Tabelle hineinrechnet. Alle 19 `include:`-Stellen und alle `_count`-Stellen einzeln beurteilt, vom Orchestrator UND vom Verifizierer unabhaengig gegengeprueft (der Plan-Pruefer hatte diesen Punkt als 'plausibel' durchgewinkt statt ihn zu pruefen): nur diese drei waren gefaehrlich. Der MECHANISMUS bleibt offen und ist als WINDOWS #27 festgehalten — der Planer wollte keinen Eintrag, weil die Instanz behoben ist; Orchestrator und Verifizierer sahen das anders, weil eine Luecke im Messwerkzeug, die nachweislich einen echten Defekt verborgen hat, genau dafuer ins Ledger gehoert. **Die seit Etappe 1 offene Architekturfrage ist entschieden:** `req.tenantPrisma` wurde bei jeder Anfrage gebaut und NIRGENDS gelesen; neun Bereiche haben die Konvention auf dienst-internes `forTenant()` festgelegt. Middleware geloescht (sie war nirgends registriert — der Auftrag irrte bei `app.module.ts:57`, dort ist der Guard verdrahtet), Guard ohne Prisma-Abhaengigkeit, setzt nur noch `req.tenantId` (22 Leser in 9 Dateien) und den `x-tenant-id`-Wechsel fuer SUPER_ADMIN (4 Frontend-Stellen) — beides erstmals getestet; Guard und Middleware hatten NIE Tests, 'ihre Tests' in Etappe 1 war eine Annahme. Totes Kabel, das wie eine Sicherung aussieht, ist schlimmer als keins. Ausnahmeliste in `rls-access-inventory.spec.ts` geleert und mit Wachhund versehen. Drei Kommentare berichtigt, die `TenantMiddleware`/`req.tenantPrisma` als lebendig beschrieben. Executor fing einen still fehlgeschlagenen `git add` (2 von 7 Dateien) selbst an `git status` und lieferte nach. **Verifiziert 10/10 mit vier eigenhaendigen Falsifizierungen** (Header-Wechsel zweimal gebrochen, Fan-out gebrochen, Wachhund ausgeloest — je exakt die benannten Tests rot; 911/911 Tests, 59 Dateien, Typpruefung sauber, 110/110 Live-Pruefungen; 64 Paare und Klassenverteilung 32/17/13/2 nachgerechnet) | 2026-09-11 | 652e762,11f5731,17dca0d,c8de72e | [260911-e2s-mandantentrennung-etappe-2-bereich-tenan](./quick/260911-e2s-mandantentrennung-etappe-2-bereich-tenan/) | +| 260911-fh9 | Mandantentrennung Etappe 2, Bereich auth — die drei in Etappe 1 bewusst ausgelassenen Wege (`getMe`, `changePassword`, `adminResetPassword`) gebunden, alle drei brauchten neue Signaturen (nahmen nur `userId`). Der Anmeldeweg ueber die drei SECURITY-DEFINER-Funktionen NICHT angefasst, per `pg_proc` belegt (weiterhin genau 9 Spalten, auch nachdem die Wegwerf-Tabelle `User` 5 fehlende Spalten bekam). Verbleibende 3 'ungebundene' Stellen sind die `$queryRaw`-Anmeldesuchen, keine Modellzugriffe. **Falle, die der Auftrag selbst gestellt hatte:** Selbstbedienung darf NICHT an `req.tenantId` binden — der Guard laesst SUPER_ADMIN diese Kennung per `x-tenant-id` umschalten (Marktplatz), 'mein Profil' haette ihn sich selbst gegenueber unsichtbar gemacht; gebunden wird an den Mandanten aus dem Sitzungsnachweis (`@CurrentUser().tenantId`), der Controller enthaelt null Verweise auf `req.tenantId`/`x-tenant-id`. **Zwei Loecher in `adminResetPassword` geschlossen, keines davon ein Mandantenproblem:** der Weg pruefte weder den Mandanten des Ziels noch dessen Rolle — ein ADMIN konnte das Passwort eines SUPER_ADMIN ueberschreiben. Beides jetzt dicht, SUPER_ADMIN-Pfad ueber `UserService.findByIdForPlatformAdmin`; `AuthModule` importiert `UserModule`, zyklusfrei. Der Schwesterweg `PATCH /users/:id` hat dieselbe Rollenluecke (T-02-08 prueft nur das ZUWEISEN der Rolle, nicht die bestehende Rolle des Ziels) — ausserhalb der Erlaubnisliste, als WINDOWS #29 festgehalten. **Umgekehrte Fehlerrichtung ist hier leise, nicht laut:** `getMe`-Leere wird zu 200 mit leerem Rumpf, `header.tsx` tut bei `if (u)` nichts — 'nicht angemeldet' und 'Zeile unsichtbar' sind derselbe Wert (WINDOWS #28); `changePassword`-Leere liest sich als `networkError`. Identitaets-Attrappe (ldap-Form) durch asymmetrischen Doppel ersetzt: ungebundener Nachbau ohne Modelle, gebundener ohne `$queryRaw` — beide Grenzen einzeln falsifizierbar. **Verifiziert 8/8** (951/951 Tests, 60 Dateien, Typpruefung sauber, 120/120 Live-Pruefungen; zwei Falsifizierungen vom Pruefer eigenhaendig reproduziert — genau 4 bzw. 2 benannte Tests rot) | 2026-09-11 | 9782bea,92aa8c4,f68beb3 | [260911-fh9-mandantentrennung-etappe-2-bereich-auth-](./quick/260911-fh9-mandantentrennung-etappe-2-bereich-auth-/) | | 260909-eor | Etappe 1 der Mandantentrennung: Anmeldeweg mandantenfaehig gemacht und alle Zugriffe klassifiziert. **Kernfund (#20):** `forTenant()` setzte den Mandantenkontext per set_config auf der Transaktionsverbindung, dispatchte die Abfrage aber ueber den aeusseren Client — empirisch reproduziert (set_config auf Backend-PID 254999, Abfrage auf 255000, Kontext dort NULL). Die Trennung hat damit nie funktioniert, auch nicht an den Stellen, die sie scheinbar nutzten; nach dem Scharfschalten haetten diese Abfragen NULL Zeilen geliefert, was der LDAP-Loeschzweig als 'Gruppe im Verzeichnis verschwunden' gedeutet und geloescht haette. Behoben und live nachgewiesen. Der Anmeldeweg bekam drei SECURITY-DEFINER-Funktionen als schmale Ausnahme (feste Spaltenliste, Gleichheitsbedingung, LIMIT 1) — eine Policy haette nicht gereicht, weil sie zwangslaeufig die ganze Tabelle freigibt. Browser-Gegenprobe lokal bestanden: Anmeldung laedt das Portal, falsches Kennwort verraet weiterhin nicht welches Feld, Kennwort-vergessen laeuft durch (der einzige Protokollfehler war ein lokal fehlender Mailserver, also NACH dem Datenbankzugriff). Klassifikation aller 227 Zugriffe in 59 Einheiten, maschinell gegen Abdriften abgesichert: 31 muessen mandantengebunden werden, 9 teilweise, 16 betreffen keine mandantengebundene Tabelle, 3 bleiben bewusst uebergreifend. 701 Tests gruen | 2026-09-09 | da0ac04 | [260909-eor-anmeldeweg-mandantenfaehig-machen-und-al](./quick/260909-eor-anmeldeweg-mandantenfaehig-machen-und-al/) | | 260910-jab | Die drei zu kurz greifenden Datenbankregeln geschlossen — T-JTS-02, T-JTS-03, WINDOWS #19 (bewusste Reihenfolge-Abweichung, vorgezogen auf Nutzerwunsch, statt wie geplant nach Etappe 2). Neue, handgeschriebene, lokal angewandte Migration `20260910120000_rls_widen_membership_grant_and_platform_read`: `GroupMembership` prueft jetzt beide Seiten der Beziehung (Gruppe UND Benutzer), `ModuleGrant` prueft zusaetzlich beide moeglichen Ziele mit Leer-Zulassung (D-04), `TenderRssFeedSource` bekommt vier nach Befehl getrennte Regeln (Lesen schliesst plattformweite Zeilen ein, Schreiben verlangt weiterhin einen Mandanten — die Trennung ist noetig, weil ein einzelner USING-Ausdruck sonst auch UPDATE/DELETE mitregelt). `SearchProvider` bewusst NICHT angefasst: die WINDOWS-#19-Praemisse ist fuer dieses Modell widerlegt (kein Codeweg erzeugt eine mandantenlose Zeile). Drei loch-behauptende Pruefungen im Wegwerf-Werkzeug UMGEKEHRT statt geloescht (66→74 Pruefungen), mit Verweis auf die alten Pruefungsnamen und Befundkennungen im Meldetext. Genau EIN Anwendungspfad musste mitgebunden werden (`TenderRssFeedSourceService.listForUser`) — sonst haette die Reparatur ihn still von 'liefert nach dem Scharfschalten nichts' auf 'liefert nur die plattformweiten Zeilen, taeuscht Vollstaendigkeit vor' verschlechtert; Falsifizierungsnachweis gefuehrt (Bindung zurueckgenommen, genau ein Test rot, zurueckgesetzt). WINDOWS #19 geschlossen mit Beleg, WINDOWS #24 neu angelegt (Verwaltungsweg fuer plattformweite Zeilen unter der Anwendungsrolle fehlt weiterhin — verschwindet nicht mit #19). Aktenstand kohaerent: Klassifikation, Kritikschrift (neuer Abschnitt "Regelschluss T-JTS-02, T-JTS-03 und WINDOWS #19" mit Signaltabelle beider Fehlerrichtungen je Regel), Betriebsanleitung, WINDOWS.md — fuenf ueberholte Bestandsstellen mit Nachtraegen versehen, alte Messprotokolle bleiben woertlich stehen. Selbst gemessen statt uebernommen: Baseline 833/56 Tests, 66/66 Live-Pruefungen; Endstand 839/56, 74/74; keine zweite Sitzungsvariable fuer den Benutzer gefunden (nur `app.current_tenant`). Rule-1-Fix: implizites `any` in `tenders.controller.ts` nach der Bindung behoben. `npx prisma` versuchte ungefragt Prisma 8 herunterzuladen — abgebrochen, lokale gepinnte 6.19.3 verwendet | 2026-09-10 | f4f3115,6b23735,03fb3bf | [260910-jab-mandantentrennung-die-drei-zu-kurz-greif](./quick/260910-jab-mandantentrennung-die-drei-zu-kurz-greif/) | diff --git a/.planning/quick/260911-fh9-mandantentrennung-etappe-2-bereich-auth-/260911-fh9-SUMMARY.md b/.planning/quick/260911-fh9-mandantentrennung-etappe-2-bereich-auth-/260911-fh9-SUMMARY.md new file mode 100644 index 0000000..1656d68 --- /dev/null +++ b/.planning/quick/260911-fh9-mandantentrennung-etappe-2-bereich-auth-/260911-fh9-SUMMARY.md @@ -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-``-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. diff --git a/.planning/quick/260911-fh9-mandantentrennung-etappe-2-bereich-auth-/260911-fh9-VERIFICATION.md b/.planning/quick/260911-fh9-mandantentrennung-etappe-2-bereich-auth-/260911-fh9-VERIFICATION.md new file mode 100644 index 0000000..0750257 --- /dev/null +++ b/.planning/quick/260911-fh9-mandantentrennung-etappe-2-bereich-auth-/260911-fh9-VERIFICATION.md @@ -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)_