Files
tessera-ctl/.planning/quick/260909-mir-mandantentrennung-etappe-2-bereich-dkv-a/260909-mir-SUMMARY.md
T

184 lines
9.0 KiB
Markdown

---
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.