16 KiB
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 |
|
|
|
|
|
a2516a9 |
|
|
|
|
~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) andgetCatalogFlags's own activation read now run throughforTenant(), one client per method — existingtenantIdwhere-filters remain as the second net (T-JTS-02/T-JTS-03).ModuleRegistryService.findActiveForTenant,activateForTenant,deactivateForTenant(both activation accesses over one client), andisModuleActiveare 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.tscreated 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 (isModuleActivewithout an activation returnsfalse) as named, deliberately-preserved properties.module-access.service.spec.tsrebuilt 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.tspins down that "genuinely no grant" and "the resolution found nothing" produce the identicalForbiddenExceptionmessage today — the machine record of the area's central finding. isModuleActive's header comment corrected: it claimedModuleGuardcalls it; measured zero callers exist (the guard usesfindBySlug+getAccessibleModuleIdsinstead).rls-scratch-check.mjsgained 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 withtenantId) is now committed evidence, not an assertion.docs/mandantentrennung-zugriffsklassifikation.mdfully 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.mdgained the## Bereich module-registrysection (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:
- Aufgabe 1: measure the error direction, no production code —
7d45e2f(test) - Aufgabe 2: bind ModuleAccessService —
3df7268(feat) - 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/getCatalogFlagsbound toforTenant(), catalog access left unbound with a measurement+condition commentapps/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 signalapps/api/src/module-registry/module-registry.service.ts—findActiveForTenant/activateForTenant/deactivateForTenant/isModuleActivebound;isModuleActive's header comment correctedapps/api/src/module-registry/module-registry.service.spec.ts— new, 16 casesapps/api/src/tenders/tender-scheduler.service.spec.ts—forTenant()mocked to identity (same convention asldap.service.spec.ts) to fix a cross-area break caused by theactivateForTenantconversionapps/api/scripts/rls-scratch-check.mjs— eighth sectionrunModuleRegistryAreaChecks, 13 named checksdocs/mandantentrennung-etappe2-fehlerrichtung.md— new## Bereich module-registrysection (m1–m5) plus Task-3 addendumdocs/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 —
Modulecarries 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
getAllActiveConfigsinldapand the five spots intenders): 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.tsdrives the realModuleRegistryServiceagainst a hand-rolled fake without$extends. Rather than adding$extends/$transactionsupport to that fake or weakening the production binding,forTenantwas mocked to identity in that one file — the exact patternldap.service.spec.tsalready 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
ModuleRegistryServiceagainst a hand-rolled fake prisma object that has no$extendsmethod (by design — it predates any RLS binding in this service).forTenant()callsprisma.$extends(...), so the test failed withprisma.$extends is not a function. - Fix: Mocked
forTenantto an identity function (vi.fn((p) => p)) in this one file, matching the exact conventionldap.service.spec.tsalready 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.tsgreen (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 testsuite, which includes this cross-check between the classification doc and the source) - Issue:
module-access.service.ts'smoduleGrantandtenantModuleActivationrows indocs/mandantentrennung-zugriffsklassifikation.mdstill saidStand: ungebundenthe 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
Standcolumn (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.tsgreen; 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 insidegetAccessibleModuleIds) back tothis.prisma.moduleGrant.findMany(...). module-access.service.spec.tstest "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(...)(indeactivateForTenant) back tothis.prisma.tenantModuleActivation.update(...). module-registry.service.spec.tstest "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— FOUNDapps/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 ingit log - Commit
3df7268— FOUND ingit log - Commit
9c0eefe— FOUND ingit 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— cleannode 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