Files

9.0 KiB

phase, plan, status, subsystem, tags, requires, provides, affects, actuals, tech-stack, key-files
phase plan status subsystem tags requires provides affects actuals tech-stack key-files
quick-260909-mir 01 complete database
prisma
postgresql
row-level-security
multi-tenancy
nestjs
dkv
phase provides
quick-260909-laa forTenant() binding pattern for user-CRUD services plus the two-client test harness (tenders)
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
stage-3-planning
stage-4-preflight
settings-area-quick-task
tasks commits
3 3
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
created modified
apps/api/src/dkv/dkv.service.spec.ts
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.