187 lines
21 KiB
Markdown
187 lines
21 KiB
Markdown
---
|
||
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)_
|