Files
tessera-ctl/.planning/quick/260910-exd-mandantentrennung-etappe-2-bereich-modul/260910-exd-VERIFICATION.md
T

20 KiB
Raw Blame History

phase, verified, status, score, covered_files, covered_digest, overrides_applied, behavior_unverified
phase verified status score covered_files covered_digest overrides_applied behavior_unverified
quick-260910-exd 2026-09-10T00:00:00Z passed 9/9 must-haves verified
.planning/WINDOWS.md
.planning/quick/260910-exd-mandantentrennung-etappe-2-bereich-modul/260910-exd-PLAN.md
.planning/quick/260910-exd-mandantentrennung-etappe-2-bereich-modul/260910-exd-SUMMARY.md
apps/api/scripts/rls-scratch-check.mjs
apps/api/src/module-registry/module-access.service.spec.ts
apps/api/src/module-registry/module-access.service.ts
apps/api/src/module-registry/module-registry.service.spec.ts
apps/api/src/module-registry/module-registry.service.ts
apps/api/src/module-registry/module.guard.spec.ts
apps/api/src/tenders/tender-scheduler.service.spec.ts
docs/mandantentrennung-etappe2-fehlerrichtung.md
docs/mandantentrennung-zugriffsklassifikation.md
v1:sha256:47e75c553a401d96420242d168f37b0fbc0f89e54dfac382bd3ee80ba1d04a0c 0 0

Quick Task 260910-exd Verification: Mandantentrennung Etappe 2, Bereich module-registry

Task Goal: Bind the tenant-bound access sites in apps/api/src/module-registry/, leave the platform-wide module catalogue deliberately unbound with the CORRECTED reason, create the missing spec coverage, record the absent denial-signal three ways, and leave the classification document's five hand-maintained sections in sync.

Verified: 2026-09-10 Status: passed Commits reviewed: 7d45e2f, 3df7268, 9c0eefe (base a2516a9)

This is a re-verification-grade, adversarial re-audit against the codebase — not a re-read of SUMMARY.md. Every claim below was independently reproduced (live DB run, source greps, recomputed class-distribution table, commit-level diffs) rather than accepted from the SUMMARY.

Priority Findings (per orchestrator's numbered scrutiny list)

1. THE PRIORITY ITEM — tender-scheduler.service.spec.ts identity mock: LEGITIMATE, not a regression

Verified by reading the file and its neighbors directly:

  • tender-scheduler.service.spec.ts mocks forTenant to identity (vi.fn((p) => p)) — this is true, and by itself would be the exact defect this effort documented in ldap.
  • But the binding for the method this file exercises (ModuleRegistryService.activateForTenant) is proven elsewhere, and proven rigorously. apps/api/src/module-registry/module-registry.service.spec.ts (new file, 344 lines, 16 it() cases) uses a genuine two-client bound-call-log proof (__makeBoundClient, a second, distinguishable wrapper object that logs every call routed through it) — not an identity mock. Its activateForTenant describe block (lines 182–207) asserts expectBoundCall(prisma, 't1', 'tenantModuleActivation', 'upsert') — this assertion is false (test fails) if the binding is removed. Confirmed live: the SUMMARY's claimed falsification proof for this exact method (Task 3, deactivateForTenant's update call) is reproduced verbatim in docs/mandantentrennung-etappe2-fehlerrichtung.md (see below) with a concrete red message, not just a commit-log claim.
  • Checked whether "the exact ldap.service.spec.ts convention" claim holds up: ldap.service.spec.ts does carry a file-level identity mock at the top (line 56–57, forTenant: vi.fn((p) => p)) and a separate, dedicated binding-proof block later in the same file (from ~line 2300) that reconfigures the forTenant spy per-test and asserts expect(forTenant).toHaveBeenCalledWith(...) for listGroups, searchUsers, etc. ldap-config.service.spec.ts follows the identical two-part pattern (identity mock at top, line 9–10; dedicated "Bindung an forTenant()" describe block at line 197 asserting toHaveBeenCalledWith). So the convention is real, not a misreading. tender-scheduler.service.spec.ts only needed the "identity mock" half of that convention, because — unlike ldap.service.spec.ts testing itself — it doesn't test ModuleRegistryService's binding at all; it tests TenderSchedulerService's poll-once-fan-out-many behavior using the real ModuleRegistryService as an unmocked collaborator. The binding proof for ModuleRegistryService.activateForTenant correctly lives in module-registry.service.spec.ts, which is the file that owns that method.
  • Verified no other unmocked caller of new ModuleRegistryService(...) exists (grep -rn "new ModuleRegistryService" apps/api/src → only the DI module and this one spec file).

