Files
tessera-ctl/.planning/quick/260907-e8k-zugriffs-guard-greift-nicht-auf-modul-ei/260907-e8k-PLAN.md
T
schalli 74a30fb8ef docs(quick-260907-e8k): plan module access gate for module-owned routes
WINDOWS #10: der Zugriffs-Guard sitzt nur in der generischen Route
modules/[category]/[moduleSlug]/page.tsx. Die vier fest verdrahteten
Modulverzeichnisse laufen daran vorbei.

Plan: gemeinsame 403-Komponente + ModuleAccessGate (Server Component),
je ein layout.tsx pro Modulverzeichnis (deckt Unterrouten mit ab),
generische Route auf dieselben Bausteine umgestellt. Zwei Tasks,
Browser-Gegenprobe als human-check.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K5jtbGzC5Sf9npJ3JCjKhq
2026-09-07 10:22:44 +02:00

21 KiB

quick_id, slug, date, status, relates_to, windows_ref, severity, phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, estimate, must_haves
quick_id slug date status relates_to windows_ref severity phase plan type wave depends_on files_modified autonomous requirements estimate must_haves
260907-e8k zugriffs-guard-greift-nicht-auf-modul-ei 2026-09-07 planned 15-modul-berechtigungen-gruppen-user-grants 10 high quick-260907-e8k 01 execute 1
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
apps/web/src/app/(portal)/modules/[category]/[moduleSlug]/page.tsx
apps/web/src/app/(portal)/modules/[category]/[moduleSlug]/module-access.test.tsx
true
PERM-04
tokens raw_tokens tasks confidence
45000 45000 2 low
truths artifacts key_links
Ein USER ohne Modulfreigabe bekommt auf /modules/tender-radar, /modules/tender-radar/my-sources, /modules/tender-radar/settings, /modules/dkv-fleet, /modules/dkv-fleet/settings, /modules/dkv-fleet/vehicles, /modules/cert-manager und /modules/domaincheck die 403-Seite als Serverantwort statt der Modulseite.
Ein USER MIT Freigabe sowie jeder ADMIN/SUPER_ADMIN sieht genau dieselben Adressen unveraendert vollstaendig — die Sperre trifft niemanden zu viel.
Die 403-Darstellung ist die Serverantwort selbst: keine Weiterleitung, kein Not-Found (D-07). Der Nutzer erfaehrt, dass das Modul existiert und ihm die Freigabe fehlt.
Die Zugriffspruefung bleibt geschlossen bei fehlendem Sitzungscookie, nicht-ok-Antwort der API, Netzfehler UND geworfener Ausnahme.
Die 403-Markierung steht genau einmal im Code — generische Route und modul-eigene Routen rendern dieselbe Komponente.
apps/web/src/components/modules/module-access-denied.tsx
apps/web/src/components/modules/module-access-gate.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/components/modules/module-access-gate.test.tsx
apps/web/src/app/(portal)/modules/module-layouts.test.tsx
layout.tsx jedes Modulverzeichnisses -> ModuleAccessGate mit dem Slug, der exakt dem Verzeichnisnamen entspricht (cert-manager, dkv-fleet, domaincheck, tender-radar sind zugleich die DB-Slugs)
ModuleAccessGate -> checkModuleAccess -> GET /modules/active — dieselbe Zugriffsaufloesung wie Sidebar und ModuleGuard (D-01), keine zweite Rollenlogik im Frontend
Ein Layout deckt in Next.js App Router alle verschachtelten Unterrouten mit ab — genau darueber werden my-sources, settings und vehicles ohne eigene Datei geschlossen
Der serverseitige Zugriffs-Guard fuer Module greift heute nur auf der generischen Route `modules/[category]/[moduleSlug]/page.tsx`. Die vier Module mit fest verdrahteten eigenen Routen laufen daran vorbei und rendern fuer einen USER ohne Freigabe die volle Seite (WINDOWS #10, gemessen am 2026-09-07).

Dieser Plan zieht die Pruefung in ein wiederverwendbares Gate und haengt sie ueber je ein layout.tsx vor jedes Modulverzeichnis. Ein Layout deckt alle Unterrouten mit ab — damit schliessen sich my-sources, settings und vehicles ohne eigene Datei.

Purpose: PERM-04 gilt bisher nur fuer eine von zwei Routenformen. Es werden zwar keine Daten preisgegeben (die API antwortet ueberall mit 403), aber der Nutzer sieht bedienbare Knoepfe, die ins Leere laufen, und rohe englische Fehlertexte mitten in der deutschen Oberflaeche. Output: Eine gemeinsame 403-Komponente, ein Zugriffs-Gate als Server Component, vier Layout-Dateien, die zugehoerigen Tests und die auf die gemeinsamen Bausteine umgestellte generische Route.

<execution_context> @/.claude/gsd-core/workflows/execute-plan.md @/.claude/gsd-core/templates/summary.md </execution_context>

@.planning/STATE.md @CLAUDE.md @.planning/phases/15-modul-berechtigungen-gruppen-user-grants/15-UAT-2026-09-07.md

@apps/web/src/lib/module-access-actions.ts @apps/web/src/app/(portal)/modules/[category]/[moduleSlug]/page.tsx @apps/web/src/app/(portal)/modules/[category]/[moduleSlug]/module-access.test.tsx

Task 1: Gemeinsame 403-Komponente und ModuleAccessGate mit Tests 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/[category]/[moduleSlug]/page.tsx (das heute inline stehende 403-Markup, wird 1:1 uebernommen), apps/web/src/lib/module-access-actions.ts (checkModuleAccess, faellt bereits geschlossen aus — T-15-29), apps/web/src/app/(portal)/modules/[category]/[moduleSlug]/module-access.test.tsx (Mock-Muster fuer next-intl/server und vi.doMock, wird uebernommen) - Gate mit gewaehrter Freigabe: rendert die uebergebenen Kinder, und die 403-Ueberschrift kommt nicht vor. - Gate mit verweigerter Freigabe: rendert Ueberschrift, Erlaeuterungstext und Rueckweg der 403-Seite, und die Kinder kommen NICHT vor. - Gate, wenn die Zugriffspruefung eine Ausnahme wirft: rendert die 403-Seite, nicht die Kinder (geschlossen fallen). - Gate uebergibt den Slug unveraendert an die Zugriffspruefung: bei Aufruf mit "tender-radar" wird die Pruefung genau einmal und genau mit "tender-radar" aufgerufen. - Die erwarteten deutschen Texte stehen im Test handgeschrieben ("Kein Zugriff auf dieses Modul", "Zur Startseite") und werden NICHT ueber dieselbe Uebersetzungsfunktion gebildet, die der Produktivcode nutzt — der Uebersetzungszugriff wird wie in module-access.test.tsx mit einer handgepflegten Schluesseltabelle nachgebildet. Erstelle `apps/web/src/components/modules/module-access-denied.tsx` mit der Komponente `ModuleAccessDenied`. Sie ist bewusst synchron und uebersetzungsfrei: sie nimmt die drei fertigen Texte als Eigenschaften `title`, `body` und `backToDashboard` entgegen und rendert das 403-Markup, das heute inline in `[category]/[moduleSlug]/page.tsx` steht — dieselbe Aussenhuelle, dasselbe Schloss-Symbol, derselbe `Link` auf `/`. Kopiere das Markup unveraendert, damit die bereits im Browser bestaetigte Ebene 2 der UAT optisch identisch bleibt. Die Komponente traegt keine Client-Direktive am Dateikopf.

Erstelle apps/web/src/components/modules/module-access-gate.tsx mit der asynchronen Server-Komponente ModuleAccessGate({ moduleSlug, children }). Sie ruft checkModuleAccess(moduleSlug) aus @/lib/module-access-actions auf. Bei gewaehrtem Zugriff gibt sie die Kinder zurueck. Andernfalls holt sie die Uebersetzungen ueber getTranslations('modules') aus next-intl/server und rendert ModuleAccessDenied mit accessDenied.title, accessDenied.body und accessDenied.backToDashboard. Es entstehen KEINE neuen i18n-Schluessel — die drei existieren bereits in src/messages/de.json und src/messages/en.json unter modules.accessDenied.

Umschliesse den Aufruf der Zugriffspruefung mit einer Fehlerbehandlung, die jede geworfene Ausnahme als "kein Zugriff" wertet. checkModuleAccess faengt heute selbst ab und wirft nicht; die Absicherung im Gate ist die zweite Verteidigungslinie, damit ein spaeterer Umbau der Pruefung das Gate nicht versehentlich oeffnet. Die Bedingung ist ausdruecklich "nur bei explizit gewaehrtem Zugriff durchlassen", niemals "nur bei explizit verweigertem Zugriff sperren". Keine Rollenabfrage im Frontend: ADMIN und SUPER_ADMIN loesen ueber GET /modules/active ohnehin als zugriffsberechtigt auf (D-01). Das Gate leitet nicht weiter und ruft kein Not-Found auf — die 403-Darstellung ist die Serverantwort (D-07). Die Datei traegt keine Client-Direktive am Dateikopf.

Erstelle apps/web/src/components/modules/module-access-gate.test.tsx nach dem Muster von module-access.test.tsx: next-intl/server per vi.mock mit einer handgeschriebenen Schluesseltabelle, @/lib/module-access-actions per vi.doMock je Testfall, afterEach mit cleanup, vi.resetModules und vi.doUnmock. Da das Gate eine asynchrone Server-Komponente ist, wird es im Test als Funktion aufgerufen und das Ergebnis abgewartet, bevor es an render uebergeben wird — genau so, wie der bestehende Test die Seitenkomponente behandelt. Decke die vier Faelle aus dem Verhaltensblock ab. cd /home/vicolab/projects/tessera-ctl && pnpm --filter @tessera/web exec vitest run src/components/modules/module-access-gate.test.tsx cd /home/vicolab/projects/tessera-ctl && pnpm --filter @tessera/web type-check module-access-gate.test.tsx ist gruen mit mindestens vier Faellen (durchgelassen, verweigert, Ausnahme, Slug-Weitergabe). ModuleAccessDenied enthaelt das 403-Markup, ModuleAccessGate enthaelt die Zugriffslogik, und die Typpruefung des Web-Pakets ist fehlerfrei.

Task 2: Vier Modul-Layouts, Strukturtest und Umstellung der generischen Route 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, apps/web/src/app/(portal)/modules/[category]/[moduleSlug]/page.tsx, apps/web/src/app/(portal)/modules/[category]/[moduleSlug]/module-access.test.tsx apps/web/src/components/modules/module-access-gate.tsx (aus Task 1), apps/web/src/app/(portal)/modules/[category]/[moduleSlug]/module-access.test.tsx (die beiden Testfaelle der Seitenkomponente werden umgeschrieben, die Bloecke zu checkModuleAccess und ModuleShell bleiben unangetastet) - Jedes der vier Layouts gibt ein ModuleAccessGate zurueck, dessen Slug exakt dem Verzeichnisnamen entspricht: cert-manager, dkv-fleet, domaincheck, tender-radar. Die erwarteten Slugs stehen im Test handgeschrieben. - Jedes Layout reicht die empfangenen Kinder unveraendert an das Gate weiter. - Jedes Unterverzeichnis von modules/, dessen Name nicht mit einer eckigen Klammer beginnt, besitzt eine layout.tsx — ein kuenftig hinzugefuegtes Modulverzeichnis ohne Layout laesst den Test rot werden. - Die generische Route uebergibt dem Gate den Slug aus den Routenparametern und als Kind die ModuleShell mit Kategorie und Slug. Lege je eine `layout.tsx` in `apps/web/src/app/(portal)/modules/cert-manager/`, `dkv-fleet/`, `domaincheck/` und `tender-radar/` an. Jede Datei exportiert eine gewoehnliche, synchrone Standardfunktion, die `children` entgegennimmt und ein `ModuleAccessGate` mit dem als Zeichenkette fest eingetragenen Slug des jeweiligen Verzeichnisses zurueckgibt. Die Verzeichnisnamen sind zugleich die Modul-Slugs in der Datenbank — nicht umbenennen, nicht ableiten, nicht aus dem Pfad berechnen. Die Layouts bleiben Server Components: keine Client-Direktive am Dateikopf, keine Hooks, keine Ereignisbehandler. Die Datei enthaelt ausser dem Import des Gates und der Funktion nichts weiter; alle bestehenden Seiten bleiben unveraendert.

Der entscheidende Effekt: ein Layout im App Router umschliesst automatisch alle verschachtelten Unterrouten. tender-radar/layout.tsx schliesst damit auch tender-radar/my-sources und tender-radar/settings, dkv-fleet/layout.tsx auch dkv-fleet/settings und dkv-fleet/vehicles. Es werden keine weiteren Layout-Dateien in den Unterverzeichnissen angelegt.

Erstelle apps/web/src/app/(portal)/modules/module-layouts.test.tsx. Der Test importiert die vier Layouts relativ, ruft jedes als Funktion mit einem Platzhalter-Kind auf und prueft am zurueckgegebenen Element, dass die Eigenschaft moduleSlug dem handgeschriebenen Erwartungswert entspricht und die Kinder durchgereicht werden. Ergaenze einen zweiten Fall, der das Modulverzeichnis mit node:fs ausliest (Pfad ueber import.meta.url, die Testdatei liegt selbst im Modulverzeichnis), alle Unterverzeichnisse einsammelt, deren Name nicht mit einer eckigen Klammer beginnt, und fuer jedes das Vorhandensein einer layout.tsx verlangt. Damit faellt ein kuenftig hinzugefuegtes Modulverzeichnis ohne Gate auf.

Stelle [category]/[moduleSlug]/page.tsx auf die gemeinsamen Bausteine um: die Seite liest weiterhin die Routenparameter und gibt danach ein ModuleAccessGate mit dem Slug zurueck, in dem die ModuleShell als Kind steht. Das bisher inline stehende 403-Markup und der direkte Aufruf der Zugriffspruefung entfallen dort ersatzlos, ebenso die dann ungenutzten Importe. Der erklaerende Kommentarkopf der Datei wird auf den neuen Aufbau angepasst und behaelt den Hinweis auf D-07 und die gemeinsame Zugriffsaufloesung.

Passe module-access.test.tsx an: die beiden Faelle der Seitenkomponente pruefen jetzt die Weitergabe statt der Darstellung — die Seite gibt ein Element zurueck, dessen moduleSlug dem Slug aus den Routenparametern entspricht und dessen Kind die ModuleShell mit derselben Kategorie und demselben Slug ist. Die Faelle zu checkModuleAccess (faellt geschlossen) und zur ModuleShell-Whitelist bleiben unveraendert erhalten; das Verhalten von 403 gegenueber Durchlassen ist seit Task 1 im Gate-Test abgedeckt und wird hier nicht doppelt geprueft. cd /home/vicolab/projects/tessera-ctl && 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 cd /home/vicolab/projects/tessera-ctl && pnpm --filter @tessera/web exec vitest run cd /home/vicolab/projects/tessera-ctl && pnpm --filter @tessera/web type-check cd /home/vicolab/projects/tessera-ctl && pnpm --filter @tessera/web build Browser-Gegenprobe nach Neubau des Web-Containers — die vollstaendige Adress- und Kontenmatrix steht unten im Abschnitt "Browser-Gegenprobe". Wird vom Orchestrator ausgefuehrt, nicht vom Ausfuehrenden. Die vier Layout-Dateien existieren, tragen jeweils den korrekten Slug und keine Client-Direktive. module-layouts.test.tsx und module-access.test.tsx sind gruen, die vollstaendige Web-Testsuite ist gruen, Typpruefung und Produktionsbau des Web-Pakets laufen fehlerfrei durch. Das 403-Markup steht nur noch in module-access-denied.tsx.

<threat_model>

Trust Boundaries

Boundary Description
Browser -> Next.js Server Component Der Nutzer bestimmt die Adresse frei. Sidebar und Marktplatz sind Bequemlichkeit, keine Zugriffskontrolle — ein Lesezeichen oder eine getippte Adresse umgeht beide.
Next.js Server Component -> NestJS API Das Gate leitet das Sitzungscookie an GET /modules/active weiter; die verbindliche Entscheidung faellt in der API (ModuleAccessService), nicht im Frontend.

STRIDE Threat Register

Threat ID Category Component Severity Disposition Mitigation Plan
T-e8k-01 Elevation of Privilege modul-eigene Routen unter (portal)/modules/{cert-manager,dkv-fleet,domaincheck,tender-radar} samt Unterseiten high mitigate Je ein layout.tsx pro Modulverzeichnis ruft ModuleAccessGate; das Layout umschliesst alle verschachtelten Unterrouten automatisch (Task 2)
T-e8k-02 Information Disclosure ModuleAccessGate bei Netzfehler oder geworfener Ausnahme der Zugriffspruefung medium mitigate Fehlerbehandlung im Gate wertet jede Ausnahme als "kein Zugriff"; durchgelassen wird nur bei explizit gewaehrtem Zugriff (Task 1, Testfall 3)
T-e8k-03 Tampering Falscher Slug in einem der vier Layouts (Kopierfehler zwischen fast identischen Dateien) medium mitigate module-layouts.test.tsx prueft je Verzeichnis den handgeschriebenen Erwartungs-Slug (Task 2)
T-e8k-04 Elevation of Privilege Kuenftig hinzugefuegtes Modulverzeichnis ohne Layout laeuft erneut am Gate vorbei medium mitigate module-layouts.test.tsx liest das Modulverzeichnis aus und verlangt fuer jedes nicht-dynamische Unterverzeichnis eine layout.tsx (Task 2)
T-e8k-05 Denial of Service Ueberschiessende Sperre trifft ADMIN/SUPER_ADMIN oder freigegebene USER medium mitigate Keine Rollenlogik im Frontend; das Gate fragt ausschliesslich checkModuleAccess (D-01). Browser-Gegenprobe deckt admin und nutzer1 ausdruecklich ab

Keine Paketinstallationen in diesem Plan — das Package-Legitimacy-Gate entfaellt. </threat_model>

Automatisiert

  1. pnpm --filter @tessera/web exec vitest run — vollstaendige Web-Suite gruen, inklusive der neuen Dateien module-access-gate.test.tsx und module-layouts.test.tsx.
  2. pnpm --filter @tessera/web type-check — fehlerfrei.
  3. pnpm --filter @tessera/web build — der Produktionsbau muss durchlaufen. Das ist zugleich die Probe darauf, dass die Layouts echte Server Components geblieben sind: eine Client-Direktive wuerde am Import der Server-Aktion scheitern.

Browser-Gegenprobe (human-check, vom Orchestrator auszufuehren)

Die Weboberflaeche laeuft aus einem gebauten Docker-Image. Vor jeder Pruefung neu bauen und ersetzen, ein blosses Hochfahren baut nicht neu:

docker compose build web && docker compose up -d --force-recreate web

Danach http://localhost:3000 im echten Browser. Zwischen den Konten jeweils sauber abmelden. Jede Adresse direkt eintippen, nicht ueber die Sidebar navigieren — geprueft wird genau der Weg an der Sidebar vorbei. Bei jedem Adresswechsel innerhalb desselben Moduls einmal neu laden.

A — nutzer2 / Test1234! (USER, KEINE Freigabe fuer tender-radar, HAT cert-manager ueber die Gruppe "Alle Benutzer")

Adresse Erwartung
/modules/procurement/tender-radar 403-Seite "Kein Zugriff auf dieses Modul" (unveraendert gegenueber heute)
/modules/tender-radar 403-Seite statt Trefferliste — der eigentliche Befund
/modules/tender-radar/my-sources 403-Seite statt Postfach- und Feed-Formular
/modules/tender-radar/settings 403-Seite
/modules/dkv-fleet 403-Seite, insbesondere ohne den Knopf "Jetzt pruefen"
/modules/dkv-fleet/settings 403-Seite
/modules/dkv-fleet/vehicles 403-Seite
/modules/cert-manager volle Seite — hier besteht eine Freigabe, die Sperre darf nicht ueberschiessen
/modules/domaincheck Ergebnis muss zu dem passen, was die Freigaben-Matrix in der Administration fuer nutzer2 und domaincheck ausweist: mit Freigabe volle Seite, ohne Freigabe die 403-Seite

Zusaetzlich auf allen 403-Seiten pruefen: die Adresszeile bleibt unveraendert stehen (keine Weiterleitung, D-07), die Seite ist deutsch, und es erscheinen keine rohen englischen Techniktexte.

B — nutzer1 / Test1234! (USER, HAT tender-radar ueber Gruppe und Direkt-Grant)

Adresse Erwartung
/modules/tender-radar volle Seite mit Trefferliste
/modules/tender-radar/my-sources volle Seite mit Postfach- und Feed-Formular
/modules/tender-radar/settings Seite laedt (keine 403-Seite); die bereits bestehende Rollenunterscheidung innerhalb der Seite bleibt davon unberuehrt

C — admin / admin123 (SUPER_ADMIN)

Adresse Erwartung
/modules/tender-radar volle Seite
/modules/tender-radar/my-sources volle Seite
/modules/dkv-fleet volle Seite
/modules/cert-manager volle Seite
/modules/domaincheck volle Seite

Faellt hier auch nur eine Seite auf 403, ist die Aenderung zu scharf und muss zurueck — SUPER_ADMIN umgeht Grants (D-01).

Ledger

WINDOWS.md #10 bleibt offen, bis Abschnitt A, B und C im Browser bestanden sind. Erst dann auf geschlossen setzen, mit Datum und Belegbild.

<success_criteria>

  • Ein USER ohne Freigabe erhaelt auf allen acht in Abschnitt A gelisteten Adressen die 403-Seite als Serverantwort, nicht die Modulseite.
  • Ein USER mit Freigabe und jeder SUPER_ADMIN erreichen dieselben Adressen unveraendert vollstaendig.
  • Adresszeile bleibt bei der 403-Seite stehen: keine Weiterleitung, kein Not-Found (D-07).
  • Die Zugriffspruefung faellt weiterhin geschlossen aus — fehlendes Cookie, nicht-ok-Antwort, Netzfehler und geworfene Ausnahme fuehren alle zur 403-Seite.
  • Das 403-Markup existiert genau einmal im Code; die generische Route und die vier modul-eigenen Routen rendern dieselbe Komponente.
  • Vollstaendige Web-Testsuite, Typpruefung und Produktionsbau sind gruen.
  • Ein neu angelegtes Modulverzeichnis ohne layout.tsx laesst module-layouts.test.tsx fehlschlagen.

</success_criteria>

Erstelle `.planning/quick/260907-e8k-zugriffs-guard-greift-nicht-auf-modul-ei/260907-e8k-SUMMARY.md`, wenn fertig.