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
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 |
|
true |
|
|
|
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>
@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.
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
pnpm --filter @tessera/web exec vitest run— vollstaendige Web-Suite gruen, inklusive der neuen Dateienmodule-access-gate.test.tsxundmodule-layouts.test.tsx.pnpm --filter @tessera/web type-check— fehlerfrei.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.tsxlaesstmodule-layouts.test.tsxfehlschlagen.
</success_criteria>
Erstelle `.planning/quick/260907-e8k-zugriffs-guard-greift-nicht-auf-modul-ei/260907-e8k-SUMMARY.md`, wenn fertig.