Conclusion: legitimate, not a regression in disguise.

2. The Task-2/Task-3 classification-doc split: HONEST ordering consequence, not gaming

git show 3df7268 -- docs/mandantentrennung-zugriffsklassifikation.md shows the Task-2 commit touched exactly two lines of the classification doc: the Stand column (ungebunden→gebunden) and a one-sentence addition to the existing Begründung for module-access.service.ts/moduleGrant and /tenantModuleActivation — nothing else. The overview line, sum line, class-distribution table, and background-service-trap section were untouched in that commit and only changed in Task 3's commit (9c0eefe), exactly as the plan required. This is the minimum edit forced by Task 2's own verify gate (npm --prefix apps/api run test includes rls-access-inventory.spec.ts, which checks the doc against the live source on every run) — not scope creep or a doc bent to make a check pass.

3. The corrected premise (Befund E) is preserved as corrected

Both module-access.service.ts (lines 101–107) and module-registry.service.ts (multiple locations) carry the comment in the required two-part form: MEASUREMENT ("die Tabelle traegt heute keinen Zeilenschutz, eine Bindung waere heute wirkungslos, nicht katastrophal") plus CONDITION ("katastrophal wuerde sie erst, WENN Etappe 3 dieser Tabelle eine Regel gibt"). The classification doc rows (lines 301, 304) and the critique doc's (m4)/(m1) sections repeat this exact framing. No occurrence of the original, now-falsified claim ("a binding would make the catalog invisible today") was found anywhere in the diff.

4. Coverage and the bind/don't-bind line — independently counted

this.prisma.module    → module-access.service.ts: 1    (unbound)
this.prisma.module    → module-registry.service.ts: 6  (unbound)
tenantPrisma.*        → module-access.service.ts: 5     (bound: 2× moduleGrant, 3× tenantModuleActivation)
tenantPrisma.*        → module-registry.service.ts: 5   (bound: 5× tenantModuleActivation)

7 unbound / 10 bound — matches the SUMMARY's claim exactly, and both numbers were counted fresh from the current source, not copied from any document. Raw-hit total (17) also matches Befund A. grep -rn 'tenantPrisma\.module\.' (the forbidden literal) returns zero hits — the catalog was not accidentally bound.

5. Default-closed cannot fail open

module-access.service.spec.ts line 354 ("Vorgabezustand bleibt geschlossen und ueberlebt die Bindung") pins that with grantedIds.length === 0 the intersection query (tenantModuleActivation) is never called — the bound-call log is asserted empty for that model in that path. The ADMIN/SUPER_ADMIN short-circuit condition (role === 'ADMIN' || role === 'SUPER_ADMIN') is unchanged from the pre-existing code — confirmed by reading module-access.service.ts line 53 and the git diff, which shows no changes to the role-check line.

