Files
schalli 5228f28cbe
Tessera CI/CD / Lint & Type Check (push) Successful in 47s
Tessera CI/CD / Tests (push) Successful in 55s
Tessera CI/CD / Build & Publish Images (push) Successful in 29s
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
2026-09-09 11:14:58 +02:00

18 KiB

phase, plan, subsystem, tags, requires, provides, affects, actuals, tech-stack, key-files, key-decisions, requirements-completed, coverage, duration, completed, status
phase plan subsystem tags requires provides affects actuals tech-stack key-files key-decisions requirements-completed coverage duration completed status
quick-260909-eor 01 database
postgresql
rls
prisma
multi-tenancy
auth
security
phase provides
quick-260909-dgj Datenbankrolle tessera_app, sieben plus 16 RLS-Policies, WINDOWS
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
datenbank
auth
betrieb
mandantenfaehigkeit
WINDOWS-18
WINDOWS-19
WINDOWS-20
tokens tasks commits
26800 4 4
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
created modified
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
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
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
WINDOWS-18
WINDOWS-19
id description requirement verification human_judgment
D1 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 WINDOWS-20
kind ref status
unit apps/api/src/prisma/prisma-tenant.extension.spec.ts (4 Tests) pass
kind ref status
integration apps/api/scripts/rls-scratch-check.mjs, Abschnitt 1 (5 Live-Pruefungen gegen Wegwerf-Datenbank) pass
false
id description requirement verification human_judgment
D2 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 WINDOWS-18
kind ref status
unit apps/api/src/prisma/auth-lookup-functions.spec.ts (11 Tests), apps/api/src/auth/auth.service.spec.ts (12 Tests) pass
kind ref status
integration apps/api/scripts/rls-scratch-check.mjs, Abschnitt 2 (3 Live-Pruefungen gegen Wegwerf-Datenbank) pass
false
id description requirement verification human_judgment
D3 Alle 227 this.prisma.*-Fundstellen sind klassifiziert (muss-mandantengebunden/bewusst-uebergreifend/keine-mandantengebundene-tabelle/beides), die Klassifikation ist maschinell gegen den Quelltext abgesichert WINDOWS-18
kind ref status
unit apps/api/src/prisma/rls-access-inventory.spec.ts (6 Tests, Drift-Erkennung manuell durchgespielt und bestaetigt) pass
false
id description verification human_judgment rationale
D4 Anmeldung, Fehlversuch und Kennwort-vergessen verhalten sich im echten Browser gegen den neu gebauten lokalen Stand unveraendert
true 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.
~55min 2026-09-09 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