diff --git a/.planning/quick/260907-e8k-zugriffs-guard-greift-nicht-auf-modul-ei/260907-e8k-SUMMARY.md b/.planning/quick/260907-e8k-zugriffs-guard-greift-nicht-auf-modul-ei/260907-e8k-SUMMARY.md new file mode 100644 index 0000000..488af20 --- /dev/null +++ b/.planning/quick/260907-e8k-zugriffs-guard-greift-nicht-auf-modul-ei/260907-e8k-SUMMARY.md @@ -0,0 +1,176 @@ +--- +phase: quick-260907-e8k +plan: 01 +subsystem: auth +tags: [nextjs, app-router, server-components, next-intl, vitest, module-access] + +requires: + - phase: 15-modul-berechtigungen-gruppen-user-grants + provides: checkModuleAccess (module-access-actions.ts) — fails-closed session-cookie-based access check against GET /modules/active +provides: + - Shared ModuleAccessDenied 403 markup component (translation-free, props-driven) + - Reusable ModuleAccessGate server component wrapping checkModuleAccess with a second fail-closed layer + - layout.tsx access gate for each of the four module-owned route trees (cert-manager, dkv-fleet, domaincheck, tender-radar) + - Generic [category]/[moduleSlug]/page.tsx route rewired onto the shared gate + - module-layouts.test.tsx guarding against future module directories missing a layout +affects: [15-modul-berechtigungen-gruppen-user-grants, module-routing, module-marketplace] + +actuals: + tokens: 5709 + tasks: 2 + commits: 2 + +tech-stack: + added: [] + patterns: + - "Server-side access gate as a reusable component (ModuleAccessGate) consumed both via App Router layout.tsx (covers whole route subtrees) and directly in a page — single source of truth for the 403 decision and markup" + - "Fail-closed defense in depth: checkModuleAccess already fails closed; ModuleAccessGate adds a second try/catch layer so a future change to the check function cannot silently open the gate" + +key-files: + created: + - apps/web/src/components/modules/module-access-denied.tsx + - apps/web/src/components/modules/module-access-gate.tsx + - apps/web/src/components/modules/module-access-gate.test.tsx + - apps/web/src/app/(portal)/modules/cert-manager/layout.tsx + - apps/web/src/app/(portal)/modules/dkv-fleet/layout.tsx + - apps/web/src/app/(portal)/modules/domaincheck/layout.tsx + - apps/web/src/app/(portal)/modules/tender-radar/layout.tsx + - apps/web/src/app/(portal)/modules/module-layouts.test.tsx + modified: + - apps/web/src/app/(portal)/modules/[category]/[moduleSlug]/page.tsx + - apps/web/src/app/(portal)/modules/[category]/[moduleSlug]/module-access.test.tsx + +key-decisions: + - "403 markup and translation lookup live once in ModuleAccessGate/ModuleAccessDenied; the generic route and all four module-owned layouts render the same component instead of each having its own inline check" + - "ModuleAccessDenied stays translation-free (props for title/body/backToDashboard) — only ModuleAccessGate calls getTranslations, keeping the presentational component trivially testable without mocking next-intl" + - "A layout.tsx per module directory is used instead of per-page checks, because a Next.js App Router layout automatically wraps every nested subroute — my-sources/settings/vehicles close without any file of their own" + +patterns-established: + - "Module directory coverage test: module-layouts.test.tsx reads the modules/ directory via node:fs at test time and requires a layout.tsx for every non-dynamic subdirectory, so a future module added without a gate fails CI instead of silently reopening the vulnerability" + +requirements-completed: [PERM-04] + +coverage: + - id: D1 + description: "ModuleAccessDenied — shared, translation-free 403 markup component" + requirement: "PERM-04" + verification: + - kind: unit + ref: "apps/web/src/components/modules/module-access-gate.test.tsx#renders the 403 heading, body, and dashboard link and not the children when access is denied" + status: pass + human_judgment: false + - id: D2 + description: "ModuleAccessGate — server-side access gate calling checkModuleAccess, fails closed on denial and on thrown exceptions, passes children through only on explicit grant" + requirement: "PERM-04" + verification: + - kind: unit + ref: "apps/web/src/components/modules/module-access-gate.test.tsx (4 cases: granted, denied, exception, slug pass-through)" + status: pass + human_judgment: false + - id: D3 + description: "layout.tsx for cert-manager, dkv-fleet, domaincheck, tender-radar — each wraps its whole route subtree (including nested routes like my-sources, settings, vehicles) in ModuleAccessGate with the correct slug" + requirement: "PERM-04" + verification: + - kind: unit + ref: "apps/web/src/app/(portal)/modules/module-layouts.test.tsx#module layouts — ModuleAccessGate slug wiring" + status: pass + - kind: unit + ref: "apps/web/src/app/(portal)/modules/module-layouts.test.tsx#module directory coverage — every module has a layout" + status: pass + - kind: manual_procedural + ref: "Browser-Gegenprobe (Abschnitt A/B/C in 260907-e8k-PLAN.md) — vom Orchestrator nach Docker-Rebuild auszufuehren" + status: unknown + human_judgment: true + rationale: "Der eigentliche Behebungserfolg (403 als Serveranwort statt Modulseite, keine Ueberdeckung fuer berechtigte Nutzer/Admins) ist nur im echten Browser gegen die Docker-Instanz mit drei realen Konten pruefbar — WINDOWS #10 bleibt laut Plan explizit offen, bis diese Gegenprobe bestanden ist." + - id: D4 + description: "Generische Route [category]/[moduleSlug]/page.tsx auf ModuleAccessGate umgestellt, inline-403-Markup entfernt" + requirement: "PERM-04" + verification: + - kind: unit + ref: "apps/web/src/app/(portal)/modules/[category]/[moduleSlug]/module-access.test.tsx#ExpandedModulePage — passes slug through to ModuleAccessGate" + status: pass + human_judgment: false + +duration: 25min +completed: 2026-09-07 +status: complete +--- + +# Phase quick-260907-e8k Plan 01: Modul-Zugriffs-Guard auf Layout-Ebene Summary + +**Vier fest verdrahtete Modulrouten (cert-manager, dkv-fleet, domaincheck, tender-radar) liefen bislang komplett am 403-Guard vorbei — sie sind jetzt ueber ein layout.tsx pro Modulverzeichnis an dieselbe ModuleAccessGate-Komponente angeschlossen wie die generische Route.** + +## Performance + +- **Duration:** 25 min +- **Started:** 2026-09-07T08:03Z (ca.) +- **Completed:** 2026-09-07T08:28Z +- **Tasks:** 2/2 completed +- **Files modified:** 10 (8 created, 2 modified) + +## Accomplishments + +- Das bisher inline in `[category]/[moduleSlug]/page.tsx` stehende 403-Markup existiert jetzt genau einmal, in `ModuleAccessDenied` — eine reine, uebersetzungsfreie Praesentationskomponente. +- `ModuleAccessGate` buendelt die Zugriffspruefung als wiederverwendbare Server Component: ruft `checkModuleAccess`, faengt zusaetzlich jede geworfene Ausnahme als "kein Zugriff" ab (zweite Verteidigungslinie ueber die bereits geschlossen ausfallende Funktion), gibt Kinder nur bei explizit gewaehrtem Zugriff durch. +- Vier neue `layout.tsx`-Dateien (eine pro Modulverzeichnis) schliessen jetzt auch `tender-radar/my-sources`, `tender-radar/settings`, `dkv-fleet/settings` und `dkv-fleet/vehicles` ab — ohne eigene Datei je Unterroute, weil ein App-Router-Layout automatisch alle verschachtelten Unterrouten mit umschliesst. +- Ein Struktur-Test (`module-layouts.test.tsx`) liest das Modulverzeichnis zur Testzeit per `node:fs` aus und verlangt fuer jedes nicht-dynamische Unterverzeichnis eine `layout.tsx` — ein kuenftig hinzugefuegtes Modul ohne Gate faellt damit in der Testsuite auf, nicht erst in Produktion. +- Die generische Route ist auf dieselben Bausteine umgestellt; ihr eigener Test prueft jetzt die Weitergabe des Slugs statt die 403-Darstellung erneut zu pruefen (die liegt seit Task 1 vollstaendig in `module-access-gate.test.tsx`). + +## Task Commits + +Each task was committed atomically: + +1. **Task 1: Gemeinsame 403-Komponente und ModuleAccessGate mit Tests** - `4a23e13` (feat) +2. **Task 2: Vier Modul-Layouts, Strukturtest und Umstellung der generischen Route** - `69b6418` (feat) + +## Files Created/Modified + +- `apps/web/src/components/modules/module-access-denied.tsx` - Uebersetzungsfreie 403-Praesentationskomponente (title/body/backToDashboard als Props) +- `apps/web/src/components/modules/module-access-gate.tsx` - Server-Component-Gate: ruft checkModuleAccess, faengt Ausnahmen ab, rendert Kinder nur bei explizitem Zugriff +- `apps/web/src/components/modules/module-access-gate.test.tsx` - 4 Testfaelle: durchgelassen, verweigert, Ausnahme, Slug-Weitergabe +- `apps/web/src/app/(portal)/modules/cert-manager/layout.tsx` - Gate mit Slug "cert-manager" +- `apps/web/src/app/(portal)/modules/dkv-fleet/layout.tsx` - Gate mit Slug "dkv-fleet" +- `apps/web/src/app/(portal)/modules/domaincheck/layout.tsx` - Gate mit Slug "domaincheck" +- `apps/web/src/app/(portal)/modules/tender-radar/layout.tsx` - Gate mit Slug "tender-radar" +- `apps/web/src/app/(portal)/modules/module-layouts.test.tsx` - Slug-Verdrahtung je Layout + Verzeichnis-Abdeckungstest +- `apps/web/src/app/(portal)/modules/[category]/[moduleSlug]/page.tsx` - Auf ModuleAccessGate umgestellt, Inline-403 und direkter checkModuleAccess-Aufruf entfernt +- `apps/web/src/app/(portal)/modules/[category]/[moduleSlug]/module-access.test.tsx` - Seiten-Testfaelle auf Gate-Weitergabe umgeschrieben, checkModuleAccess- und ModuleShell-Whitelist-Bloecke unveraendert + +## Decisions Made + +- Keine neuen i18n-Schluessel: `modules.accessDenied.{title,body,backToDashboard}` existierten bereits in `de.json`/`en.json` und werden unveraendert weiterverwendet. +- Layouts bleiben gewoehnliche synchrone Server Components ohne Client-Direktive, ohne Hooks — reiner Import + Return des Gates. + +## Deviations from Plan + +None - plan executed exactly as written. + +## Issues Encountered + +None. + +## User Setup Required + +None - no external service configuration required. + +## Next Phase Readiness + +Automatisierte Verifikation vollstaendig gruen (Details unten). Die Browser-Gegenprobe aus Abschnitt A/B/C des Plans (drei Konten, acht+ Adressen, Docker-Rebuild) ist laut Plan explizit Aufgabe des Orchestrators und wurde in dieser Ausfuehrung nicht durchgefuehrt — WINDOWS #10 bleibt bis dahin offen. + +## Real verification results + +1. `pnpm --filter @tessera/web exec vitest run src/components/modules/module-access-gate.test.tsx` — **4/4 passed** (Task 1 verify) +2. `pnpm --filter @tessera/web type-check` — clean, no output (Task 1 verify) +3. `pnpm --filter @tessera/web exec vitest run 'src/app/(portal)/modules/module-layouts.test.tsx' 'src/app/(portal)/modules/[category]/[moduleSlug]/module-access.test.tsx'` — **9/9 passed** (Task 2 verify) +4. `pnpm --filter @tessera/web exec vitest run` (full suite) — **213/213 passed, 36/36 test files** (Task 2 verify) +5. `pnpm --filter @tessera/web type-check` — clean, no output (Task 2 verify) +6. `pnpm --filter @tessera/web build` — **succeeded**, all 27 routes generated including the four module routes and the generic route; only pre-existing, unrelated warning (jose Edge Runtime CompressionStream/DecompressionStream, not touched by this plan) +7. Browser-Gegenprobe (human-check) — **not run**; per plan and per task instructions this is executed by the orchestrator after a Docker rebuild, not by the executor. + +--- +*Phase: quick-260907-e8k* +*Completed: 2026-09-07* + +## Self-Check: PASSED + +All 10 created/modified source files and this SUMMARY.md verified present on disk. Both task commits (`4a23e13`, `69b6418`) verified present in `git log`.