6. The absent denial signal — recorded three ways, all confirmed

  1. Critique text, unvarnished: docs/mandantentrennung-etappe2-fehlerrichtung.md (m3), "Die Antwort lautet keines — schlicht, ohne Beschönigung," with the three identical-looking spots named (same ForbiddenException message, same empty 200 list, no log line) and the added observation that the affected user has a ready, false explanation.
  2. Test case: module.guard.spec.ts lines 137–184, two guard instances (genuinely-no-grant vs. query-found-nothing) held against each other, asserting identical exception messages, with a header comment stating the test is supposed to go red once a distinguishing signal is added.
  3. WINDOWS #23: present in both the table (line 40) and the JSON block (lines 309–319) of .planning/WINDOWS.md, status: open, naming the concrete Etappe-4 preflight (rls-preflight.mjs: active activation rows present but resolution empty for a known admin) and the reasoned rejection of a runtime warning. Table-row count (23) matches JSON-entry count (23) — internally consistent.

7. No safeguard that guards nothing

grep -rn "P2002" apps/api/src/module-registry returns zero hits. No unique-constraint-collision translation was added — consistent with Befund H/pruefung 12/13 (both affected unique keys lead with tenantId, so the tenders/user collision chain structurally cannot occur here).

8. The measurements are committed — reproduced live against the real container

Independently re-ran the tool against the live tessera-ctl-db-1 container (address resolved fresh via docker inspect, 172.19.0.2 at verification time — not copied from any document):

$ TESSERA_SCRATCH_ADMIN_URL="postgresql://tessera:tessera_dev@172.19.0.2:5432/postgres" \
  node apps/api/scripts/rls-scratch-check.mjs
...
Alle 66 Pruefungen bestanden.

All 13 named module-registry checks (modulegrant-ungebunden-null-zeilen through freigabe-eindeutigkeitsindizes-fuehren-mit-der-mandantenkennung) passed with output byte-identical to what's recorded in the critique document's (m1) section. 66 = 53 baseline + 13 new — confirmed.

9. The five hand-maintained document sections — recomputed independently

All five confirmed by independent recomputation, not by trusting the document:

  • Bestandsaufnahme rows: all five (file, model) pairs for module-registry present with the correct Stand (3 gebunden, 2 ungebunden) and MEASUREMENT+CONDITION-separated reasoning.
  • Übersichtszeile: module-registry | 7 | 10 — matches the independently-counted raw hits.
  • Summenzeile: independently summed all 12 area rows — ungebunden 36+0+4+1+8+7+13+8+12+8+7+4 = 108, gebunden 26+31+26+22+14+10+0+5+0+0+0+0 = 134 — matches the document's **108**/**134** exactly.
  • Klassen-Verteilung: ran an independent awk pass over every apps/api/src/... Bestandsaufnahme row and recomputed class counts from scratch: muss-mandantengebunden: 31, keine-mandantengebundene-tabelle: 17, beides: 13, bewusst-uebergreifend: 2, TOTAL: 63 — matches the document's table and its "63 Paare" heading exactly.
  • Hintergrunddienst-als-Falle: heading says "fünf Fälle"; four are formatted as - **\file`** bullets (ldap.service.ts, tender-digest.scheduler.ts, tender-matching.service.ts, admin-seed.service.ts) and the fifth (dkv-scheduler.service.ts) is deliberately formatted differently ("anderer Bauart") — count of 5 confirmed by inspection. The section explicitly states module-registry adds **no** sixth case, with a measured reason (seedModule` writes without tenant context but doesn't iterate per-tenant, so it lacks the read-across/bind-within-loop shape).

10. Falsification proofs — both present in the document, not just commit messages

Confirmed both are written into docs/mandantentrennung-etappe2-fehlerrichtung.md's "Nachtrag (260910-exd, Aufgabe 3)" section (lines 1515–1533), each with the exact test name and exact failure message:

  • Task 2: group-path rollback → module-access.service.spec.ts test "USER-Zweig bindet BEIDE..." went red with expected 1 to be 2; reverted, 23/23 green again.
  • Task 3: deactivateForTenant write rollback → module-registry.service.spec.ts test "bindet beide Aktivierungszugriffe..." went red with the bound-call-log assertion message quoted verbatim; reverted, 16/16 green again.

