Files
tessera-ctl/.planning/quick/260911-fh9-mandantentrennung-etappe-2-bereich-auth-/260911-fh9-SUMMARY.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

17 KiB
Raw Blame History

phase, plan, subsystem, tags, requires, provides, affects, actuals, plan_head_before, tech-stack, key-files, key-decisions, patterns-established, requirements-completed, coverage, duration, completed, status
phase plan subsystem tags requires provides affects actuals plan_head_before tech-stack key-files key-decisions patterns-established requirements-completed coverage duration completed status
quick-260911-fh9 01 auth
prisma
postgresql
row-level-security
multi-tenancy
nestjs
jwt
phase provides
quick-260909-eor drei SECURITY-DEFINER-Funktionen fuer den Anmeldeweg (auth_lookup_user_by_username/email/reset_token, Migration 20260909160000_auth_lookup_functions)
phase provides
quick-260910-das UserService.findByIdForPlatformAdmin (gebundener Fan-out je Mandant) und der Praezedenzfall resolveTargetUser in user.controller.ts
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
quick-260911-favorites-settings
etappe-3-mandantentrennung
tokens tasks commits
26271 3 3
4c3172b5a5
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
created modified
apps/api/src/auth/auth.controller.spec.ts
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
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
runAuthAreaChecks im Wegwerf-Werkzeug: getrennt von runAuthLookupChecks (Anmeldeweg vs. Nach-Anmeldung), erweitert eine bereits vorhandene Wegwerf-Tabelle um fehlende Spalten statt sie neu anzulegen
WINDOWS-18
ETAPPE-2-AUTH
id description requirement verification human_judgment
D1 getMe/changePassword/adminResetPassword binden je ueber tenantPrisma an den Mandanten aus dem Sitzungsnachweis WINDOWS-18
kind ref status
unit apps/api/src/auth/auth.service.spec.ts — AuthService.getMe/changePassword/adminResetPassword (20 Faelle) pass
kind ref status
integration 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) pass
false
id description requirement verification human_judgment
D2 adminResetPassword schliesst die Rechteausweitung ueber die Mandantengrenze (T-FH9-01) und innerhalb des Mandanten (T-FH9-04) ETAPPE-2-AUTH
kind ref status
unit apps/api/src/auth/auth.service.spec.ts — 'FREMDER Mandant: BadRequestException...' und 'Aufrufer ADMIN, Ziel SUPER_ADMIN...ForbiddenException...' pass
kind ref status
integration apps/api/scripts/rls-scratch-check.mjs — auth-adminreset-generierter-client-gebundenes-update-fremder-mandant-scheitert-laut pass
false
id description requirement verification human_judgment
D3 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 ETAPPE-2-AUTH
kind ref status
unit apps/api/src/auth/auth.controller.spec.ts — Mandantenquelle je Handler (9 Faelle) plus Rollen-/Public-Metadaten (14 Faelle) pass
false
id description requirement verification human_judgment
D4 Klassifikationsdokument und WINDOWS.md sind auf den neuen Stand nachgezogen (auth.service.ts/user gebunden, zwei neue offene Ledger-Eintraege) ETAPPE-2-AUTH
kind ref status
unit apps/api/src/prisma/rls-access-inventory.spec.ts (11 Tests, insbesondere der Stand-Vergleich gegen den Quelltext) pass
false
40min 2026-09-11 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.
  • adminResetPasswords 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):

  1. 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):

  1. 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.
  2. Ü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.