docs(quick-260910-exd): Etappe 2 Bereich module-registry abgeschlossen und verifiziert
This commit is contained in:
+180
@@ -0,0 +1,180 @@
|
||||
---
|
||||
phase: quick-260910-exd
|
||||
plan: 01
|
||||
subsystem: database
|
||||
tags: [prisma, postgresql, row-level-security, multi-tenancy, nestjs, vitest]
|
||||
|
||||
requires:
|
||||
- phase: quick-260909-jts
|
||||
provides: forTenant()/withTenantTransaction(), the Group/GroupMembership/ModuleGrant/TenantModuleActivation RLS policies, and module-grants.service.ts as the reference pattern for two-client bound tests
|
||||
provides:
|
||||
- ModuleAccessService (getAccessibleModuleIds, findAccessibleModules, getCatalogFlags) bound to forTenant() on every mandate-scoped access
|
||||
- ModuleRegistryService (findActiveForTenant, activateForTenant, deactivateForTenant, isModuleActive) bound to forTenant()
|
||||
- A new test file for module-registry.service.ts (previously had none) covering 11 of the area's 17 raw accesses, including every write path
|
||||
- Both falsely-worded header comments from Befund G corrected
|
||||
- docs/mandantentrennung-zugriffsklassifikation.md fully reconciled (5 hand-maintained sections) at 108 ungebunden / 134 gebunden
|
||||
- WINDOWS #23: the recorded absence of a signal distinguishing "genuinely no grant" from "query found nothing" in the module-access hot path
|
||||
affects: [module-registry, dashboard (picks up the binding transitively via getAccessibleModuleIds), etappe-3-rls-policy-tightening, etappe-4-cutover]
|
||||
|
||||
actuals:
|
||||
tokens: 25541
|
||||
tasks: 3
|
||||
commits: 3
|
||||
plan_head_before: a2516a9
|
||||
|
||||
tech-stack:
|
||||
added: []
|
||||
patterns:
|
||||
- "EIN gebundener Klient je Methode unter dem Namen tenantPrisma, existing where-filters kept as a second net (T-JTS-02/T-JTS-03 precedent from module-grants.service.ts)"
|
||||
- "Nested method calls (getCatalogFlags calling getAccessibleModuleIds) each create their own forTenant() client — bound clients are never passed between methods"
|
||||
- "Catalog access (Module model) stays deliberately unbound with a comment separating today's measurement (no RLS on the table) from the future condition (Etappe 3 adding a policy would make binding catastrophic)"
|
||||
|
||||
key-files:
|
||||
created:
|
||||
- apps/api/src/module-registry/module-registry.service.spec.ts
|
||||
modified:
|
||||
- apps/api/src/module-registry/module-access.service.ts
|
||||
- apps/api/src/module-registry/module-access.service.spec.ts
|
||||
- apps/api/src/module-registry/module.guard.spec.ts
|
||||
- apps/api/src/module-registry/module-registry.service.ts
|
||||
- apps/api/src/tenders/tender-scheduler.service.spec.ts
|
||||
- apps/api/scripts/rls-scratch-check.mjs
|
||||
- docs/mandantentrennung-etappe2-fehlerrichtung.md
|
||||
- docs/mandantentrennung-zugriffsklassifikation.md
|
||||
- .planning/WINDOWS.md
|
||||
|
||||
key-decisions:
|
||||
- "Module catalog binding decision separates MEASUREMENT (no RLS on Module today, so binding it would be inert) from CONDITION (it becomes catastrophic once Etappe 3 adds a policy) — corrects the planning brief's premise that binding would be catastrophic today"
|
||||
- "The one absent signal (genuinely-no-grant vs query-found-nothing) is recorded as unsolved, not runtime-warned-around — same reasoning as getAllActiveConfigs in ldap and the five spots in tenders: a warning on a routine empty-result path is noise, not signal"
|
||||
- "A cross-area test break (tender-scheduler.service.spec.ts, unmocked ModuleRegistryService against a $extends-less fake) was fixed with the same identity-mock convention already used in ldap.service.spec.ts, not by reshaping the production code"
|
||||
|
||||
requirements-completed: [WINDOWS-18, ETAPPE-2-MODULE-REGISTRY]
|
||||
|
||||
coverage: []
|
||||
|
||||
duration: ~75min
|
||||
completed: 2026-09-10
|
||||
status: complete
|
||||
---
|
||||
|
||||
# Quick Task 260910-exd: Etappe 2, Bereich module-registry Summary
|
||||
|
||||
**The two-stage module-access decision path (TenantModuleActivation + ModuleGrant) is now fully forTenant()-bound in both services of the area, with a machine-verified measurement that the platform module catalog stays deliberately unbound and a recorded absence of a signal distinguishing a real access denial from a silently-broken query.**
|
||||
|
||||
## Performance
|
||||
|
||||
- **Duration:** ~75 min
|
||||
- **Tasks:** 3
|
||||
- **Files modified:** 9 (1 created, 8 modified)
|
||||
|
||||
## Accomplishments
|
||||
|
||||
- `ModuleAccessService.getAccessibleModuleIds` (ADMIN/SUPER_ADMIN short-circuit, direct grant path, group grant path, D-02 intersection) and `getCatalogFlags`'s own activation read now run through `forTenant()`, one client per method — existing `tenantId` where-filters remain as the second net (T-JTS-02/T-JTS-03).
|
||||
- `ModuleRegistryService.findActiveForTenant`, `activateForTenant`, `deactivateForTenant` (both activation accesses over one client), and `isModuleActive` are bound the same way; the six catalog accesses (`findAll`, `findBySlug`, both existence checks, `isModuleActive`'s catalog lookup, `seedModule`) stay deliberately unbound.
|
||||
- `module-registry.service.spec.ts` created from scratch — the file previously had zero tests despite holding 11 of the area's 17 raw accesses and every write path. Covers both the loud direction (deactivating an unactivated module throws) and the silent direction (`isModuleActive` without an activation returns `false`) as named, deliberately-preserved properties.
|
||||
- `module-access.service.spec.ts` rebuilt onto the two-client proof (`__makeBoundClient`, bound-call log) with a watchdog that fails if the catalog access ever appears in the bound-call log.
|
||||
- One new case in `module.guard.spec.ts` pins down that "genuinely no grant" and "the resolution found nothing" produce the identical `ForbiddenException` message today — the machine record of the area's central finding.
|
||||
- `isModuleActive`'s header comment corrected: it claimed `ModuleGuard` calls it; measured zero callers exist (the guard uses `findBySlug` + `getAccessibleModuleIds` instead).
|
||||
- `rls-scratch-check.mjs` gained an eighth section (`runModuleRegistryAreaChecks`, 13 named checks) run against the real, delivered migrations — 66/66 checks pass. The measurement that the module catalog is genuinely unprotected today (no RLS, `pg_class.relrowsecurity = false`) and that the activation/grant unique keys structurally cannot repeat the tenders/user visible-row-collision chain (both lead with `tenantId`) is now committed evidence, not an assertion.
|
||||
- `docs/mandantentrennung-zugriffsklassifikation.md` fully reconciled: all five hand-maintained sections (inventory rows, overview line 7/10, sum line 108/134, class distribution unchanged at 63 pairs, background-service-trap section recording the absence of a sixth case) — all machine-gated against the source.
|
||||
- `docs/mandantentrennung-etappe2-fehlerrichtung.md` gained the `## Bereich module-registry` section (m1–m5) plus a Task-3 addendum naming both falsification proofs with test name and failure message.
|
||||
- `.planning/WINDOWS.md` #23 records the missing distinguishing signal as an open deviation, with the concrete Etappe-4 preflight check and the reasoned rejection of a runtime warning.
|
||||
|
||||
## Task Commits
|
||||
|
||||
Each task was committed atomically:
|
||||
|
||||
1. **Aufgabe 1: measure the error direction, no production code** — `7d45e2f` (test)
|
||||
2. **Aufgabe 2: bind ModuleAccessService** — `3df7268` (feat)
|
||||
3. **Aufgabe 3: bind ModuleRegistryService, finish the classification doc** — `9c0eefe` (feat)
|
||||
|
||||
_Note: no separate docs-only metadata commit yet — the orchestrator adds that after this SUMMARY._
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
- `apps/api/src/module-registry/module-access.service.ts` — `getAccessibleModuleIds`/`getCatalogFlags` bound to `forTenant()`, catalog access left unbound with a measurement+condition comment
|
||||
- `apps/api/src/module-registry/module-access.service.spec.ts` — rebuilt onto the two-client bound-call-log proof, all 15 pre-existing cases retained plus 6 new binding cases (die Zahl stand hier zunaechst als 7; vom Verifizierer nachgezaehlt und berichtigt — 6 entspricht den im Plan benannten sechs Verhaltensweisen. Dieselbe Fehlerart wie in 260909-laa, wo eine Zusammenfassung drei Uebersetzungen behauptete und zwei geliefert waren)
|
||||
- `apps/api/src/module-registry/module.guard.spec.ts` — one new case pinning the absence of a distinguishing signal
|
||||
- `apps/api/src/module-registry/module-registry.service.ts` — `findActiveForTenant`/`activateForTenant`/`deactivateForTenant`/`isModuleActive` bound; `isModuleActive`'s header comment corrected
|
||||
- `apps/api/src/module-registry/module-registry.service.spec.ts` — new, 16 cases
|
||||
- `apps/api/src/tenders/tender-scheduler.service.spec.ts` — `forTenant()` mocked to identity (same convention as `ldap.service.spec.ts`) to fix a cross-area break caused by the `activateForTenant` conversion
|
||||
- `apps/api/scripts/rls-scratch-check.mjs` — eighth section `runModuleRegistryAreaChecks`, 13 named checks
|
||||
- `docs/mandantentrennung-etappe2-fehlerrichtung.md` — new `## Bereich module-registry` section (m1–m5) plus Task-3 addendum
|
||||
- `docs/mandantentrennung-zugriffsklassifikation.md` — 5 inventory rows updated/reconciled, overview/sum/class-distribution/background-service-trap sections all reconciled
|
||||
- `.planning/WINDOWS.md` — new open entry #23
|
||||
|
||||
## Decisions Made
|
||||
|
||||
- **Module catalog binding stays a two-part statement, not a single claim.** The planning brief's premise ("binding the catalog would be catastrophic today") was measured and found FALSE — `Module` carries no RLS policy at all, so binding it today would be inert. The action (don't bind it) is unchanged, but the written reason now separates the measurement (no policy today) from the condition (it becomes catastrophic once Etappe 3 gives the table a policy).
|
||||
- **The absent distinguishing signal is recorded, not engineered around.** There is no way today to tell "the user genuinely has no grant" from "a query silently found nothing" — both produce the identical 403, the identical empty 200 list, and no log line. A runtime warning at these spots was considered and rejected (same reasoning as `getAllActiveConfigs` in `ldap` and the five spots in `tenders`): a warning on a routine "no access" case would be constant noise on a fresh install.
|
||||
- **Cross-area test break fixed with the existing convention, not a code reshape.** `tender-scheduler.service.spec.ts` drives the real `ModuleRegistryService` against a hand-rolled fake without `$extends`. Rather than adding `$extends`/`$transaction` support to that fake or weakening the production binding, `forTenant` was mocked to identity in that one file — the exact pattern `ldap.service.spec.ts` already established for tests that don't care about RLS binding mechanics.
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
### Auto-fixed Issues
|
||||
|
||||
**1. [Rule 1/3 - Blocking bug in a dependent area] `tender-scheduler.service.spec.ts` broke after `ModuleRegistryService.activateForTenant` started calling `forTenant()`**
|
||||
- **Found during:** Task 3 (full-suite green check after converting `module-registry.service.ts`)
|
||||
- **Issue:** This spec instantiates the real, unmocked `ModuleRegistryService` against a hand-rolled fake prisma object that has no `$extends` method (by design — it predates any RLS binding in this service). `forTenant()` calls `prisma.$extends(...)`, so the test failed with `prisma.$extends is not a function`.
|
||||
- **Fix:** Mocked `forTenant` to an identity function (`vi.fn((p) => p)`) in this one file, matching the exact convention `ldap.service.spec.ts` already uses for the same reason (the test verifies poll-once-fan-out-many scheduler invariants, not RLS binding mechanics).
|
||||
- **Files modified:** `apps/api/src/tenders/tender-scheduler.service.spec.ts`
|
||||
- **Verification:** `npm --prefix apps/api run test -- src/tenders/tender-scheduler.service.spec.ts` green (4/4); full suite green afterward.
|
||||
- **Committed in:** `9c0eefe` (Task 3 commit)
|
||||
|
||||
**2. [Rule 3 - Blocking, full-suite gate] `rls-access-inventory.spec.ts` went red immediately after binding `module-access.service.ts` in Task 2, before Task 3 (which owns the classification doc) had run**
|
||||
- **Found during:** Task 2 (the plan's own verify block runs the full `npm --prefix apps/api run test` suite, which includes this cross-check between the classification doc and the source)
|
||||
- **Issue:** `module-access.service.ts`'s `moduleGrant` and `tenantModuleActivation` rows in `docs/mandantentrennung-zugriffsklassifikation.md` still said `Stand: ungebunden` the moment the service code became bound — the doc and source diverged mid-plan, and Task 2's own verify gate (full test suite) demanded they match.
|
||||
- **Fix:** Updated only the `Stand` column (and a one-sentence addition to the existing Begründung) for those two specific rows — not the overview line, sum line, class distribution, or background-service-trap section, all of which stayed correctly assigned to Task 3's full reconciliation pass.
|
||||
- **Files modified:** `docs/mandantentrennung-zugriffsklassifikation.md`
|
||||
- **Verification:** `npm --prefix apps/api run test -- src/prisma/rls-access-inventory.spec.ts` green; full suite green (817/817) at the end of Task 2.
|
||||
- **Committed in:** `3df7268` (Task 2 commit)
|
||||
|
||||
---
|
||||
|
||||
**Total deviations:** 2 auto-fixed (1 cross-area blocking bug, 1 blocking full-suite-gate correction split across tasks by necessity)
|
||||
**Impact on plan:** Both were forced by the plan's own verify gates (full test suite must stay green after every task) rather than scope creep. No production behavior outside the two converted services was changed; the tender-scheduler fix only affects test wiring.
|
||||
|
||||
## Falsification Proofs (required, per plan)
|
||||
|
||||
**Task 2 — group path binding rolled back and restored:**
|
||||
- Rolled `tenantPrisma.moduleGrant.findMany(...)` (group path inside `getAccessibleModuleIds`) back to `this.prisma.moduleGrant.findMany(...)`.
|
||||
- `module-access.service.spec.ts` test **"ModuleAccessService — Bindung an forTenant() (260910-exd) > USER-Zweig bindet BEIDE Freigabe-Lesezugriffe (Direktweg und Gruppenweg) UND den Schnittmengen-Lesezugriff an DIESELBE Mandantenkennung"** went red: `AssertionError: expected 1 to be 2`.
|
||||
- Reverted the rollback (file byte-identical to before the probe, confirmed via `diff`); the same test run went green again (23/23).
|
||||
|
||||
**Task 3 — deactivation write binding rolled back and restored:**
|
||||
- Rolled `tenantPrisma.tenantModuleActivation.update(...)` (in `deactivateForTenant`) back to `this.prisma.tenantModuleActivation.update(...)`.
|
||||
- `module-registry.service.spec.ts` test **"ModuleRegistryService.deactivateForTenant > bindet beide Aktivierungszugriffe (Lesen, Schreiben) an denselben Mandanten, über einen Klienten"** went red: `erwarteter gebundener Aufruf tenantModuleActivation.update(tenant=t1) fehlt im Protokoll: [{"tenantId":"t1","model":"tenantModuleActivation","method":"findUnique"}]: expected false to be true`.
|
||||
- Reverted the rollback (file byte-identical to before the probe, confirmed via `diff`); the same test run went green again (16/16).
|
||||
|
||||
## Issues Encountered
|
||||
|
||||
None beyond the two deviations above — both handled inline without blocking task progress.
|
||||
|
||||
## User Setup Required
|
||||
|
||||
None — no external service configuration required. `DATABASE_URL` remains unchanged, pointed at the `tessera` role with `BYPASSRLS`. The switch stays off; this was measurement and application-layer binding work only.
|
||||
|
||||
## Self-Check
|
||||
|
||||
- `apps/api/src/module-registry/module-registry.service.spec.ts` — FOUND
|
||||
- `apps/api/src/module-registry/module-access.service.spec.ts` — FOUND (modified)
|
||||
- `apps/api/src/tenders/tender-scheduler.service.spec.ts` — FOUND (modified)
|
||||
- Commit `7d45e2f` — FOUND in `git log`
|
||||
- Commit `3df7268` — FOUND in `git log`
|
||||
- Commit `9c0eefe` — FOUND in `git log`
|
||||
- `npm --prefix apps/api run test` — 833/833 green, 56 files (was 810/55 at plan start)
|
||||
- `npm --prefix apps/api run type-check` — clean
|
||||
- `node apps/api/scripts/rls-scratch-check.mjs` — 66/66 checks passed, exit 0
|
||||
- WINDOWS #23 present in both the table and the JSON block of `.planning/WINDOWS.md`
|
||||
|
||||
## Next Phase Readiness
|
||||
|
||||
Five bereiche remain in Etappe 2, by today's raw-hit count: `dashboard` (13), `calendar` (12), `tenant` (8), `favorites` (7), `auth` (5, gemischt), `settings` (4). Two order conditions carried forward from this run: `dashboard`'s module-access filter is already correct because the binding sits in `ModuleAccessService` (no file in `dashboard` needs touching for that); `settings` remains the open order condition for `tenders` (SMTP credentials) and is the smallest remaining area at 4 raw hits.
|
||||
|
||||
## Self-Check: PASSED
|
||||
|
||||
All created/modified files and all three task commits verified present via `[ -f ... ]` and `git log --oneline --all | grep`; no missing items.
|
||||
|
||||
---
|
||||
*Phase: quick-260910-exd*
|
||||
*Completed: 2026-09-10*
|
||||
+275
@@ -0,0 +1,275 @@
|
||||
---
|
||||
phase: quick-260910-exd
|
||||
verified: 2026-09-10T00:00:00Z
|
||||
status: passed
|
||||
score: 9/9 must-haves verified
|
||||
covered_files: [".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"]
|
||||
covered_digest: "v1:sha256:47e75c553a401d96420242d168f37b0fbc0f89e54dfac382bd3ee80ba1d04a0c"
|
||||
overrides_applied: 0
|
||||
behavior_unverified: 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)*
|
||||
Reference in New Issue
Block a user