docs(quick-260909-eor): Etappe 1 der Mandantentrennung — Bericht und Klassifikation
Der Kernfund dieser Etappe war ein Defekt im Fundament: forTenant() setzte den Mandantenkontext auf der Transaktionsverbindung, dispatchte die Abfrage aber ueber den aeusseren Client. Empirisch reproduziert statt hergeleitet — set_config auf Backend-PID 254999, Abfrage auf 255000, Kontext dort NULL. Damit hat die Mandantentrennung nie funktioniert, auch nicht dort, wo sie scheinbar benutzt wurde. Nach einem Scharfschalten haette sich das umgekehrt: die betroffenen Abfragen liefern dann null Zeilen, und der LDAP-Loeschzweig haette das als "Gruppe im Verzeichnis verschwunden" gedeutet und sie samt Mitgliedschaften und Modulfreigaben geloescht. Aufgefallen, weil vor dem Umbau geprueft wurde, ob das Fundament traegt. Der Anmeldeweg bekam eine bewusst schmale Ausnahme: drei SECURITY-DEFINER- Funktionen mit fester Spaltenliste, Gleichheitsbedingung und LIMIT 1. Eine Policy waere hier untauglich — sie ist ein Zeilenpraedikat und haette zwangslaeufig die ganze Tabelle freigegeben. Die Klassifikation macht die restliche Arbeit planbar: 227 Zugriffe in 59 Einheiten, davon 31 umzustellen, 9 teilweise, 16 ohne Mandantenbezug und 3 bewusst uebergreifend. Ein Test haelt die Einteilung gegen Abdriften fest. Browser-Gegenprobe lokal bestanden. 701 Tests gruen. Der Schalter bleibt aus; #18, #19 und #20 bleiben offen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FYZcd3SSmo14QTqWx2KKzU
This commit is contained in:
+7
-6
@@ -4,10 +4,10 @@ milestone: v1.2
|
||||
current_phase: 17
|
||||
current_phase_name: eigene-ausschreibungs-quellen-je-nutzer
|
||||
status: verified
|
||||
stopped_at: "Drei Vorhaben am 2026-09-09 abgearbeitet: Dateisicherung fuer user-files (benanntes Volume), Versionsangaben in CLAUDE.md auf den installierten Stand samt Abschnitt ueber sechs nie eingebaute Empfehlungen, und die Vorbereitung der Mandantentrennung. Bei letzterer kam der schwerste Befund der Sitzung heraus: die vorhandene Datenbank-Trennung wirkt gar nicht, weil die Anwendungsrolle sie umgeht (#18) — praktisch gemessen. Gebaut sind Rolle, Verbindungstrennung, Pruefwerkzeug und Policies fuer alle 16 offenen Tabellen; das Scharfschalten bleibt aus, weil 182 Zugriffe ohne Mandantenkontext im Code stehen und der Anmeldeweg darunter zwingend ist. OFFEN: #17 (Volume-Zeile auf dem Server nachtragen), #18 (Scharfschalten, braucht die 182 Stellen), #19 (nullable tenantId bei SearchProvider und TenderRssFeedSource)."
|
||||
last_updated: "2026-09-09T10:25:00.000Z"
|
||||
stopped_at: "Etappe 1 der Mandantentrennung abgeschlossen (Quick 260909-eor). Der kaputte forTenant-Helfer ist repariert und live nachgewiesen, der Anmeldeweg laeuft ueber drei schmale SECURITY-DEFINER-Funktionen, und alle 227 Zugriffe sind klassifiziert (31 umzustellen, 9 teilweise, 16 ohne Mandantenbezug, 3 bewusst uebergreifend) samt maschineller Absicherung gegen Abdriften. NAECHSTE ETAPPEN laut Plan: Etappe 2 = die eigentliche Umstellung der 31+9 Einheiten, geschaetzt 5-8 Durchlaeufe, sinnvollerweise nach Bereichen (tenders 62, groups 37, ldap 21, dkv 21 sind die grossen); Etappe 3 = Systemkontext und WINDOWS #19 (nullable tenantId), 1-2 Durchlaeufe; Etappe 4 = Scharfschalten mit Vorabpruefung und Rueckweg, 1 Durchlauf. Der User hat am 2026-09-09 ausdruecklich erklaert, dass Datenverlust in der Datenbank derzeit egal ist (nichts laeuft produktiv) — das erlaubt beim Scharfschalten den direkten Weg statt aufwendiger Absicherung, gilt aber nur solange das so bleibt."
|
||||
last_updated: "2026-09-09T11:15:00.000Z"
|
||||
last_activity: 2026-09-09
|
||||
last_activity_desc: Dateisicherung auf dem Server wirksam und belegt (#17 zu); offen nur noch #18 und #19
|
||||
last_activity_desc: Etappe 1 der Mandantentrennung fertig — forTenant repariert, Anmeldeweg geloest, alle Zugriffe klassifiziert
|
||||
state_head: c80704957a5518e316a53edc0b8d7e98052a9750
|
||||
progress:
|
||||
total_phases: 17
|
||||
@@ -369,6 +369,7 @@ None yet.
|
||||
| 260909-cx0 | Hochgeladene Dateien (Avatare, DKV-Exporte) ueberlebten kein `--force-recreate` des api-Containers (WINDOWS #17) — lagen nur in der fluechtigen Container-Schicht, keine Compose-Datei mountete `/app/user-files`. Jetzt benanntes Docker-Volume `user-files` in `docker-compose.yml` und `docker-compose.prod.yml` (Eigentuemerschaft uid 1001 aus dem Image, kein Bind-Mount), Betriebshandbuch Kapitel 6/7 entsprechend nachgezogen. Zweiter, unabhaengiger Punkt: CLAUDE.md nannte fuer die Technik-Tabelle noch die 2026-06/07-Empfehlung (Next.js 16.2.x, Prisma 7.8.x, Keycloak, Redis, TanStack Query, shadcn/ui, Playwright, Husky, lint-staged) statt des installierten Stands — jetzt korrigiert auf Next.js 15.5.19, Prisma 6.19.3 etc., nie uebernommene Empfehlungen in eigenem Abschnitt "Recommended But Not Adopted", `.planning/research/STACK.md` nur mit Hinweiszeile ergaenzt. Keine Abhaengigkeit aktualisiert. **WINDOWS #17 bleibt offen** — die Aenderung erreicht die laufende Installation auf alpha nicht, `/opt/tessera/docker-compose.yml` weicht vom Repository ab und muss vom Nutzer selbst ergaenzt werden | 2026-09-09 | dab72eb,c807049 | [260909-cx0-dateisicherung-nachruesten-und-versionsa](./quick/260909-cx0-dateisicherung-nachruesten-und-versionsa/) |
|
||||
| 260909-cx0 | Dateisicherung nachgeruestet und Versionsangaben geradegezogen. **user-files** liegt jetzt in einem benannten Volume (docker-compose.yml und .prod.yml) — vorher lag der Ordner nur in der fluechtigen Container-Schicht, hochgeladene Profilbilder und DKV-Exporte waeren bei jedem --force-recreate weg gewesen. Benanntes Volume statt Bind-Mount, weil das Image /app/user-files an uid 1001 uebereignet; ein frisch angelegtes Host-Verzeichnis gehoert root und haette aus dem Datenverlust einen kaputten Upload gemacht. docker-compose.dev.yml blieb bewusst unveraendert (Compose fuehrt Mount-Listen ueber das Ziel zusammen). **CLAUDE.md** nennt jetzt die installierten Fassungen statt der urspruenglich empfohlenen (Next.js 15.5.19 statt 16, Prisma 6.19.3 statt 7); neu ist ein Abschnitt 'Recommended But Not Adopted', der sechs nie eingebaute Empfehlungen benennt — darunter Keycloak, Redis und shadcn/ui. Der Block ist generiert, deshalb traegt er einen Herkunftsvermerk und die Recherchedatei eine datierte Hinweiszeile; ihre Zahlen blieben unangetastet. Keine Abhaengigkeit angefasst (per git diff gegengeprueft). **WINDOWS #17 am 2026-09-09 geschlossen.** Beim Nachtragen auf dem Server kam heraus, dass dort gar nicht `docker-compose.yml` gilt: die `.env` setzt `COMPOSE_FILE=docker-compose.prod.yml`. Die drei Zeilen wurden in dieser Datei ergaenzt (Sicherung `docker-compose.prod.yml.bak.20260909-0818`). Nach dem Neuerstellen durch den User belegt: Mount `tessera_user-files -> /app/user-files` vorhanden, Ordner gehoert uid 1001 (das benannte Volume hat die Eigentuemerschaft uebernommen), Schreiben als Dienstnutzer funktioniert, und eine Probedatei lag tatsaechlich unter /var/lib/docker/volumes/tessera_user-files/_data auf dem Host — also ausserhalb des Containers | 2026-09-09 | c807049 | [260909-cx0-dateisicherung-nachruesten-und-versionsa](./quick/260909-cx0-dateisicherung-nachruesten-und-versionsa/) |
|
||||
| 260909-dgj | Mandantentrennung auf Datenbankebene vorbereitet (WINDOWS #18). Ausloeser war ein gemessener Befund: die Anwendung verbindet als Rolle mit Superuser- und BYPASSRLS-Recht, daher greifen die sieben vorhandenen Policies gar nicht — ohne gesetzten Mandantenkontext lieferte 'SELECT count(*) FROM Group' zwei statt null Zeilen. Gebaut wurden: Rolle `tessera_app` ohne Umgehungsrecht (wiederholbare Migration, kein Kennwort im SQL), Trennung von Migrations- und Laufzeitverbindung ueber TESSERA_MIGRATE_DATABASE_URL, ein Pruefwerkzeug mit fuenf transaktionssicheren Nachweisen, Policies fuer die 16 fehlenden Tabellen und eine Betriebsanleitung. **Der Schalter bleibt bewusst aus, #18 bleibt offen:** im Code stehen 182 Datenbankzugriffe ohne Mandantenkontext gegen 19 mit — darunter zwingend der Anmeldeweg, der die Benutzerzeile liest, bevor der Mandant bekannt ist (er kommt erst aus dieser Zeile). Ein Umschalten wuerde die Anmeldung fuer alle sperren. Dabei fiel #19 an: SearchProvider und TenderRssFeedSource haben ein nullable tenantId; die einfache Policy wuerde die plattformweiten Zeilen nach dem Scharfschalten fuer jeden Mandanten unsichtbar machen. 673/673 Tests gruen | 2026-09-09 | efaabc9 | [260909-dgj-mandantentrennung-auf-alle-tabellen-mit-](./quick/260909-dgj-mandantentrennung-auf-alle-tabellen-mit-/) |
|
||||
| 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/) |
|
||||
|
||||
## Deferred Items
|
||||
|
||||
@@ -408,7 +409,7 @@ sind. Kein Anlass, sie vorher erneut vorzulegen.
|
||||
|
||||
## Session Continuity
|
||||
|
||||
Last session: 2026-09-09T10:25:00.000Z
|
||||
Stopped at: Nichts in Arbeit. Offen im Ledger nur noch #18 und #19, die zusammengehoeren und ein eigener Vorgang sind: die Mandantentrennung scharf schalten, wofuer zuerst die 182 Datenbankzugriffe ohne Mandantenkontext umgestellt werden muessen (der Anmeldeweg ist zwingend darunter), und dabei die nullable-tenantId-Faelle mitloesen. Alles Vorbereitende dafuer ist gebaut und liegt bereit. Zurueckgestellt bleiben #12, Abnahmeplan 02-05, Mandanten-Branding und die Lizenzpruefung.
|
||||
Last session: 2026-09-09T11:15:00.000Z
|
||||
Stopped at: Etappe 1 der Mandantentrennung ist fertig und gepusht. Weiter geht es mit Etappe 2 — der Umstellung der 31 vollstaendig und 9 teilweise betroffenen Einheiten, sinnvollerweise nach Bereichen gebuendelt. Die Grundlage dafuer liegt in docs/mandantentrennung-zugriffsklassifikation.md, die dort getroffene Einteilung ist maschinell gegen Abdriften abgesichert. Offen im Ledger: #18 (Scharfschalten), #19 (nullable tenantId, mit #18 zu loesen), #20 (bleibt offen bis die Wirkung nach dem Scharfschalten belegt ist, der Defekt selbst ist behoben).
|
||||
Resume file: None
|
||||
Last activity: 2026-09-09 - Dateisicherung auf dem Server eingetragen und nachgewiesen, #17 geschlossen
|
||||
Last activity: 2026-09-09 - Etappe 1 der Mandantentrennung abgeschlossen
|
||||
|
||||
+219
@@ -0,0 +1,219 @@
|
||||
---
|
||||
phase: quick-260909-eor
|
||||
plan: 01
|
||||
subsystem: database
|
||||
tags: [postgresql, rls, prisma, multi-tenancy, auth, security]
|
||||
|
||||
requires:
|
||||
- phase: quick-260909-dgj
|
||||
provides: Datenbankrolle tessera_app, sieben plus 16 RLS-Policies, WINDOWS #18/#19
|
||||
|
||||
provides:
|
||||
- Repariertes forTenant() — Mandantenkontext und Abfrage auf derselben Datenbankverbindung (Array-Form von $transaction)
|
||||
- Drei SECURITY-DEFINER-Funktionen fuer den Anmeldeweg (auth_lookup_user_by_username/_by_email/_reset_token)
|
||||
- Vollstaendige Klassifikation aller 227 this.prisma.*-Fundstellen (59 Datei-Modell-Paare) in docs/mandantentrennung-zugriffsklassifikation.md
|
||||
- Maschinelle Absicherung der Klassifikation gegen Quelltextdrift (rls-access-inventory.spec.ts)
|
||||
- Wegwerf-Pruefwerkzeug rls-scratch-check.mjs mit 8 Live-Nachweisen gegen eine eigene Scratch-Datenbank
|
||||
|
||||
affects: [datenbank, auth, betrieb, mandantenfaehigkeit, WINDOWS-18, WINDOWS-19, WINDOWS-20]
|
||||
|
||||
actuals:
|
||||
tokens: 26800
|
||||
tasks: 4
|
||||
commits: 4
|
||||
|
||||
tech-stack:
|
||||
added: []
|
||||
patterns:
|
||||
- "Array-Form von $transaction statt interaktiver Callback-Form fuer RLS-ueber-Extensions — Prismas eigenes vorgesehenes Muster, einzige Form, die set_config und Abfrage an dieselbe Verbindung bindet"
|
||||
- "SECURITY-DEFINER-Funktion mit festem Suchpfad (public, pg_temp), STABLE, LIMIT 1, GRANT ausschliesslich an die Anwendungsrolle — enge Ausnahme statt Policy-Aufweichung, wenn eine Abfrage vor bekanntem Mandanten stattfinden muss"
|
||||
- "Bestandsaufnahme-Spec liest Quelltext UND Dokumentation bei jedem Testlauf neu ein und vergleicht ueber (Datei, Modell)-Schluessel statt Zeilennummer — ueberlebt das Verschieben von Code"
|
||||
- "Wegwerf-Datenbank mit fest verdrahtetem Namen (nie per Umgebungsvariable) fuer Live-Nachweise, die keine Rolle mit echten Rechten gegen die Produktivdatenbank brauchen"
|
||||
|
||||
key-files:
|
||||
created:
|
||||
- apps/api/src/prisma/prisma-tenant.extension.spec.ts
|
||||
- apps/api/scripts/rls-scratch-check.mjs
|
||||
- apps/api/prisma/migrations/20260909160000_auth_lookup_functions/migration.sql
|
||||
- apps/api/src/prisma/auth-lookup-functions.spec.ts
|
||||
- apps/api/src/prisma/rls-access-inventory.spec.ts
|
||||
- docs/mandantentrennung-zugriffsklassifikation.md
|
||||
modified:
|
||||
- apps/api/src/prisma/prisma-tenant.extension.ts
|
||||
- apps/api/src/auth/auth.service.ts
|
||||
- apps/api/src/auth/auth.service.spec.ts
|
||||
- docs/mandantentrennung-datenbankrolle.md
|
||||
- .planning/WINDOWS.md
|
||||
|
||||
key-decisions:
|
||||
- "SECURITY-DEFINER-Funktionen statt Policy-Aufweichung oder zweiter Datenbankrolle fuer den Anmeldeweg — eine Policy kann die Form der Abfrage nicht einschraenken, eine zweite Rolle braucht einen zweiten Verbindungspool"
|
||||
- "WINDOWS #20 bleibt bewusst OPEN statt fixed, obwohl der forTenant()-Verbindungsdefekt technisch behoben und live nachgewiesen ist — die Wirkung unter der echten Anwendungsrolle tessera_app ist erst nach dem Scharfschalten (#18) beobachtbar, #20 bleibt an dieselbe Bedingung gebunden wie #18/#19"
|
||||
- "getMe/changePassword/adminResetPassword in auth.service.ts bewusst NICHT angefasst — sie kennen den Mandanten bereits aus dem Sitzungsnachweis und gehoeren als gewoehnliche muss-mandantengebunden-Fundstellen in Etappe 2, nicht in die Anmelde-Ausnahme"
|
||||
|
||||
requirements-completed: [WINDOWS-18, WINDOWS-19]
|
||||
|
||||
coverage:
|
||||
- id: D1
|
||||
description: "forTenant() bindet Mandantenkontext und Abfrage nachweislich an dieselbe Datenbankverbindung; unter einer Rolle ohne BYPASSRLS liefert forTenant(A) nur Zeilen von A, nie von B, ein ungebundener Zugriff derselben Rolle liefert null Zeilen"
|
||||
requirement: "WINDOWS-20"
|
||||
verification:
|
||||
- kind: unit
|
||||
ref: "apps/api/src/prisma/prisma-tenant.extension.spec.ts (4 Tests)"
|
||||
status: pass
|
||||
- kind: integration
|
||||
ref: "apps/api/scripts/rls-scratch-check.mjs, Abschnitt 1 (5 Live-Pruefungen gegen Wegwerf-Datenbank)"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D2
|
||||
description: "Anmeldesuche findet den passenden Benutzer unter einer Rolle ohne BYPASSRLS weiterhin; ein gewoehnlicher SELECT auf User liefert unter derselben Rolle null Zeilen; unbekannter Benutzername liefert nichts, wirft nicht"
|
||||
requirement: "WINDOWS-18"
|
||||
verification:
|
||||
- kind: unit
|
||||
ref: "apps/api/src/prisma/auth-lookup-functions.spec.ts (11 Tests), apps/api/src/auth/auth.service.spec.ts (12 Tests)"
|
||||
status: pass
|
||||
- kind: integration
|
||||
ref: "apps/api/scripts/rls-scratch-check.mjs, Abschnitt 2 (3 Live-Pruefungen gegen Wegwerf-Datenbank)"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D3
|
||||
description: "Alle 227 this.prisma.*-Fundstellen sind klassifiziert (muss-mandantengebunden/bewusst-uebergreifend/keine-mandantengebundene-tabelle/beides), die Klassifikation ist maschinell gegen den Quelltext abgesichert"
|
||||
requirement: "WINDOWS-18"
|
||||
verification:
|
||||
- kind: unit
|
||||
ref: "apps/api/src/prisma/rls-access-inventory.spec.ts (6 Tests, Drift-Erkennung manuell durchgespielt und bestaetigt)"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D4
|
||||
description: "Anmeldung, Fehlversuch und Kennwort-vergessen verhalten sich im echten Browser gegen den neu gebauten lokalen Stand unveraendert"
|
||||
verification: []
|
||||
human_judgment: true
|
||||
rationale: "Vom Nutzer im Browser gegen localhost:3000 durchgeklickt (lokal neu gebauter API-Container, Server 192.168.13.12 nicht angefasst): Anmeldung mit lokalem Benutzer erfolgreich, falsches Kennwort abgelehnt ohne Feld-Hinweis (T-02-01 eingehalten), Kennwort-vergessen-Formular ohne sichtbaren Fehler abgeschickt. Ein einzelner Protokolleintrag (MailService: getaddrinfo ENOTFOUND mailhog) ist umgebungsbedingt (kein mailhog-Container lokal) und liegt NACH dem erfolgreichen Datenbankzugriff ueber die neue SECURITY-DEFINER-Funktion — kein Hinweis auf einen durch den Umbau verursachten Fehler."
|
||||
|
||||
duration: ~55min
|
||||
completed: 2026-09-09
|
||||
status: complete
|
||||
---
|
||||
|
||||
# Quick Task 260909-eor: Anmeldeweg mandantenfaehig machen und alle Datenbankzugriffe klassifizieren Summary
|
||||
|
||||
**Repariert einen zuvor unbemerkten Verbindungsfehler in `forTenant()` (WINDOWS #20), gibt dem Anmeldeweg eine schmale SECURITY-DEFINER-Ausnahme fuer die drei Lesezugriffe vor bekanntem Mandanten, und klassifiziert alle 227 Datenbankzugriffe im API-Quelltext maschinell abgesichert — schaltet die Mandantentrennung selbst aber weiterhin NICHT scharf.**
|
||||
|
||||
## Performance
|
||||
|
||||
- **Duration:** ~55 min
|
||||
- **Tasks:** 4/4
|
||||
- **Files modified:** 11 (5 neu, 6 geaendert), plus ein Nachtrag zu `.planning/WINDOWS.md` in einem eigenen fuenften Commit
|
||||
|
||||
## Accomplishments
|
||||
|
||||
- **`forTenant()` repariert (Aufgabe 1, WINDOWS #20).** Die bisherige interaktive Callback-Form von `$transaction` fuehrte die eigentliche Abfrage auf einer ANDEREN Datenbankverbindung aus als `set_config()` — gemessen: `set_config` auf Backend-PID 254999, die Abfrage auf 255000, Mandantenkontext dort `NULL`. Ersetzt durch die Array-Form (`prisma.$transaction([setTenantContext, query(args)])`), Prismas eigenes vorgesehenes RLS-ueber-Extensions-Muster. Injektionsfestigkeit (T-02-05) bleibt ueber ein getaggtes `$executeRaw`-Template erhalten. Live nachgewiesen gegen eine Wegwerf-Datenbank: gleiche Backend-Verbindung, gesetzter Kontext, keine Fremdmandanten-Zeilen, keine Zeilen ohne Kontext — alle 5 Pruefungen bestanden.
|
||||
- **Anmeldeweg mandantenfaehig (Aufgabe 2, WINDOWS #18).** Drei enge `SECURITY DEFINER`-Funktionen (`STABLE`, fester Suchpfad `public, pg_temp`, `LIMIT 1`, Ausfuehrungsrecht ausschliesslich `tessera_app`) ersetzen die drei Lesezugriffe in `auth.service.ts`, die vor bekanntem Mandanten stattfinden: `auth_lookup_user_by_username`, `auth_lookup_user_by_email`, `auth_lookup_reset_token`. Alle nachfolgenden Schreibzugriffe (lastLoginAt, passwordHash, Reset-Token) laufen ueber `forTenant()`, gebunden an den soeben gefundenen Benutzer. Live nachgewiesen: Anmeldesuche findet den Benutzer, unbekannter Name liefert nichts ohne zu werfen, gewoehnlicher `SELECT * FROM "User"` liefert unter derselben Rolle null Zeilen.
|
||||
- **Vollstaendige Klassifikation (Aufgabe 3).** `docs/mandantentrennung-zugriffsklassifikation.md` ordnet alle 227 `this.prisma.*`-Fundstellen (32 Dateien, zusammengefasst zu 59 Datei-Modell-Paaren) einer von vier Klassen zu: 31 `muss-mandantengebunden`, 16 `keine-mandantengebundene-tabelle`, 9 `beides` (Hintergrunddienst-Sonderfall: uebergreifend lesen, je Zeile mandantengebunden schreiben — `ldap.service.ts`, `tender-digest.scheduler.ts`, `tender-matching.service.ts`), 3 `bewusst-uebergreifend`. `rls-access-inventory.spec.ts` ermittelt die Fundstellen bei jedem Testlauf neu und scheitert bei jeder Abweichung — im Zuge der Arbeit selbst getestet (eine Zeile aus dem Dokument geloescht, Fehlschlag bestaetigt, zurueckgesetzt).
|
||||
- **Zwei belegte Befunde festgehalten:** `req.tenantPrisma` wird von `tenant.middleware.ts`/`tenant.guard.ts` gesetzt, aber im gesamten `apps/api/src` von niemandem gelesen — Entscheidung fuer Etappe 2. WINDOWS #19 (nullbares `tenantId` bei `SearchProvider`/`TenderRssFeedSource`) bleibt benannter Blocker fuer Etappe 3.
|
||||
- **Browser-Gegenprobe bestanden (Aufgabe 4).** Lokale Dienste mit dem neuen Stand neu gebaut (`docker compose build api && up -d --force-recreate api`), dann im Browser gegen `localhost:3000` geprueft: Anmeldung mit lokalem Benutzer erfolgreich, falsches Kennwort abgelehnt ohne Feld-Hinweis, Kennwort-vergessen-Formular ohne sichtbaren Fehler. Details siehe Abschnitt "Browser-Gegenprobe" unten.
|
||||
|
||||
## Task Commits
|
||||
|
||||
Each task was committed atomically:
|
||||
|
||||
1. **Task 1: forTenant() auf eine Verbindung zwingen und die Wirkung live nachweisen** - `bbf1795` (fix)
|
||||
2. **Task 2: Anmeldeweg ueber drei eng geschnittene Datenbankfunktionen mandantenfaehig machen** - `de50297` (feat)
|
||||
3. **Task 3: Alle 227 Datenbankzugriffe klassifizieren und die Klassifikation maschinell absichern** - `5f3a39c` (docs)
|
||||
4. **Task 4: Anmeldung lokal gegenpruefen** - kein eigener Code-Commit (Checkpoint), Nachtrag zur Bewertung von WINDOWS #20 in `da0ac04` (fix)
|
||||
|
||||
Aufgabe 2 lief nach TDD: Testdateien (`auth-lookup-functions.spec.ts`, erweitertes `auth.service.spec.ts`) zusammen mit der Implementierung im selben Commit, rot-vor-gruen waehrend der Entwicklung bestaetigt, keine separate RED-Phase committet.
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
- `apps/api/src/prisma/prisma-tenant.extension.ts` - Array-Form von `$transaction`, getaggtes `$executeRaw`, ausfuehrlicher Kopfkommentar mit gemessenen Backend-PIDs und benannten Grenzfaellen fuer Etappe 2
|
||||
- `apps/api/src/prisma/prisma-tenant.extension.spec.ts` - prueft die Form des Aufrufs (Array mit zwei Eintraegen, Rueckgabewert ist der zweite) ohne laufende Datenbank
|
||||
- `apps/api/scripts/rls-scratch-check.mjs` - Wegwerf-Datenbank, 8 Live-Pruefungen (5 fuer forTenant(), 3 fuer die Anmelde-Funktionen), fest verdrahteter Datenbankname, nutzt `prisma db execute --file` fuer mehrteilige Migrationsskripte
|
||||
- `apps/api/prisma/migrations/20260909160000_auth_lookup_functions/migration.sql` - drei SECURITY-DEFINER-Funktionen mit ausgeschriebener Entscheidungsbegruendung im Kopf
|
||||
- `apps/api/src/prisma/auth-lookup-functions.spec.ts` - 11 Tests gegen den Migrationstext (Suchpfad, STABLE, LIMIT 1, Rechteentzug/-vergabe, kein Kennwort)
|
||||
- `apps/api/src/auth/auth.service.ts` - drei Lesezugriffe auf `$queryRaw`-Funktionsaufrufe umgestellt, fuenf Schreibzugriffe auf `forTenant()`
|
||||
- `apps/api/src/auth/auth.service.spec.ts` - Mocks fuer `forTenant()` und `$queryRaw`, neue Testfaelle fuer alle drei Funktionsaufrufe
|
||||
- `apps/api/src/prisma/rls-access-inventory.spec.ts` - ermittelt (Datei, Modell)-Fundstellen aus dem Quelltext, vergleicht gegen die Dokument-Tabelle
|
||||
- `docs/mandantentrennung-zugriffsklassifikation.md` - vollstaendige Bestandsaufnahme, Mengentabelle, Hintergrunddienst-Abschnitt, zwei belegte Befunde
|
||||
- `docs/mandantentrennung-datenbankrolle.md` - verlinkt das neue Dokument, korrigiert die ueberholte Zahl 182 auf 227/59, ergaenzt den WINDOWS-#20-Sperrgrund
|
||||
- `.planning/WINDOWS.md` - #18/#19 um Nachtrag ergaenzt, #20 zunaechst faelschlich als `fixed` markiert und auf Rueckmeldung wieder auf `open` gesetzt (siehe Deviations)
|
||||
|
||||
## Decisions Made
|
||||
|
||||
- WINDOWS #20 bleibt `open`, nicht `fixed` — siehe `key-decisions` oben und Abschnitt "Deviations".
|
||||
- `getMe`/`changePassword`/`adminResetPassword` in `auth.service.ts` bewusst nicht angefasst, bereits als `muss-mandantengebunden` in der Klassifikation fuer Etappe 2 eingetragen.
|
||||
- `SearchProvider` in der Klassifikation als `muss-mandantengebunden` eingestuft (nicht `keine-mandantengebundene-tabelle`), mit Fussnote zu WINDOWS #19 — die Spalte ist nullbar, aber laut 05-02-Entscheidung kommen die tatsaechlichen Vorgaben aus Konstanten, nicht aus der Datenbank.
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
### Auto-fixed Issues
|
||||
|
||||
**1. [Rule 3 - Blocking issue] Eigentreffer der Bestandsaufnahme-Pruefung durch woertlichen Code-Text im Kommentar**
|
||||
- **Found during:** Task 3 (Vorbereitung der Bestandsaufnahme-Spec)
|
||||
- **Issue:** Der Kopfkommentar von `validateUser()` in `auth.service.ts` (aus Aufgabe 2) enthielt woertlich `this.prisma.user.findUnique` als erklaerenden Text — die grep-basierte Bestandsaufnahme-Spec haette das als echte Fundstelle gezaehlt.
|
||||
- **Fix:** Kommentar auf "this dot prisma dot user dot findUnique" umformuliert, inhaltlich identisch, textuell nicht mehr treffend fuer den Regex.
|
||||
- **Files modified:** apps/api/src/auth/auth.service.ts
|
||||
- **Commit:** 5f3a39c
|
||||
|
||||
**2. [Rule 1 - Bug] WINDOWS-Ledger-Tabelle und JSON-Block liefen auseinander**
|
||||
- **Found during:** Auf Rueckmeldung des Koordinators zu Task 4 (nach Task 3 bereits committet)
|
||||
- **Issue:** Die WINDOWS.md-Bearbeitung in Task 3 hat nur die Markdown-Tabelle von Hand angepasst (Eintrag #20 auf `fixed`, `#18`/`#19` um Nachtrag ergaenzt) und dabei den massgeblichen JSON-Block am Dateiende — die eigentliche Quelle der Wahrheit fuer `gsd-tools windows status` — nicht mitgezogen. `gsd-tools windows status` scheiterte seitdem mit "Ledger counts disagree with entries".
|
||||
- **Fix:** Auf ausdruecklichen Wunsch des Koordinators sollte #20 ohnehin OPEN bleiben statt `fixed` (siehe Rationale in `coverage`/D4-Nachbarschaft). Ledger aus der Vorversion ueber die `broken-windows.cjs`-Bibliothek (`parseLedger`/`renderLedger`) neu aufgebaut, dabei Tabelle und JSON-Block wieder synchron gehalten und #20 korrekt auf `open` belassen.
|
||||
- **Files modified:** .planning/WINDOWS.md
|
||||
- **Commit:** da0ac04
|
||||
|
||||
**Total deviations:** 2 (beide Rule 1/3, keine Architekturentscheidung noetig)
|
||||
**Impact on plan:** Kein inhaltlicher Einfluss auf die drei Aufgaben-Ergebnisse — beide Korrekturen betrafen Nebenprodukte (Kommentartext, Ledger-Konsistenz), nicht den geprueften Datenbank-/Anwendungscode.
|
||||
|
||||
## Issues Encountered
|
||||
|
||||
Keine blockierenden Probleme jenseits der beiden oben dokumentierten Deviations. Alle Verify-Kommandos aus dem Plan liefen gruen: `npm run test` (701/701 Tests, 53 Dateien), `npm run type-check` sauber, `rls-scratch-check.mjs` 8/8 Pruefungen bestanden.
|
||||
|
||||
## Browser-Gegenprobe (Aufgabe 4)
|
||||
|
||||
Durchgefuehrt gegen `localhost:3000` (Server 192.168.13.12 nicht angefasst), nach lokalem Neubau des API-Containers mit dem Stand aus allen drei Code-Commits:
|
||||
|
||||
1. Anmeldung mit lokalem Benutzer (admin): erfolgreich, Portal laedt, Seitenleiste und Dashboard erscheinen wie zuvor.
|
||||
2. Falsches Kennwort: abgelehnt mit "Benutzername oder Passwort ungueltig" — verraet nicht, welches Feld falsch war (T-02-01 eingehalten).
|
||||
3. Kennwort-vergessen-Seite: Formular abgeschickt, Seite antwortet ohne sichtbaren Fehler.
|
||||
|
||||
`docker compose logs api` zeigte genau einen Protokolleintrag waehrend der Pruefung:
|
||||
|
||||
[MailService] Failed to send password reset email to admin@tessera.local
|
||||
Error: getaddrinfo ENOTFOUND mailhog
|
||||
|
||||
Als **umgebungsbedingt eingestuft, nicht dem Umbau zuzurechnen**: lokal existiert kein `mailhog`-Container (`docker compose ps` bestaetigt nur `api`/`db`/`web`). Entscheidend fuer die Bewertung ist die Reihenfolge — der Fehler kommt aus dem `MailService`, also NACH dem Datenbankzugriff. Der neue SECURITY-DEFINER-Weg fuer den Token-Lookup (`auth_lookup_reset_token`) wurde durchlaufen und hat den passenden Datensatz gefunden; gescheitert ist erst der Mailversand an einen lokal nicht existierenden Server. Keine Meldung ueber verweigerte Rechte, keine Prisma-Ausnahme, kein Hinweis auf eine leere Ergebnismenge.
|
||||
|
||||
## User Setup Required
|
||||
|
||||
Keine sofortige Handlung noetig. `DATABASE_URL` ist unveraendert, der Server 192.168.13.12 wurde nicht angefasst, keine Migration auf eine Live-Datenbank angewendet.
|
||||
|
||||
## Known Stubs
|
||||
|
||||
Keine — alle Bausteine sind vollstaendig implementiert und getestet, keine leeren Rueckgabewerte, kein Platzhaltertext.
|
||||
|
||||
## Next Phase Readiness
|
||||
|
||||
**Bereit fuer Etappe 2** (Umbau der 31 `muss-mandantengebunden`- und 9 `beides`-Fundstellen auf `forTenant()`), sobald diese in eigenen Plaenen geschnitten wird — siehe `<next_stages>` im Plan `260909-eor-PLAN.md` fuer die vorgeschlagene Reihenfolge (`tenders` 62, `groups` 37, `ldap` 21, `dkv` 21, `user` 17, `module-registry` 17, `dashboard` 13, `calendar` 12, `favorites` 7, `settings` 4). `auth` (8) ist durch diese Etappe bereits erledigt, `tenant` (8) faellt weitgehend unter Etappe 3.
|
||||
|
||||
**Blocker:** keiner fuer diesen Vorgang selbst. Die Umstellung (`DATABASE_URL` auf `tessera_app`) bleibt weiterhin blockiert, bis Etappe 2 und Etappe 3 (benannter Systemkontext fuer die 9 `beides`- und 3 `bewusst-uebergreifend`-Fundstellen, plus WINDOWS #19) abgeschlossen sind.
|
||||
|
||||
**WINDOWS #18, #19 und #20 bleiben `open`** — dieser Vorgang hat das Fundament repariert und die Landkarte gezeichnet, vollzieht die Umstellung selbst aber nicht.
|
||||
|
||||
## Self-Check: PASSED
|
||||
|
||||
- FOUND: apps/api/src/prisma/prisma-tenant.extension.ts
|
||||
- FOUND: apps/api/src/prisma/prisma-tenant.extension.spec.ts
|
||||
- FOUND: apps/api/scripts/rls-scratch-check.mjs
|
||||
- FOUND: apps/api/prisma/migrations/20260909160000_auth_lookup_functions/migration.sql
|
||||
- FOUND: apps/api/src/prisma/auth-lookup-functions.spec.ts
|
||||
- FOUND: apps/api/src/auth/auth.service.ts
|
||||
- FOUND: apps/api/src/auth/auth.service.spec.ts
|
||||
- FOUND: apps/api/src/prisma/rls-access-inventory.spec.ts
|
||||
- FOUND: docs/mandantentrennung-zugriffsklassifikation.md
|
||||
- FOUND: docs/mandantentrennung-datenbankrolle.md
|
||||
- FOUND: .planning/WINDOWS.md
|
||||
- FOUND commit bbf1795, de50297, 5f3a39c, da0ac04
|
||||
|
||||
---
|
||||
*Quick Task: 260909-eor*
|
||||
*Completed: 2026-09-09*
|
||||
Reference in New Issue
Block a user