11. Constraints held

  • No Prisma schema/migration changes: git diff --name-only a2516a9..9c0eefe -- apps/api/prisma → empty.
  • No compose/env changes: none of docker-compose*.yml/.env*.example appear in the diff file list.
  • assertTargetBelongsToTenant still present and called twice in apps/api/src/groups/module-grants.service.ts (lines 46, 103, 242) — unchanged.
  • No dashboard file touched: git diff --name-only a2516a9..9c0eefe -- apps/api/src/dashboard → empty.
  • T-JTS-02/T-JTS-03 recorded as open, not fixed, in (m4) of the critique doc, with explicit note that the write-side check in module-grants.service.ts remains the only protection until Etappe 4.
  • Full diff file list (10 files) matches exactly what the plan's files_modified frontmatter and the task-level file scopes declared — no unexpected files touched.

Goal Achievement

Observable Truths

# Truth Status Evidence
1 Both stages (activation, grant) bound; no method binds one stage and leaves the other unbound; no method mixes two tenants in one resolution ✓ VERIFIED module-access.service.ts/module-registry.service.ts read in full; two-client bound-call-log tests pin single-tenant-per-resolution property (e.g. "Rollen-Kurzschluss waechst..." test)
2 Catalog stays unbound with MEASURED (not assumed) reasoning, condition stated as condition ✓ VERIFIED Comments in both service files + classification doc rows 301/304 use the exact measurement+condition framing; live DB run confirms module-tabelle-traegt-keinen-zeilenschutz and katalog-bindung-aendert-heute-nichts-an-der-ergebnismenge
3 Reverse error direction measured against the real delivered rule, per-path signal described ✓ VERIFIED (m2) signal table in critique doc covers all named paths incl. the two deliberately-unbound catalog paths as boundary
4 Absence of a distinguishing denial signal explicitly handled, three ways ✓ VERIFIED Critique text (m3), module.guard.spec.ts test, WINDOWS #23 — all three confirmed present and consistent
5 Follows module-grants.service.ts convention: one client per method named tenantPrisma, existing where-filters retained, app-layer check not replaced by DB ✓ VERIFIED Code read directly; assertTargetBelongsToTenant untouched; where-filters retained (e.g. getAccessibleModuleIds still filters tenantId in every query)
6 Test coverage established BEFORE conversion; forgotten binding call goes red ✓ VERIFIED Two falsification proofs reproduced in the doc with exact red messages; two-client proof (not identity mock) used throughout
7 Both false header comments corrected at the measurement ✓ VERIFIED isModuleActive/findActiveForTenant comments in module-registry.service.ts now state "Richtiggestellt... Gemessen... kein Aufrufer" with reference to TEIL 3
8 Baseline held: 810+ tests green, type-check clean, scratch tool all-pass, at end of every task ✓ VERIFIED Orchestrator confirmed 833/833 green (56 files, baseline 810/55), type-check exit 0; independently reran scratch tool live: 66/66
9 Switch stays off: DATABASE_URL on tessera role, schema/migrations unchanged, T-JTS-02/03 recorded not fixed ✓ VERIFIED No prisma/migration diff; T-JTS-02/03 explicitly recorded as open in (m4)

Score: 9/9 truths verified, 0 present-but-behavior-unverified.

Required Artifacts

