docs(quick-260909-mir): Etappe 2 Bereich dkv abgeschlossen, zwei Doku-Luecken behoben
This commit is contained in:
+183
@@ -0,0 +1,183 @@
|
||||
---
|
||||
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.
|
||||
+186
@@ -0,0 +1,186 @@
|
||||
---
|
||||
phase: quick-260909-mir-mandantentrennung-etappe-2-bereich-dkv-a
|
||||
verified: 2026-09-10T09:05:00Z
|
||||
status: gaps_found
|
||||
score: 9/9 must-have truths verified; 2 task-3 deliverable-completeness gaps found
|
||||
covered_files:
|
||||
- ".planning/WINDOWS.md"
|
||||
- ".planning/quick/260909-mir-mandantentrennung-etappe-2-bereich-dkv-a/260909-mir-PLAN.md"
|
||||
- ".planning/quick/260909-mir-mandantentrennung-etappe-2-bereich-dkv-a/260909-mir-SUMMARY.md"
|
||||
- "apps/api/scripts/rls-scratch-check.mjs"
|
||||
- "apps/api/src/dkv/dkv-scheduler.service.ts"
|
||||
- "apps/api/src/dkv/dkv.service.spec.ts"
|
||||
- "apps/api/src/dkv/dkv.service.ts"
|
||||
- "docs/mandantentrennung-etappe2-fehlerrichtung.md"
|
||||
- "docs/mandantentrennung-zugriffsklassifikation.md"
|
||||
covered_digest: "v1:sha256:0689c48c2d159c62763ed9cdb8c9c43ef3c4db6817dc3305e1aaaea807a55407"
|
||||
behavior_unverified: 0
|
||||
overrides_applied: 0
|
||||
re_verification:
|
||||
previous_status: none — initial verification
|
||||
gaps:
|
||||
- truth: "Task 3 <action>: 'Den Abschnitt zum Hintergrunddienst als Falle um den DKV-Planer erweitern, mit der Feststellung, dass er die entartete Form dieser Falle ist' — required by Task 3's own <done> criterion ('der Abschnitt zum Hintergrunddienst als Falle nennt den DKV-Planer als entartete Form')."
|
||||
status: failed
|
||||
reason: "The section '## Der Hintergrunddienst als Falle — drei \"beides\"-Faelle' in docs/mandantentrennung-zugriffsklassifikation.md still lists exactly the same three pre-existing cases (ldap.service.ts, tender-digest.scheduler.ts, tender-matching.service.ts) it listed before this task. No DKV bullet was added, and the heading still says 'drei' (three), not four. grep -in 'dkv|entartet' over that section returns zero matches. This is a required Task 3 deliverable that the interrupted execution never produced, and the orchestrator's after-the-fact recovery (per SUMMARY's own 'Ablauf-Hinweis') only backfilled the overview table and sum row — explicitly not this section."
|
||||
artifacts:
|
||||
- path: "docs/mandantentrennung-zugriffsklassifikation.md"
|
||||
issue: "Missing a fourth bullet under 'Der Hintergrunddienst als Falle' naming DkvSchedulerService/loadAnyActiveConfigForScheduler as the degenerate form of the trap (pulls ONE arbitrary tenant instead of iterating all; the rest get nothing, not too little)."
|
||||
missing:
|
||||
- "Add a DKV bullet to the 'Hintergrunddienst als Falle' section (and update 'drei' to 'vier' in the heading), stating that unlike the other three cases the DKV scheduler does not iterate over all tenants at all — it is the degenerate form of the trap."
|
||||
- truth: "Plan <verification>: 'Der Falsifizierungsnachweis ist je Aufgabe (2 und 3) im SUMMARY ausgeschrieben: welche Bindung probeweise zurueckgebaut wurde, welcher Test daraufhin rot wurde, und dass der Rueckbau zurueckgenommen ist.' (also required per-task in Aufgabe 2's <behavior> and Aufgabe 3's <behavior>.)"
|
||||
status: partial
|
||||
reason: "260909-mir-SUMMARY.md contains no falsification-proof narrative at all for either task (grep -i 'falsifizier|rot ge|rueckbau|revert' over SUMMARY.md returns zero hits). Task 2's proof exists only in commit 222f453's commit-message body ('getConfigForApi's erster gebundener Client probeweise durch this.prisma ersetzt, genau Test 1 wurde rot...'), not in SUMMARY.md. Task 3's proof is documented nowhere — commit 5e8237d has a bare one-line subject with no body, and SUMMARY.md is silent on it. The underlying claim is TRUE (I independently reverted the Task-3 ownership-gate binding in getExportFile and reran the suite: exactly Test 10 went red with the message 'erwaerteter gebundener Aufruf dkvInvoiceHistory.findFirst(tenant=t1) fehlt im Protokoll', no other test failed; restored, 17/17 green again) — but the plan's own required written evidence trail is missing from the one file (SUMMARY.md) the plan designates for it."
|
||||
artifacts:
|
||||
- path: ".planning/quick/260909-mir-mandantentrennung-etappe-2-bereich-dkv-a/260909-mir-SUMMARY.md"
|
||||
issue: "No falsification-proof section for either task, despite the plan requiring it in SUMMARY specifically."
|
||||
missing:
|
||||
- "Add a short section to SUMMARY.md documenting both falsification proofs: which binding was reverted, which named test went red, and that the revert was undone — for Task 2 (already exists in the 222f453 commit message and can be copied) and Task 3 (not documented anywhere; the verifier's own reproduction above can serve as the basis)."
|
||||
---
|
||||
|
||||
# Quick Task 260909-mir — Mandantentrennung Etappe 2, Bereich `dkv` — Verification Report
|
||||
|
||||
**Task goal:** Bind the 21 classified access sites in `apps/api/src/dkv/dkv.service.ts`
|
||||
to a bound client, close the pre-existing cross-tenant export-file gap, create the
|
||||
area's missing test coverage, and carry the single-tenant scheduler start path
|
||||
forward as explicitly named debt.
|
||||
|
||||
**Verified:** 2026-09-10T09:05:00Z
|
||||
**Status:** gaps_found (2 task-3 documentation/completeness gaps — the security- and
|
||||
functionality-relevant substance of the task is verified and holds)
|
||||
|
||||
**Process note acknowledged:** the executor was interrupted mid-Task-3 by a session
|
||||
rate limit; the orchestrator hand-finished only the classification doc's overview
|
||||
table, sum row, and SUMMARY.md. This verification treats every SUMMARY.md claim as
|
||||
unproven until independently checked against the code, per that note's own
|
||||
instruction, and found the two gaps above are exactly the kind of thing that
|
||||
recovery-by-hand would miss.
|
||||
|
||||
## Goal Achievement
|
||||
|
||||
### Observable Truths (must_haves.truths from PLAN frontmatter)
|
||||
|
||||
| # | Truth | Status | Evidence |
|
||||
|---|-------|--------|----------|
|
||||
| 1 | Every access touching one of the three DKV tables on behalf of exactly one tenant runs through a bound client | ✓ VERIFIED | Direct count in `dkv.service.ts`: 23 total `dkvModuleConfig`/`dkvVehicleMaster`/`dkvInvoiceHistory` call sites, 22 via `forTenant(this.prisma, tenantId)`, exactly 1 via `this.prisma` directly (the named exception, see truth 2). Matches SUMMARY's "22 statt 21" claim exactly — independently recomputed, not copied. |
|
||||
| 2 | The one deliberately cross-tenant access (planner start path) is its own named method with its own header comment, not a branch behind an optional parameter | ✓ VERIFIED | `loadAnyActiveConfigForScheduler()` (dkv.service.ts:148-150) is a distinct method; `loadConfig(tenantId)` now takes a mandatory tenantId (line 107). Confirmed by diff against base: `loadConfig(tenantId?)` was split into two methods, not left as an optional-parameter branch. |
|
||||
| 3 | The planner decision is written out, not silently made: code comment, Fehlerrichtung doc, and WINDOWS register all name both states (today arbitrary, future empty) | ✓ VERIFIED | Code: dkv.service.ts:120-146 header comment names both states plus the asymmetry vs. `getAllActiveConfigs`. Doc: `docs/mandantentrennung-etappe2-fehlerrichtung.md` section "(d4) Was dieser Durchlauf bewusst nicht löst" — full three-forms writeup present. Register: `.planning/WINDOWS.md` entry id 21, status `open`, full bilingual-state text — confirmed present via direct read. |
|
||||
| 4 | The error direction of this area is MEASURED, not asserted | ✓ VERIFIED | Independently re-ran `rls-scratch-check.mjs` against the live `tessera-ctl-db-1` container (DB_IP 172.19.0.2, 2026-09-10) — all 41 checks passed (exit 0), including all 9 named dkv checks plus the new concurrency-shape check. Output matches what's pasted into fehlerrichtung.md (d1) verbatim in substance. |
|
||||
| 5 | The ldap-class gap (export file resolved by filename alone) is found and closed via a bound read on invoice history | ✓ VERIFIED | `getExportFile` (dkv.service.ts:691-720): stage 2 is a bound `tenantPrisma.dkvInvoiceHistory.findFirst({where:{tenantId, exportFilename}})`; absence and foreign-ownership collapse to the same `NotFoundException`. Test 9 in the spec proves denial to a second tenant; I independently reverted the binding and watched Test 10 (not 9 — see below) go red for exactly the expected reason, then restored. Controller confirms `tenantId` comes from `req.tenantId` (trusted), not from client input, so this gate cannot be bypassed via a crafted filename. |
|
||||
| 6 | A `dkv` section of the Fehlerrichtung exists, naming the signal per converted path AND this area's own error form (single object silently becomes `null`) | ✓ VERIFIED | `docs/mandantentrennung-etappe2-fehlerrichtung.md` "## Bereich dkv" (d1)-(d5), thorough — signal table, all 7 Befund-K sites named and classified (destructive/silent/misleading), planner decision fully written out. |
|
||||
| 7 | Test coverage was repaired: this area had ZERO test files before; a two-client-proof test file now exists and goes red on an unbound regression — demonstrated by trial revert, not claimed | ✓ VERIFIED (independently reproduced) | `dkv.service.spec.ts` (663 lines, 17 tests) uses the real two-client `__makeBoundClient` harness (ported from groups/tenders pattern), not an identity mock. I reverted the Task-3 ownership-gate binding (`forTenant` → direct `this.prisma`) and reran the suite: exactly Test 10 failed with a named, specific assertion message; all 16 others stayed green; reverted the revert, 17/17 green again. The plan-mandated *written* record of this proof in SUMMARY.md is missing — see Gap 2 below; the underlying truth itself holds. |
|
||||
| 8 | This area has NO tenant-bound transaction — measured, answering the required re-check from `prisma-tenant.extension.ts`'s header comment | ✓ VERIFIED | `grep -rn '\$transaction(' apps/api/src/dkv --include=*.ts \| grep -v spec` → zero hits (exit 1), confirmed directly. `rls-scratch-check.mjs`'s TEIL 3 measurement and the doc's (d1) TEIL 3 write-up both state the same. `withTenantTransaction()` is not imported or used anywhere in dkv.service.ts. |
|
||||
| 9 | Classification doc and `rls-access-inventory.spec.ts` show the same machine-measured status for all three pairs | ✓ VERIFIED | Doc rows: `dkvInvoiceHistory`→gebunden, `dkvVehicleMaster`→gebunden, `dkvModuleConfig`→gemischt — all three confirmed present verbatim. `npm run test -- src/prisma/rls-access-inventory.spec.ts` passes (10/10, part of the full 789-test green run below). |
|
||||
| 10 | 772+ tests and type-check green; scratch tool reports all checks passed; schema/migrations/compose/env files untouched; switch stays OFF | ✓ VERIFIED | Independently re-ran: `npm --prefix apps/api run test` → 789/789 passed, 54 files (up from 772/53 baseline — exactly the delta from the new spec file). `type-check` → exit 0. `rls-scratch-check.mjs` → 41/41, exit 0. `git diff --stat 748f0b5 HEAD` touches exactly 7 files, none of them schema/migration/compose/env files. |
|
||||
|
||||
**Score:** 9/9 must-have truths (all ten frontmatter bullets, numbered 1-10 above per
|
||||
the plan's own list) independently verified as substantively true. 2 deliverable-
|
||||
completeness gaps found at the artifact level (below) that do not falsify any of
|
||||
the above truths but represent incomplete execution of Task 3's own stated contract.
|
||||
|
||||
### Required Artifacts
|
||||
|
||||
| Artifact | Expected | Status | Details |
|
||||
|----------|----------|--------|---------|
|
||||
| `apps/api/scripts/rls-scratch-check.mjs` | `runDkvAreaChecks` section, 9 named checks + concurrency check | ✓ VERIFIED | Present, executed live, all pass (41/41 total). |
|
||||
| `docs/mandantentrennung-etappe2-fehlerrichtung.md` | "## Bereich dkv" section, (d1)-(d5) | ✓ VERIFIED | Present, complete, thorough. |
|
||||
| `apps/api/src/dkv/dkv.service.ts` | All access sites bound except the named exception | ✓ VERIFIED | 22/23 bound, 1 named exception, confirmed by direct grep and read. |
|
||||
| `apps/api/src/dkv/dkv.service.spec.ts` | Two-client-proof test file, area had none before | ✓ VERIFIED | 663 lines, 17 tests, real two-client harness, falsification independently reproduced. |
|
||||
| `apps/api/src/dkv/dkv-scheduler.service.ts` | Updated header comment, calls new named method | ✓ VERIFIED | Header comment present with both states; `onModuleInit` calls `loadAnyActiveConfigForScheduler()`. |
|
||||
| `docs/mandantentrennung-zugriffsklassifikation.md` | Overview row, 3 inventory rows, "Hintergrunddienst als Falle" DKV extension | ⚠️ PARTIAL | Overview row and 3 inventory rows present and correct. The required "Hintergrunddienst als Falle" DKV bullet is MISSING (Gap 1). |
|
||||
| `.planning/WINDOWS.md` | Entry #21, open, deviation, both states | ✓ VERIFIED | Present, id 21, status `open`, full text confirmed. |
|
||||
|
||||
### Key Link Verification
|
||||
|
||||
| From | To | Via | Status | Details |
|
||||
|------|----|----|--------|---------|
|
||||
| Bound client | `tenant_isolation_policy` on the 3 DKV tables | Migration `20260909140000_rls_remaining_tenant_tables`, policies extracted verbatim by the scratch tool | ✓ WIRED | Live-measured against the real container; all 9 named checks pass. |
|
||||
| Planner start path | Unconditioned query, arbitrary-today/empty-after-cutover | `loadAnyActiveConfigForScheduler()` → `this.prisma.dkvModuleConfig.findFirst({select: CONFIG_SAFE_SELECT})` | ✓ WIRED | Confirmed unbound by design; `dkvmoduleconfig-ungebundene-einzelabfrage-liefert-keine-zeile` measures the post-cutover form. |
|
||||
| Export filename | `DkvInvoiceHistory.exportFilename` | Bound `findFirst` in `getExportFile`, second stage after the traversal-pattern check | ✓ WIRED | Confirmed present, falsification-tested (Test 10 goes red on revert), does not break legitimate UI use (both `InvoiceHistoryTable.tsx`/`ExportFileList.tsx` source filenames exclusively from history rows). |
|
||||
| Encrypted inbox credentials | Bound read path in the processing pipeline | `_runPipeline`'s bound `findUnique` | ✓ WIRED | Confirmed bound; test 5/6 in spec cover this. |
|
||||
| Composite uniqueness (tenantId, kennzeichen) | Bound `upsert` in vehicle import | Schema `@@unique([tenantId, kennzeichen])` on `DkvVehicleMaster` | ✓ WIRED | Confirmed directly in `schema.prisma`; scratch check `dkvvehiclemaster-schluessel-traegt-mandant-keine-fremdkollision` passes. |
|
||||
| `rls-access-inventory.spec.ts` | Stand-column of classification doc | Machine comparison | ✓ WIRED | Spec passes (10/10); doc rows match for all 3 dkv pairs. |
|
||||
|
||||
### Behavioral Spot-Checks / Falsification
|
||||
|
||||
| Behavior | Command | Result | Status |
|
||||
|----------|---------|--------|--------|
|
||||
| Task-3 ownership-gate binding actually matters | Reverted `forTenant(this.prisma, tenantId)` → `this.prisma` in `getExportFile`'s stage-2 read, ran `npx vitest run dkv.service.spec.ts -t "Test 10"` | Test 10 failed with `erwarteter gebundener Aufruf dkvInvoiceHistory.findFirst(tenant=t1) fehlt im Protokoll` — exactly the expected, specifically-named failure | ✓ PASS |
|
||||
| Revert cleanly undone | Restored file from backup, reran full spec file | 17/17 passed | ✓ PASS |
|
||||
| Full test suite green | `npm --prefix apps/api run test` | 789/789 passed, 54 files | ✓ PASS |
|
||||
| Type-check clean | `npm --prefix apps/api run type-check` | exit 0 | ✓ PASS |
|
||||
| Scratch tool all-pass against live container | `rls-scratch-check.mjs` against `tessera-ctl-db-1` (172.19.0.2) | 41/41 passed, exit 0 | ✓ PASS |
|
||||
| Schema/migration/compose/env untouched | `git diff --stat 748f0b5 HEAD` | 7 files changed, none in prisma/migrations/compose/env | ✓ PASS |
|
||||
| $-prefixed pseudo-methods explain the 126-vs-127 raw-grep note (see below) | Compared `[a-zA-Z]*` vs `[a-zA-Z]+` grep variants for `this\.prisma\.` across `apps/api/src` | `+`-pattern (matches the actual counting regex in `rls-access-inventory.spec.ts`) gives 123, not 126; the 4-count gap from the naive `*`-pattern (127) is fully explained by 4 `$transaction`/`$queryRaw` lines elsewhere in the repo, unrelated to dkv or to test-file exclusion | ℹ️ INFO — see note below |
|
||||
|
||||
### Anti-Patterns Found
|
||||
|
||||
| File | Line | Pattern | Severity | Impact |
|
||||
|------|------|---------|----------|--------|
|
||||
| `apps/api/src/dkv/dkv.service.ts` | 228-230 | `catch { /* Ignore decrypt errors — will overwrite with whatever was provided */ }` inside `saveConfig`'s credential-preservation branch | ℹ️ INFO (scoped-out by design, not a regression) | This is the exact code the task's threat model (T-MIR-07) and Befund K Stelle 5 describe. The committed fix binds the read AND write of this method to the same tenant client (verified), which closes the specific post-cutover failure mode where an *unbound* read returns nothing due to RLS while the write proceeds. It does NOT change the underlying "swallow decrypt/read failure and continue with possibly-empty values" logic itself — that comment and behavior are byte-identical to the pre-task version (diffed against `748f0b5`). This matches the plan's own explicitly stated scope for T-MIR-07 (binding-consistency, not general error-handling hardening) and the doc's (d3) Stelle 5 write-up says the same thing. Not a plan-goal failure, but worth flagging: a corrupted/undecryptable stored ciphertext (unrelated to tenant binding or to the RLS cutover) would still silently wipe a stored password today, and no test exercises that specific failure path (Test 3 only covers the successful-read case). Recommend a follow-up item, not a blocker for this task. |
|
||||
|
||||
### Requirements Coverage
|
||||
|
||||
| Requirement | Source Plan | Description | Status | Evidence |
|
||||
|-------------|-------------|--------------|--------|----------|
|
||||
| WINDOWS-20 | 260909-mir Plan 01 | Etappe 2 tenant-binding sweep, dkv area | ✓ SATISFIED | 22/23 access sites bound, 1 named exception, verified above. |
|
||||
| ETAPPE-2-DKV | 260909-mir Plan 01 | dkv area conversion, export-file gap closure, test coverage, planner debt marking | ⚠️ PARTIAL | Core substance satisfied; two Task-3 documentation deliverables (Hintergrunddienst-als-Falle extension, SUMMARY falsification-proof write-up) incomplete — see gaps. |
|
||||
|
||||
## Gaps Summary
|
||||
|
||||
Two gaps found, both at the documentation/deliverable-completeness level, both
|
||||
directly attributable to the disclosed mid-Task-3 interruption and partial hand
|
||||
recovery:
|
||||
|
||||
1. **Missing DKV bullet in "Der Hintergrunddienst als Falle" section** of
|
||||
`docs/mandantentrennung-zugriffsklassifikation.md`. Task 3's own `<action>` and
|
||||
`<done>` explicitly require extending this section with the DKV planner as the
|
||||
"entartete Form" (degenerate form) of the trap — it iterates over nothing rather
|
||||
than iterating over all tenants. The section still reads "drei" and lists exactly
|
||||
the same three cases (`ldap.service.ts`, `tender-digest.scheduler.ts`,
|
||||
`tender-matching.service.ts`) that predate this task. Confirmed via direct grep —
|
||||
zero DKV mentions in that section.
|
||||
|
||||
2. **Missing falsification-proof narrative in SUMMARY.md** for both Task 2 and Task
|
||||
3, required by the plan's own `<verification>` section verbatim ("Der
|
||||
Falsifizierungsnachweis ist je Aufgabe (2 und 3) im SUMMARY ausgeschrieben").
|
||||
Task 2's proof exists only in the `222f453` commit-message body, not in
|
||||
SUMMARY.md. Task 3's proof exists nowhere in the repository — `5e8237d` has no
|
||||
commit-message body, and SUMMARY.md's "Ablauf-Hinweis" section, which candidly
|
||||
explains the interruption, does not include it either. I independently performed
|
||||
the equivalent proof for Task 3 (see Behavioral Spot-Checks above) and it holds,
|
||||
but the plan's required written record is absent.
|
||||
|
||||
Neither gap calls into question the security- or functionality-relevant substance
|
||||
of the task: the tenant-binding coverage, the export-file ownership gate, the
|
||||
planner debt-marking (code/doc/register triple), the measured error direction, and
|
||||
the real (falsification-tested) test coverage are all independently verified and
|
||||
hold. Both gaps are small, mechanical documentation additions — not a redo of any
|
||||
functional work.
|
||||
|
||||
### Note on the "126 vs 127" raw-grep discrepancy in the classification doc's sum row
|
||||
|
||||
The sum row states: "ein roher grep über apps/api/src zählt 126 statt 127
|
||||
ungebundene Treffer — die Differenz stammt aus einer geringfügig anderen
|
||||
Ausschlussregel für Testdateien, nicht aus einer offenen Fundstelle." I could not
|
||||
reproduce a 126-vs-127 (single-count) discrepancy with any test-file-exclusion
|
||||
variant I tried (path-based `! -name "*.spec.ts"` vs. content-based `grep -v spec`
|
||||
both gave 127, matching the documented total exactly). What I *could* reproduce is
|
||||
a 4-count discrepancy (123 vs. 127) explained entirely by 4 `$transaction`/
|
||||
`$queryRaw` pseudo-method call sites elsewhere in the repo (`auth.service.ts` x3,
|
||||
`tender-fingerprint-backfill.service.ts` x1) that a naive `[a-zA-Z]*`-based grep
|
||||
miscounts as zero-width matches, while the actual counting regex in
|
||||
`rls-access-inventory.spec.ts` (which uses `[a-zA-Z]+`, requiring at least one
|
||||
letter) correctly excludes them. This is a pre-existing artifact of the headline-
|
||||
count methodology used project-wide, unrelated to dkv and unrelated to test-file
|
||||
exclusion specifically. Since dkv itself has zero `$transaction`/`$queryRaw` calls
|
||||
(confirmed), and the dkv-specific counts I independently verified against the
|
||||
source code are unambiguous and correct (22 bound + 1 exception = 23, matching
|
||||
21 original + 1 new ownership-gate read), this note does not indicate a missed
|
||||
dkv conversion site — but the stated *reason* for the discrepancy in the doc is
|
||||
probably imprecise. Not raised as a gap given it predates this task and doesn't
|
||||
affect the dkv-specific claims, but flagged for awareness.
|
||||
|
||||
---
|
||||
|
||||
_Verified: 2026-09-10T09:05:00Z_
|
||||
_Verifier: Claude (gsd-verifier)_
|
||||
Reference in New Issue
Block a user