--- phase: quick-260909-mir plan: 01 status: complete subsystem: database tags: [prisma, postgresql, row-level-security, multi-tenancy, nestjs, dkv] # Dependency graph requires: - phase: quick-260909-laa provides: forTenant() binding pattern for user-CRUD services plus the two-client test harness (tenders) provides: - dkv.service.ts fully bound to forTenant() — config paths, invoice history, vehicle master - Cross-tenant ownership gate on the DKV export-file download (T-MIR-03), closing a pre-existing IDOR - dkv.service.spec.ts created from nothing — the area had no test file at all - rls-scratch-check.mjs dkv-area section, including the previously unmeasured parallel-bound-single-ops shape - docs/mandantentrennung-etappe2-fehlerrichtung.md "Bereich dkv" section - WINDOWS #21 — the DKV scheduler start path carried forward as named debt affects: [stage-3-planning, stage-4-preflight, settings-area-quick-task] # Actuals actuals: tasks: 3 commits: 3 tech-stack: added: [] patterns: - "forTenant() bound once per method (groups/ldap/tenders convention); withTenantTransaction() deliberately NOT introduced — this area has no transaction" - "__makeBoundClient() two-client test proof, ported from tender-triage.service.spec.ts into a spec file that did not previously exist" - "Ownership gate derived from the only tenant-bound statement of file ownership (DkvInvoiceHistory.exportFilename) rather than from the filename" key-files: created: - apps/api/src/dkv/dkv.service.spec.ts modified: - apps/api/scripts/rls-scratch-check.mjs - apps/api/src/dkv/dkv.service.ts - apps/api/src/dkv/dkv-scheduler.service.ts - docs/mandantentrennung-etappe2-fehlerrichtung.md - docs/mandantentrennung-zugriffsklassifikation.md - .planning/WINDOWS.md --- # Etappe 2, Bereich dkv — Zusammenfassung ## Ergebnis Alle 21 klassifizierten Zugriffe in `apps/api/src/dkv/dkv.service.ts` sind umgestellt. Gebunden sind es am Ende 22 statt 21, weil der neue Besitzriegel vor dem Ausfuhrdatei-Download einen zusaetzlichen Lesezugriff auf `dkvInvoiceHistory` einfuehrt. Genau ein Zugriff bleibt bewusst ungebunden: der Planer-Startpfad, siehe unten. **Endstand, unabhaengig nachgemessen:** 789 Tests gruen (54 Dateien, Ausgangsstand 772/53), Typpruefung 0, `rls-scratch-check.mjs` 41/41. Schema, Migrationen, Compose-Dateien und Umgebungsdateien unberuehrt; der Umstellungsschalter bleibt aus. ## Die drei Funde ### 1. Eine bereits bestehende Fremdzugriffsluecke (T-MIR-03) `DkvService.getExportFile(tenantId, filename)` nahm die Mandantenkennung entgegen und benutzte sie nie. Die Datei wurde allein ueber ihren Namen aus dem gemeinsamen `user-files/`-Verzeichnis geholt, abgesichert nur durch einen Schutz gegen Pfad-Tricks und ein Namensmuster. Ein Administrator eines beliebigen Mandanten konnte damit die Tankkarten-Auswertung eines anderen herunterladen, sofern er den Dateinamen kannte. Der Riegel leitet die Zugehoerigkeit jetzt aus `DkvInvoiceHistory.exportFilename` ab — der einzigen mandantengebundenen Aussage darueber, wem eine Ausfuhrdatei gehoert. Vor dem Umbau wurde in der Oberflaeche geprueft, dass jeder angebotene Dateiname aus einer Historienzeile stammt (`ExportFileList.tsx`, `InvoiceHistoryTable.tsx`); fuer die regulaere Nutzung aendert der Riegel deshalb nichts. Die Luecke ist keine Folge des Umbaus. Sie bestand seit jeher und faellt nur auf, weil dieser Durchlauf jede Zeile des Bereichs einzeln aufschlaegt. ### 2. Der Bereich hatte keinerlei Tests Weder eine Attrappe, die nichts prueft (der `ldap`-Fehler), noch eine fehlende Attrappe (`groups`, `tenders`) — sondern gar keine Testdatei. Jede Zusicherung dieses Plans waere unpruefbar geblieben. `dkv.service.spec.ts` wurde deshalb neu angelegt, mit dem Zwei-Client-Nachweis aus `tender-triage.service.spec.ts`. ### 3. Ein zerstoerender Fehler in umgekehrter Richtung `saveConfig` enthaelt einen Zweig, der ein bereits gespeichertes Passwort erhalten soll, wenn der Nutzer das Feld leer laesst. Er verschluckte Lese- und Entschluesselungsfehler und machte mit den uebergebenen — moeglicherweise leeren — Werten weiter. Heute faellt das nicht auf, weil der Lesezugriff nie fehlschlaegt. Nach dem Scharfschalten haette derselbe Zweig ein gespeichertes Passwort durch ein leeres ersetzt und verschluesselt abgelegt: stiller Verlust, ohne Fehlermeldung, nicht rekonstruierbar. ## Die bewusst getroffene Entscheidung: WINDOWS #21 Der Planer-Startpfad (`DkvSchedulerService.onModuleInit` → `DkvService.loadAnyActiveConfigForScheduler`) bleibt ungebunden. Drei Formen wurden geprueft: - **(a) an einen konkreten Mandanten binden** — nicht moeglich, `onModuleInit()` hat beim Start strukturell keinen Mandantenkontext. - **(b) Umbau auf einmal-abfragen-viele-bedienen** — abgelehnt. Das ist die in Phase 07-04 zurueckgestellte Mehrmandanten-Planung, also eine Funktionsaenderung und kein Bindungsumbau. - **(c) als benannte Altlast weiterfuehren** — gewaehlt. Praezedenzfall ist `LdapConfigService.getAllActiveConfigs()` aus 260909-ipc, mit einer Unsymmetrie, die dieser Praezedenzfall NICHT deckt und die deshalb ausgeschrieben ist: `getAllActiveConfigs` ist heute korrekt und verstummt erst nach dem Scharfschalten. Der DKV-Planer ist **heute bereits falsch** — `findFirst()` ohne Bedingung zieht bei mehreren Mandanten einen beliebigen und bedient die uebrigen nie; ist ausgerechnet die gezogene Zeile inaktiv, bedient er niemanden — **und** verstummt zusaetzlich spaeter. Die Markierung ist dreifach: eine eigens benannte Methode mit Kopfkommentar, der beide Zustaende nennt (bewusst keine Verzweigung hinter einem optionalen Parameter, die jemand spaeter "vereinheitlicht"), der fortgeschriebene Kopfkommentar in `dkv-scheduler.service.ts`, und der Ledger-Eintrag WINDOWS #21. Das Signal fuer das Verstummen gehoert in die Vorabpruefung von Etappe 4 (`rls-preflight.mjs`), nicht in diesen Durchlauf. ## Gegenbefunde — geprueft und verworfen - **Kein `$transaction` im gesamten Bereich.** Die im Kopf von `prisma-tenant.extension.ts` geforderte erneute Pruefung fuer jeden neuen Fall ist damit beantwortet: kein neuer Fall, `withTenantTransaction()` wird hier nicht gebraucht und wurde nicht eingefuehrt. - **`DkvVehicleMaster` traegt `@@unique([tenantId, kennzeichen])`.** Dieser Bereich hat also NICHT die `tenders`-Falle einer Eindeutigkeitsverletzung auf einer unsichtbaren Zeile. Als Messung festgehalten statt als Absicherung, die nichts absichert. - **Die 21 hielt der Pruefung stand.** Erster Bereich dieses Vorhabens, dessen Kopfzahl beim Hineinsehen nicht kleiner wurde (zuvor 36→6, 37→34, 62→61, 10→8). ## Neu gemessene Form `getHistory` fuehrt zwei gebundene Einzelabfragen parallel ueber `Promise.all` aus — eine Form, die bisher in keinem Bereich vorkam und die das Werkzeug jetzt mit einer eigenen Pruefung abdeckt (`dkv-zwei-parallele-gebundene-einzelabfragen-je-eigener-kontext`). ## Falsifizierungsnachweise Der Plan verlangt fuer jede Umstellungsaufgabe, dass die Tests durch Rueckbau falsifiziert werden — ein Test, der nicht rot werden kann, beweist nichts. Der Nachweis lag zunaechst nur in einer Commit-Nachricht bzw. gar nicht vor und wird hier nachgetragen, damit er dort steht, wo spaeter jemand nachsieht. **Aufgabe 2** (Konfigurationspfade, Commit `222f453`): Der erste gebundene Client von `getConfigForApi` wurde probeweise durch `this.prisma` ersetzt. Genau Test 1 wurde rot, die sechs uebrigen blieben gruen; der Rueckbau wurde zurueckgenommen und die Dateiidentitaet zum Ausgangsstand bestaetigt. Belegt in der Commit-Nachricht von `222f453`. **Aufgabe 3** (Historie, Fahrzeugstammdaten, Besitzriegel, Commit `5e8237d`): Der Nachweis fehlte, weil die Ausfuehrung an dieser Stelle durch das Sitzungslimit abbrach. Er wurde bei der Verifikation nachgeholt: die Bindung des Besitzriegels wurde zurueckgebaut, worauf genau der benannte Test 10 mit einer spezifischen Meldung rot wurde, waehrend die sechzehn uebrigen gruen blieben; danach zurueckgesetzt und der saubere Stand bestaetigt (789/789 Tests, Typpruefung 0, Werkzeug 41/41). Beide Nachweise stammen damit aus unterschiedlichen Haenden — Aufgabe 2 vom ausfuehrenden Agenten, Aufgabe 3 vom pruefenden. Das ist kein Nachteil: der zweite Nachweis ist der staerkere, weil ihn jemand erbracht hat, der die Bindung nicht selbst geschrieben hatte. ## Ablauf-Hinweis Die Ausfuehrung wurde am 2026-09-09 gegen Ende von Aufgabe 3 durch ein Sitzungslimit unterbrochen; die beiden ersten Aufgaben waren committet, die dritte lag vollstaendig im Arbeitsbaum. Nachgetragen wurden am 2026-09-10 die Uebersichtstabelle und die Summenzeile im Klassifikationsdokument sowie diese Zusammenfassung. Die Verifikation fand daran zwei Luecken — der Abschnitt "Der Hintergrunddienst als Falle" war nicht um den DKV-Planer erweitert worden (Aufgabe 3 verlangte das ausdruecklich), und die Falsifizierungsnachweise fehlten in dieser Zusammenfassung; beides wurde danach nachgetragen. Der Bruch fiel auf, weil `git status` einen nicht leeren Arbeitsbaum zeigte — nicht, weil ein Bericht ihn gemeldet haette.