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

16 KiB
Raw Blame History

phase, plan, subsystem, tags, requires, provides, affects, actuals, plan_head_before, tech-stack, key-files, key-decisions, requirements-completed, coverage, duration, completed, status
phase plan subsystem tags requires provides affects actuals plan_head_before tech-stack key-files key-decisions requirements-completed coverage duration completed status
quick-260910-exd 01 database
prisma
postgresql
row-level-security
multi-tenancy
nestjs
vitest
phase provides
quick-260909-jts forTenant()/withTenantTransaction(), the Group/GroupMembership/ModuleGrant/TenantModuleActivation RLS policies, and module-grants.service.ts as the reference pattern for two-client bound tests
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
module-registry
dashboard (picks up the binding transitively via getAccessibleModuleIds)
etappe-3-rls-policy-tightening
etappe-4-cutover
tokens tasks commits
25541 3 3
a2516a9
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)
created modified
apps/api/src/module-registry/module-registry.service.spec.ts
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
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
WINDOWS-18
ETAPPE-2-MODULE-REGISTRY
~75min 2026-09-10 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