--- phase: quick-260909-ipc plan: 01 subsystem: database tags: [prisma, postgresql, row-level-security, ldap, multi-tenancy, nestjs] requires: - phase: quick-260909-eor provides: "repaired forTenant() helper (array-form $transaction), auth.service.ts bound via forTenant(), full 227-site classification of this.prisma.* access, rls-scratch-check.mjs scratch-database tool" provides: - "ldap-config.service.ts and ldap.service.ts fully converted to forTenant() (16 new/confirmed bound call sites across 6 methods), except the three access sites documented as deliberately cross-tenant" - "closed cross-tenant-delete vulnerability on DELETE /ldap/config/mappings/:id (T-IPC-01) — tenant now derived from session, not the URL id" - "rls-access-inventory.spec.ts detects bound (tenantPrisma.) sites in addition to unbound (this.prisma.) ones, and checks a new Stand column (gebunden/ungebunden/gemischt) against the source" - "5 new empirical checks in rls-scratch-check.mjs proving the LdapConfig/LdapFieldMapping RLS policies (extracted verbatim from the shipped migration) behave as intended under a role without BYPASSRLS" - "docs/mandantentrennung-etappe2-fehlerrichtung.md — written critique of the post-cutover error direction (sees-too-much becomes sees-nothing), with a per-path signal table and the four code paths that read emptiness as absence" affects: [mandantentrennung-etappe-2-groups, mandantentrennung-etappe-2-tenders, mandantentrennung-etappe-3, mandantentrennung-etappe-4] actuals: tokens: 23635 tasks: 3 commits: 3 tech-stack: added: [] patterns: - "forTenant() erzeugt dienst-intern je Methode, nicht ueber req.tenantPrisma (Konvention aus auth.service.ts fortgesetzt, req.tenantPrisma bleibt fuer alle Bereiche eine offene Architekturfrage)" - "rls-access-inventory.spec.ts erkennt gebundene Fundstellen ueber die Zuweisungsform `const = forTenant(` plus nachfolgende `.`-Treffer, mit einer begruendeten Ausnahmeliste fuer req.tenantPrisma-Veroeffentlichung" - "Policies fuer Wegwerf-Datenbank-Pruefungen werden aus der ausgelieferten Migration extrahiert, nie im Werkzeug neu getippt (T-IPC-08)" key-files: created: - docs/mandantentrennung-etappe2-fehlerrichtung.md modified: - apps/api/scripts/rls-scratch-check.mjs - apps/api/src/ldap/ldap-config.service.ts - apps/api/src/ldap/ldap-config.service.spec.ts - apps/api/src/ldap/ldap.controller.ts - apps/api/src/ldap/ldap.service.ts - apps/api/src/ldap/ldap.service.spec.ts - apps/api/src/prisma/rls-access-inventory.spec.ts - docs/mandantentrennung-zugriffsklassifikation.md key-decisions: - "resolveEmailForWrite() bleibt dauerhaft ungebunden (Befund A, T-IPC-04) — email/username sind plattformweit @unique, eine Bindung wuerde eine echte Kollision (WINDOWS #15) in einen P2002-Abbruch verwandeln. Loesung ist an Etappe 3 uebergeben (vermutlich vierte SECURITY-DEFINER-Funktion)." - "getAllActiveConfigs()/onApplicationBootstrap() in ldap-config.service.ts bleiben dauerhaft ungebunden (Befund B) — echter Planer-/Boot-Lesezugriff ueber alle Mandanten, kein vergessener forTenant()-Aufruf. Klasse von (ldap-config.service.ts, ldapConfig) korrigiert von muss-mandantengebunden auf beides." - "Standardgruppen-Uebergabe (reassignDefaultBeforeDelete/ensureDefaultGroup in groups.service.ts) bleibt in diesem Durchlauf unangetastet und ist als Reihenfolgebedingung fuer Etappe 4 dokumentiert — groups ist ohnehin der naechste Bereich." - "Zwei bisher unsichtbare, weil bereits gebundene Fundstellen (auth.service.ts/passwordResetToken, ldap.service.ts/groupMembership) wurden durch die erweiterte Inventarpruefung erstmals entdeckt und nachtraeglich ins Klassifikationsdokument aufgenommen (61 statt 59 Paare)." patterns-established: - "Distinguishable-client test pattern fuer forTenant()-Bindungsnachweise: forTenant wird per mockImplementation auf ein ZWEITES, vom uebergebenen this.prisma unterscheidbares Objekt umgebogen, damit ein Identitaets-Mock eine echte Umstellung nicht mehr verschlucken kann (Befund F)." requirements-completed: [WINDOWS-20, ETAPPE-2-LDAP] duration: ~55min completed: 2026-09-09 status: complete --- # Quick Task 260909-ipc: Mandantentrennung Etappe 2, Bereich ldap — Summary **21 klassifizierte Datenbankzugriffe in `ldap-config.service.ts` und `ldap.service.ts` auf `forTenant()` umgestellt, eine Fremdzugriffsluecke beim Loeschen von Feldzuordnungen geschlossen, und die Fehlerrichtung nach dem geplanten Scharfschalten ("sieht zu viel" wird zu "sieht nichts") schriftlich und an der echten RLS-Policy gemessen festgehalten.** ## Performance - **Duration:** ~55 min - **Tasks:** 3/3 completed - **Files modified:** 8 (1 created, 7 modified) - **Commits:** 3 ## Accomplishments - Der gesamte Bereich `ldap` (21 ursprünglich klassifizierte Zugriffe, plus zwei nachträglich entdeckte bereits-gebundene Fundstellen) läuft jetzt entweder gebunden über `forTenant()` oder trägt eine ausgeschriebene, im Code stehende Begründung, warum er bewusst übergreifend bleibt. - Die Fremdzugriffslücke beim Löschen einer LDAP-Feldzuordnung (`DELETE /ldap/config/mappings/:id`, T-IPC-01) ist geschlossen: der Mandant kommt jetzt aus dem Sitzungsnachweis, nicht mehr nur aus der URL-Kennung. - Die maschinelle Absicherung (`rls-access-inventory.spec.ts`) erkennt jetzt gebundene Zugriffe zusätzlich zu ungebundenen und prüft eine neue Stand-Spalte im Klassifikationsdokument gegen den Quelltext — eine Umstellung kann die Prüfung nicht mehr fälschlich als "Fundstelle verschwunden" scheitern lassen (Befund G). - `apps/api/scripts/rls-scratch-check.mjs` misst jetzt 13 Verhaltensweisen statt 8 (5 neue für den Bereich ldap), gegen die aus der ausgelieferten Migration extrahierten, echten `LdapConfig`/`LdapFieldMapping`-Policies. - `docs/mandantentrennung-etappe2-fehlerrichtung.md` beantwortet die vom Wiedereinstieg verlangte Frage ("Woran würde ich merken, dass eine umgestellte Abfrage zu wenig liefert?") mit einer Signaltabelle je Pfad und den vier Stellen, die Leere als Abwesenheit deuten. ## Task Commits 1. **Aufgabe 1: Fehlerrichtung schriftlich festhalten und an der echten Policy messen** - `a0c9ef0` (feat) 2. **Aufgabe 2: ldap-config.service.ts binden, Fremdzugriff beim Loeschen schliessen, Absicherung erweitern** - `9a57fa7` (feat) 3. **Aufgabe 3: ldap.service.ts binden, die uebergreifende Kollisionspruefung festnageln, Dokument schliessen** - `e1586a4` (feat) **Plan metadata:** committed separately by the orchestrator after this SUMMARY. ## Files Created/Modified - `docs/mandantentrennung-etappe2-fehlerrichtung.md` - neue Kritikschrift: Leitfrage, Messbeleg, Signaltabelle je Pfad, vier "Leere als Abwesenheit"-Stellen, drei bewusst offen gelassene Punkte - `apps/api/scripts/rls-scratch-check.mjs` - neuer Abschnitt `runLdapAreaChecks` (5 Messungen gegen die aus der Migration extrahierten LdapConfig/LdapFieldMapping-Policies) - `apps/api/src/ldap/ldap-config.service.ts` - `getConfig`/`createConfig`/`updateConfig`/`addFieldMapping`/`removeFieldMapping` gebunden; `getAllActiveConfigs`/`onApplicationBootstrap` bleiben ungebunden mit ausgeschriebener Begründung - `apps/api/src/ldap/ldap-config.service.spec.ts` - forTenant-Identitätsmock ergänzt, 9 neue Bindungstests - `apps/api/src/ldap/ldap.controller.ts` - `removeFieldMapping` nimmt jetzt den Mandanten aus dem Sitzungsnachweis, `addFieldMapping` reicht ihn durch - `apps/api/src/ldap/ldap.service.ts` - 11 Abfragen in 6 Methoden gebunden; `resolveEmailForWrite` bleibt ausdrücklich ungebunden, mit ausgeschriebener Begründung - `apps/api/src/ldap/ldap.service.spec.ts` - neuer Testblock mit zwei unterscheidbaren `forTenant()`-Ersatzobjekten, 6 neue Tests - `apps/api/src/prisma/rls-access-inventory.spec.ts` - erweiterte Fundstellensuche (gebunden + ungebunden), neue Stand-Spalten-Prüfung, Ausnahmeliste für `req.tenantPrisma`-Veröffentlichung - `docs/mandantentrennung-zugriffsklassifikation.md` - Stand-Spalte für alle 61 Paare, 2 neu entdeckte Paare, Klassenkorrektur (ldapConfig → beides), neu gerechnete Bereichsübersicht (gebunden getrennt von ungebunden), "Hintergrunddienst als Falle"-Abschnitt für ldap.service.ts auf "geschlossen" aktualisiert ## Decisions Made - **resolveEmailForWrite() bleibt dauerhaft ungebunden** (Befund A, T-IPC-04): `email`/`username` sind plattformweit `@unique`, eine Bindung würde eine echte Kollision in einen P2002-Datenbankabbruch verwandeln statt sie sauber zu melden. Lösung an Etappe 3 übergeben. - **getAllActiveConfigs()/onApplicationBootstrap() bleiben dauerhaft ungebunden** (Befund B): echter Planer-/Boot-Lesezugriff über alle Mandanten. Klasse von (`ldap-config.service.ts`, `ldapConfig`) korrigiert von `muss-mandantengebunden` auf `beides`. - **Zwei bisher unsichtbare, bereits gebundene Fundstellen entdeckt und dokumentiert**: `auth.service.ts`/`passwordResetToken` und `ldap.service.ts`/`groupMembership` waren nie Teil der `this.prisma.*`-Rohtrefferzahl, weil sie schon vor diesem Plan über `forTenant()` liefen — die alte, nur `this.prisma.*` suchende Prüfung konnte sie nicht sehen. Klassen-Verteilung damit 61 statt 59 Paare. - **Standardgruppen-Übergabe (Befund D) bewusst nicht in diesem Durchlauf gelöst**: `reassignDefaultBeforeDelete`/`ensureDefaultGroup` liegen in `groups.service.ts`, das dieser Plan nicht anfasst. Als Reihenfolgebedingung für Etappe 4 dokumentiert — `groups` ist der ohnehin nächste Bereich der Etappe 2. ## Deviations from Plan None (Rule 1-3) — plan executed as written. Two minor Rule-1/technical adjustments made without changing scope: **1. [Rule 1 - Bug] TypeScript implicit-any errors in searchUsers() after binding** - **Found during:** Task 3, type-check - **Issue:** Once `existing` came from `tenantPrisma.user.findMany` (typed `any` via the `as any` cast pattern used throughout this file), the downstream `.map((u) => ...)` callbacks lost their contextual parameter types, tripping `noImplicitAny`. - **Fix:** Added explicit inline parameter type annotations (`(u: { ldapDn: string | null })`, `(d: string | null)`, `(u: { username: string })`). - **Files modified:** apps/api/src/ldap/ldap.service.ts - **Verification:** `npm --prefix apps/api run type-check` returns 0. - **Committed in:** e1586a4 (part of task commit) **2. [Rule 1 - Bug] forTenant() function definition matched the new "unassigned call" detector** - **Found during:** Task 2, running the extended rls-access-inventory.spec.ts against the live tree - **Issue:** `export function forTenant(prisma, tenantId) { ... }` in `prisma-tenant.extension.ts` itself matched the `forTenant\(` pattern used to find call sites, triggering a false-positive "unassigned forTenant( call" violation. - **Fix:** Excluded the function *definition* (not a call) via a negative lookbehind for `function ` in the counting regex. - **Files modified:** apps/api/src/prisma/rls-access-inventory.spec.ts - **Verification:** the new "jedes forTenant(-Vorkommen..." test passes. - **Committed in:** 9a57fa7 (part of task commit) --- **Total deviations:** 2 auto-fixed (both Rule 1, both mechanical/test-tooling correctness, no scope creep). **Impact on plan:** None — both fixes were necessary to make the plan's own new tooling correct; neither touched production LDAP behavior. ## Issues Encountered None beyond the two deviations above. ## User Setup Required None - no external service configuration required. The scratch-database check requires `TESSERA_SCRATCH_ADMIN_URL` (already an existing convention from Etappe 1, not new to this plan). ## Measured Numbers (for the record) - `npm --prefix apps/api run test` → **719 tests green** (53 test files), baseline was 701 (+18: 9 new ldap-config bindings tests, 6 new ldap.service distinguishable-client tests, 3 new rls-access-inventory tests). - `npm --prefix apps/api run type-check` → **0**. - `node apps/api/scripts/rls-scratch-check.mjs` → **13/13 Prüfungen bestanden** (8 aus Etappe 1 + 5 neue aus diesem Plan), including the key evidentiary line `ldapconfig-ungebunden-null-zeilen: bestanden — ungebundener SELECT auf "LdapConfig" liefert 0 Zeile(n)`. - `git diff --stat` confirms `apps/api/prisma/schema.prisma`, `apps/api/prisma/migrations/`, `.env`, and both compose files are untouched. - `DATABASE_URL` / role `tessera` (BYPASSRLS) is unchanged — the cutover switch stays OFF. ## Deferred to Later Stages 1. **`resolveEmailForWrite` address-collision check (Befund A)** — deliberately stays cross-tenant forever; needs an Etappe-3 system-context solution (likely a fourth SECURITY-DEFINER function, mirroring the login-path pattern). 2. **Scheduler silence (Befund E, `getAllActiveConfigs`)** — after cutover this reads 0 rows and the LDAP sync silently stops for every tenant with no log line. No runtime warning added deliberately (would be noise on every install without LDAP); the signal belongs in Etappe 4's pre-cutover check (`rls-preflight.mjs`). 3. **Default-group handoff to `groups` (Befund D)** — `reassignDefaultBeforeDelete`/`ensureDefaultGroup` in `groups.service.ts` are not bound. Ordering condition for Etappe 4: `groups` must be converted before cutover, or a tenant could be left without a default group after a group deletion. ## Next Phase Readiness The `ldap` area of Etappe 2 is fully closed per this plan's success criteria. Per `.planning/.continue-here.md`'s ``, the next area is `groups` (37 sites), then `tenders` (62 sites). The `docs/mandantentrennung-etappe2-fehlerrichtung.md` critique and the newly-extended `rls-access-inventory.spec.ts` (bound-site detection, Stand column) are reusable infrastructure for those next areas — no further tooling work should be needed before starting `groups`. --- *Phase: quick-260909-ipc* *Completed: 2026-09-09* ## Self-Check: PASSED All 9 claimed files verified present on disk; all 3 claimed commit hashes verified present in git history.