Files

14 KiB

phase, plan, subsystem, tags, requires, provides, affects, actuals, tech-stack, key-files, key-decisions, patterns-established, requirements-completed, duration, completed, status
phase plan subsystem tags requires provides affects actuals tech-stack key-files key-decisions patterns-established requirements-completed duration completed status
quick-260909-ipc 01 database
prisma
postgresql
row-level-security
ldap
multi-tenancy
nestjs
phase provides
quick-260909-eor 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
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.<Modell>) sites in addition to unbound (this.prisma.<Modell>) 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
mandantentrennung-etappe-2-groups
mandantentrennung-etappe-2-tenders
mandantentrennung-etappe-3
mandantentrennung-etappe-4
tokens tasks commits
23635 3 3
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 <Name> = forTenant(` plus nachfolgende `<Name>.<Modell>`-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)
created modified
docs/mandantentrennung-etappe2-fehlerrichtung.md
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
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).
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).
WINDOWS-20
ETAPPE-2-LDAP
~55min 2026-09-09 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 <next_action>, 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.