Artifact Expected Status Details
apps/api/scripts/rls-scratch-check.mjs 8th section, 13 named checks ✓ VERIFIED runModuleRegistryAreaChecks present, called between runUserAreaChecks/runTransactionShapeMeasurement; 66/66 pass live
docs/mandantentrennung-etappe2-fehlerrichtung.md ## Bereich module-registry, (m1)-(m5) ✓ VERIFIED All 5 subsections present with required content; Nachtrag with both falsification proofs
apps/api/src/module-registry/module-access.service.ts 3 activation + 2 grant accesses bound, catalog unbound ✓ VERIFIED 5 bound call sites, 1 unbound, matches
apps/api/src/module-registry/module-access.service.spec.ts two-client proof, all cases retained ✓ VERIFIED 23 cases total, __makeBoundClient two-client log used
apps/api/src/module-registry/module.guard.spec.ts denial-signal absence case ✓ VERIFIED Present, lines 137-184
apps/api/src/module-registry/module-registry.service.ts 5 activation accesses bound, 6 catalog unbound, both comments corrected ✓ VERIFIED Matches exactly
apps/api/src/module-registry/module-registry.service.spec.ts NEW file ✓ VERIFIED 344 lines, 16 cases, two-client proof
docs/mandantentrennung-zugriffsklassifikation.md 5 sections reconciled ✓ VERIFIED Independently recomputed, matches exactly
.planning/WINDOWS.md open entry, table + JSON ✓ VERIFIED #23 present both places, internally consistent (23=23)

Behavioral Spot-Checks

Behavior Command Result Status
Scratch tool passes against live DB node apps/api/scripts/rls-scratch-check.mjs (against 172.19.0.2) "Alle 66 Pruefungen bestanden." ✓ PASS
activateForTenant binding provable Read module-registry.service.spec.ts line 193 assertion expectBoundCall(..., 'tenantModuleActivation', 'upsert') ✓ PASS
No forbidden literal tenantPrisma.module. grep -rn 'tenantPrisma\.module\.' apps/api/src/module-registry 0 hits ✓ PASS
No debt markers in modified files grep -nE "TBD|FIXME|XXX|TODO|HACK|PLACEHOLDER" across all 10 diffed files 0 hits ✓ PASS
Constraint boundaries held git diff --name-only a2516a9..9c0eefe exactly the 10 expected files ✓ PASS

Anti-Patterns Found

None. No debt markers, no stub returns, no hardcoded empty data flowing to output in the reviewed files.

Minor Informational Notes (not gaps)

  • SUMMARY.md states "module-access.service.spec.ts rebuilt... plus 7 new binding cases." Independent count of the file's final "Bindung an forTenant() (260910-exd)" describe block shows 6 new cases (lines 327, 336, 354, 370, 387, 400), matching the plan's own behavior spec (6 bullet points). This is a minor off-by-one in the SUMMARY narrative, not a must-have and not affecting any verified artifact or test outcome — recorded here for completeness, not as a gap.

Requirements Coverage

Requirement Description Status Evidence
WINDOWS-18 Broken-windows tracking for RLS bypass deviations ✓ SATISFIED WINDOWS #23 added
ETAPPE-2-MODULE-REGISTRY Bind module-registry area per Etappe 2 pattern ✓ SATISFIED All 17 raw accesses decided (10 bound, 7 unbound-with-reason)

(These are quick-task-local requirement tags, not entries in .planning/REQUIREMENTS.md — expected for a quick task, not an orphan.)

Human Verification Required

None. All must-haves were verifiable programmatically (source review, live DB run, independent recomputation of hand-maintained document sections).

Gaps Summary

No gaps found. This is one of the most rigorously self-falsifying deliveries in this series: the priority-scrutiny item (the tender-scheduler identity mock) held up under adversarial re-check because the actual binding proof lives in the correct file with a genuine two-client log, the Task-2/Task-3 document split was the minimum forced edit (verified via commit-level diff, not narrative), the corrected catalog-binding premise is preserved as a measurement+condition pair everywhere it appears, all coverage counts were independently reproduced from source (not copied from the SUMMARY), the scratch tool was re-run live against the real database and matched the committed measurement byte-for-byte, and both falsification proofs are recorded in the permanent document with exact test names and failure messages, not left only in commit messages.


Verified: 2026-09-10 Verifier: Claude (gsd-verifier)