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

361 lines
21 KiB
Markdown

---
quick_id: 260907-e8k
slug: zugriffs-guard-greift-nicht-auf-modul-ei
date: 2026-09-07
status: planned
relates_to: 15-modul-berechtigungen-gruppen-user-grants
windows_ref: 10
severity: high
phase: quick-260907-e8k
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- 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
autonomous: true
requirements: [PERM-04]
estimate:
tokens: 45000
raw_tokens: 45000
tasks: 2
confidence: low
must_haves:
truths:
- "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."
artifacts:
- 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
key_links:
- "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"
---
<objective>
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.
</objective>
<execution_context>
@~/.claude/gsd-core/workflows/execute-plan.md
@~/.claude/gsd-core/templates/summary.md
</execution_context>
<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
</context>
<tasks>
<task type="tracer" tdd="true">
<name>Task 1: Gemeinsame 403-Komponente und ModuleAccessGate mit Tests</name>
<files>
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
</files>
<read_first>
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)
</read_first>
<behavior>
- 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.
</behavior>
<action>
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.
</action>
<verify>
<automated>cd /home/vicolab/projects/tessera-ctl &amp;&amp; pnpm --filter @tessera/web exec vitest run src/components/modules/module-access-gate.test.tsx</automated>
<automated>cd /home/vicolab/projects/tessera-ctl &amp;&amp; pnpm --filter @tessera/web type-check</automated>
</verify>
<done>
`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.
</done>
</task>
<task type="auto">
<name>Task 2: Vier Modul-Layouts, Strukturtest und Umstellung der generischen Route</name>
<files>
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
</files>
<read_first>
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)
</read_first>
<behavior>
- 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.
</behavior>
<action>
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.
</action>
<verify>
<automated>cd /home/vicolab/projects/tessera-ctl &amp;&amp; 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</automated>
<automated>cd /home/vicolab/projects/tessera-ctl &amp;&amp; pnpm --filter @tessera/web exec vitest run</automated>
<automated>cd /home/vicolab/projects/tessera-ctl &amp;&amp; pnpm --filter @tessera/web type-check</automated>
<automated>cd /home/vicolab/projects/tessera-ctl &amp;&amp; pnpm --filter @tessera/web build</automated>
<human-check>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.</human-check>
</verify>
<done>
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`.
</done>
</task>
</tasks>
<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>
<verification>
## 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.
</verification>
<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>
<output>
Erstelle `.planning/quick/260907-e8k-zugriffs-guard-greift-nicht-auf-modul-ei/260907-e8k-SUMMARY.md`, wenn fertig.
</output>