docs(15): create phase plan — 8 plans, 4 waves, PERM-01..07
This commit is contained in:
@@ -0,0 +1,231 @@
|
||||
---
|
||||
phase: 15-modul-berechtigungen-gruppen-user-grants
|
||||
plan: 08
|
||||
type: execute
|
||||
wave: 4
|
||||
depends_on: ["15-03", "15-06"]
|
||||
files_modified:
|
||||
- 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-shell.tsx
|
||||
- apps/web/src/app/(portal)/modules/[category]/[moduleSlug]/module-access.test.tsx
|
||||
- apps/web/src/app/(portal)/marketplace/page.tsx
|
||||
- apps/web/src/app/(portal)/marketplace/[slug]/page.tsx
|
||||
- apps/web/src/app/(portal)/marketplace/components/MarketplaceCard.tsx
|
||||
- apps/web/src/app/(portal)/marketplace/components/MarketplaceCard.test.tsx
|
||||
autonomous: true
|
||||
requirements: [PERM-04]
|
||||
user_setup: []
|
||||
|
||||
estimate:
|
||||
tokens: 70000
|
||||
raw_tokens: 70000
|
||||
tasks: 2
|
||||
confidence: low
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "Ruft ein Benutzer die URL eines nicht freigegebenen Moduls direkt auf, erscheint eine 403-Seite mit dem Hinweis, sich an den Administrator zu wenden — kein stiller Rücksprung und keine Nicht-gefunden-Seite (D-07, PERM-04)."
|
||||
- "Die Prüfung passiert serverseitig in der Modulseiten-Route, bevor irgendein Modulcode ausgeliefert wird; das Ausblenden in der Sidebar ist ausdrücklich keine Zugriffskontrolle (D-07)."
|
||||
- "Der Marketplace zeigt auch nicht freigegebene Module weiter, gekennzeichnet mit einem Badge 'Kein Zugriff'; öffnen lassen sie sich nicht (D-08)."
|
||||
- "Der Modulkatalog bleibt für jeden authentifizierten Benutzer offen — der Marketplace ist ein Schaufenster, keine Zugriffsentscheidung (D-08)."
|
||||
- "ADMIN und SUPER_ADMIN sehen das Sperr-Badge nie; für sie ist jede aktivierte Karte normal anklickbar (D-03)."
|
||||
- "Sidebar, Modulseite und API stützen sich auf dieselbe Zugriffsauflösung — die Modulseite fragt sie über die API ab, statt eine eigene Regel zu bauen (D-01, PERM-04)."
|
||||
- "Die serverseitige Prüfung schliesst im Zweifel: fehlt das Sitzungs-Cookie oder ist die API nicht erreichbar, gilt der Zugriff als nicht gewährt."
|
||||
- "Der bisherige Inhalt der Modulseite — die Whitelist-Registrierung, das Nachladen der Modulkomponente und der Nicht-gefunden-Zustand für unregistrierte Slugs — bleibt unverändert erhalten und wandert nur in eine eigene Client-Komponente."
|
||||
# --- UI-SPEC 'UI Considerations' — covered ---
|
||||
- "empty, loading, error, populated und zero-one-many des Marketplace-Katalogs bleiben unverändert gegenüber dem ausgelieferten Stand — dieser Plan ergänzt ausschliesslich das Badge und den nicht anklickbaren Zustand einer einzelnen Karte."
|
||||
- "empty, loading und error der 403-Seite und des Aktivierungsdialogs: beide sind reine Zustandsanzeigen ohne eigenen Datenabruf; die 403-Seite rendert serverseitig fertig, Fehler beim Aktivieren laufen über den bestehenden Fehlerpfad der Modulseite."
|
||||
# --- UI-SPEC 'UI Considerations' — backstop ---
|
||||
- statement: "long-text und overflow (Marketplace-Karte mit langem Modulnamen plus zusätzlichem Sperr-Badge): Die Badge-Reihe bricht bei schmaler Karte um, statt aus der Karte zu laufen; die Titelkürzung deckt nur den Titel ab, nicht die Badge-Reihe."
|
||||
verification: backstop
|
||||
- statement: "partial (Marketplace-Karte, bevor der Freigabe-Status bekannt ist): Aktivierungs- und Freigabestatus kommen aus derselben Antwort, sodass keine Karte kurzzeitig ohne Sperr-Badge anklickbar erscheint und erst nachträglich sperrt."
|
||||
verification: backstop
|
||||
artifacts:
|
||||
- "apps/web/src/lib/module-access-actions.ts — checkModuleAccess"
|
||||
- "apps/web/src/app/(portal)/modules/[category]/[moduleSlug]/page.tsx — Server Component mit 403-Zustand"
|
||||
- "apps/web/src/app/(portal)/modules/[category]/[moduleSlug]/module-shell.tsx"
|
||||
- "apps/web/src/app/(portal)/modules/[category]/[moduleSlug]/module-access.test.tsx"
|
||||
- "MarketplaceCard mit hasAccess-Prop und Sperr-Badge"
|
||||
key_links:
|
||||
- "page.tsx → checkModuleAccess → GET /modules/active — die Modulseite fragt dieselbe Auflösung ab, die auch Guard und Sidebar bedienen"
|
||||
- "MarketplaceCard → GET /modules/catalog — beide Statusflags kommen in einer Antwort, damit kein Zwischenzustand entsteht"
|
||||
- "module-shell.tsx → module-loader Whitelist — der Sicherheitsmechanismus gegen beliebige Slugs bleibt unverändert bestehen"
|
||||
prohibitions:
|
||||
- statement: "Sichtbarkeit ist nie Zugriffskontrolle — kein Ausblenden in Sidebar, Marketplace oder Grid darf als Sperre gelten; jede Sperre muss serverseitig durchgesetzt sein und clientseitig nur gespiegelt werden."
|
||||
status: active
|
||||
verification: flagged-unverified
|
||||
---
|
||||
|
||||
<objective>
|
||||
Die beiden Oberflächen, an denen ein Benutzer ohne Freigabe tatsächlich anschlägt: die Modulseite, die serverseitig sperrt und eine erklärende 403-Seite rendert, und der Marketplace-Katalog, der gesperrte Module weiter zeigt, aber als solche kennzeichnet.
|
||||
|
||||
Purpose: Die Sidebar zeigt seit Plan 15-01 nur noch freigegebene Module — das ist Bequemlichkeit, keine Sperre. Wer die URL kennt oder ein Lesezeichen hat, kommt weiterhin auf die Seite. D-07 verlangt deshalb ausdrücklich eine serverseitige Prüfung in der Modulseiten-Route selbst. Die heutige Datei ist vollständig eine Client-Komponente ohne serverseitigen Datenabruf und erfüllt das nicht.
|
||||
Output: Eine Server-Action für den Zugriffscheck, die zur Server-Komponente umgebaute Modulseite mit ausgelagerter Client-Hülle, und die Marketplace-Karte mit drittem Zustand.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/gsd-core/workflows/execute-plan.md
|
||||
@$HOME/.claude/gsd-core/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/15-modul-berechtigungen-gruppen-user-grants/15-CONTEXT.md
|
||||
@.planning/phases/15-modul-berechtigungen-gruppen-user-grants/15-UI-SPEC.md
|
||||
@.planning/phases/15-modul-berechtigungen-gruppen-user-grants/15-PATTERNS.md
|
||||
@.planning/phases/15-modul-berechtigungen-gruppen-user-grants/15-03-SUMMARY.md
|
||||
@.planning/phases/15-modul-berechtigungen-gruppen-user-grants/15-06-SUMMARY.md
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Serverseitige Modulsperre mit 403-Seite</name>
|
||||
<files>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-shell.tsx, apps/web/src/app/(portal)/modules/[category]/[moduleSlug]/module-access.test.tsx</files>
|
||||
<read_first>
|
||||
- apps/web/src/app/(portal)/modules/[category]/[moduleSlug]/page.tsx — die komplette Datei: der Nicht-gefunden-Block (Zeilen 30–64) als visuelle Vorlage für den 403-Zustand, die Whitelist-Prüfung gegen die Modulregistrierung und der Rücknavigations-Block
|
||||
- apps/web/src/lib/auth-actions.ts — Zeilen 243–268: `fetchCurrentUser` als vollständiges Kopiermuster für die Cookie-Weiterleitung in einer Server-Action, inklusive des fehlschlagenden Pfads
|
||||
- apps/web/src/lib/module-loader.ts — die Whitelist und `loadModuleComponent`, deren Verhalten unverändert bleiben muss
|
||||
- apps/web/src/messages/de.json — die in Plan 15-06 angelegten Schlüssel unter `modules.accessDenied`
|
||||
- .planning/phases/15-modul-berechtigungen-gruppen-user-grants/15-UI-SPEC.md — Surface Contract 4 und der Copywriting Contract zur 403-Seite
|
||||
- .planning/phases/15-modul-berechtigungen-gruppen-user-grants/15-RESEARCH.md — Pattern 5 zum Server-Komponenten-Schnitt
|
||||
</read_first>
|
||||
<action>
|
||||
Lege `apps/web/src/lib/module-access-actions.ts` an, als Server-Modul mit der Direktive am Dateikopf, analog zu `auth-actions.ts`. Exportiere `checkModuleAccess(moduleSlug: string): Promise<boolean>`. Die Funktion liest das Sitzungs-Cookie über `cookies()` und ruft `GET /modules/active` mit weitergeleitetem Cookie-Header, `credentials: 'include'` und `cache: 'no-store'` auf — exakt das Muster von `fetchCurrentUser`. Aus der Antwort wird geprüft, ob der gesuchte Slug enthalten ist.
|
||||
|
||||
Die Funktion schliesst im Zweifel: fehlt das Cookie, antwortet die API nicht erfolgreich oder wirft der Aufruf, ist das Ergebnis falsch. Ein Fehler im Netzwerkpfad darf nie zu einem offenen Zugang führen.
|
||||
|
||||
Es wird bewusst der bestehende Endpoint wiederverwendet und kein eigener Zugriffs-Endpoint eingeführt. Die Modulanzahl je Mandant liegt im einstelligen bis niedrigen zweistelligen Bereich, der Umweg über die Liste ist damit unkritisch, und es entsteht kein zweiter Vertrag über dieselbe Frage.
|
||||
|
||||
Baue `page.tsx` in eine asynchrone Server-Komponente um. Die Datei trägt danach keine Client-Direktive mehr, liest `params` und ruft `checkModuleAccess(moduleSlug)` auf.
|
||||
|
||||
Ist der Zugriff nicht gewährt, rendert die Seite unmittelbar das 403-Markup als Server-Antwort. Sie ruft dabei weder die Next.js-Weiche für nicht gefundene Seiten noch eine Umleitung auf — D-07 schliesst beides ausdrücklich aus, weil der Benutzer erfahren soll, dass das Modul existiert und ihm die Freigabe fehlt, statt es für nicht vorhanden zu halten oder wortlos woanders zu landen.
|
||||
|
||||
Das 403-Markup übernimmt die Struktur des bestehenden Nicht-gefunden-Blocks eins zu eins: zentrierter Block, Icon-Container mit `rounded-lg bg-muted p-4`, eine Überschrift in 18 Pixel und Gewicht 600, ein Absatz in 14 Pixel `text-muted-foreground`, darunter ein Link. Nur zwei Dinge ändern sich. Erstens das Icon: statt des Kreises mit Schrägstrich ein Schloss-Motiv im selben SVG-Vokabular, damit "existiert nicht" und "gesperrt" optisch unterscheidbar bleiben. Zweitens Text und Ziel: Überschrift aus `modules.accessDenied.title`, Text aus `modules.accessDenied.body`, und ein Link auf die Startseite mit `modules.accessDenied.backToDashboard` — nicht zurück in die Modulkategorie, weil ein Rücksprung dorthin für ein Modul ohne Freigabe kein sinnvoller nächster Schritt ist.
|
||||
|
||||
Ist der Zugriff gewährt, rendert die Seite `<ModuleShell category={category} moduleSlug={moduleSlug} />`.
|
||||
|
||||
Lege `module-shell.tsx` als Client-Komponente an und verschiebe den bisherigen Inhalt von `page.tsx` unverändert dorthin: die Whitelist-Prüfung gegen die Modulregistrierung, das Nachladen der Modulkomponente, den Nicht-gefunden-Zustand für unregistrierte Slugs und den Rücknavigations-Block. Der Sicherheitskommentar zur Whitelist wandert mit. An diesem Verhalten wird nichts geändert: die Whitelist verhindert weiterhin, dass ein beliebiger Slug aus der URL einen Import auslöst, und ist eine von der Freigabeprüfung unabhängige zweite Absicherung. Statt der Hook-basierten Parameterauflösung bekommt die Komponente `category` und `moduleSlug` als Props.
|
||||
|
||||
Lege `module-access.test.tsx` an mit Tests für: bei verweigertem Zugriff erscheinen Titel und Text der 403-Seite und die Modulhülle wird nicht gerendert; bei gewährtem Zugriff wird die Modulhülle gerendert; bei fehlendem Sitzungs-Cookie ist das Ergebnis von `checkModuleAccess` falsch; bei einer nicht erfolgreichen API-Antwort ist das Ergebnis falsch; ein unregistrierter Slug bei gewährtem Zugriff führt weiterhin in den Nicht-gefunden-Zustand der Modulhülle.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>pnpm --filter @tessera/web test -- module-access</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `pnpm --filter @tessera/web test -- module-access` ist grün und enthält je einen Test für die fünf oben genannten Fälle.
|
||||
- `grep -c "'use client'" "apps/web/src/app/(portal)/modules/[category]/[moduleSlug]/page.tsx"` gibt `0` aus.
|
||||
- `grep -c "'use client'" "apps/web/src/app/(portal)/modules/[category]/[moduleSlug]/module-shell.tsx"` gibt `1` aus.
|
||||
- `grep -c 'notFound(\|redirect(' "apps/web/src/app/(portal)/modules/[category]/[moduleSlug]/page.tsx"` gibt `0` aus.
|
||||
- `grep -c 'checkModuleAccess' "apps/web/src/app/(portal)/modules/[category]/[moduleSlug]/page.tsx" apps/web/src/lib/module-access-actions.ts` belegt Aufruf und Definition.
|
||||
- `grep -c 'accessDenied' "apps/web/src/app/(portal)/modules/[category]/[moduleSlug]/page.tsx"` gibt mindestens `2` aus (Titel und Text).
|
||||
- `grep -c 'MODULE_REGISTRY' "apps/web/src/app/(portal)/modules/[category]/[moduleSlug]/module-shell.tsx"` gibt mindestens `1` aus — die Whitelist ist unverändert mitgewandert.
|
||||
- Manuell: als USER ohne Freigabe die URL eines aktivierten Moduls direkt aufrufen; die Antwort ist die 403-Seite mit Schloss-Icon und Link auf die Startseite. Im Netzwerk-Reiter ist erkennbar, dass kein Modul-Bundle nachgeladen wurde.
|
||||
- `pnpm --filter @tessera/web test` läuft vollständig grün und `pnpm --filter @tessera/web run build` schliesst fehlerfrei ab.
|
||||
</acceptance_criteria>
|
||||
<done>Der direkte URL-Aufruf eines nicht freigegebenen Moduls endet serverseitig in einer erklärenden 403-Seite, ohne dass Modulcode ausgeliefert wird, und der bestehende Whitelist-Schutz ist unverändert erhalten.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Marketplace-Karte mit drittem Zustand "Kein Zugriff"</name>
|
||||
<files>apps/web/src/app/(portal)/marketplace/components/MarketplaceCard.tsx, apps/web/src/app/(portal)/marketplace/components/MarketplaceCard.test.tsx, apps/web/src/app/(portal)/marketplace/page.tsx, apps/web/src/app/(portal)/marketplace/[slug]/page.tsx</files>
|
||||
<read_first>
|
||||
- apps/web/src/app/(portal)/marketplace/components/MarketplaceCard.tsx — die komplette Datei: die Props-Schnittstelle, die Badge-Reihe (Zeilen 96–110) und der Kartenzustand samt Klickverhalten (Zeilen 88, 120–138)
|
||||
- apps/web/src/app/(portal)/marketplace/page.tsx — Zeilen 34–60: der zweigeteilte Datenabruf und die Ableitung des Aktivierungszustands aus der Antwort
|
||||
- apps/web/src/app/(portal)/marketplace/[slug]/page.tsx — Zeilen 45–60: derselbe zweigeteilte Abruf auf der Detailseite
|
||||
- apps/web/src/app/(portal)/marketplace/components/Toast.tsx — die bestehende Toast-Komponente, die unverändert wiederverwendet wird
|
||||
- apps/web/src/app/(portal)/marketplace/components/MarketplaceCard.test.tsx — die bestehende Testsuite, die erweitert wird
|
||||
- apps/web/src/messages/de.json — die in Plan 15-06 angelegten Schlüssel `marketplace.statusNoAccess` und `marketplace.toastNoAccess`
|
||||
- .planning/phases/15-modul-berechtigungen-gruppen-user-grants/15-UI-SPEC.md — Surface Contract 5 und die Badge-Palette im Abschnitt Color
|
||||
- .planning/phases/15-modul-berechtigungen-gruppen-user-grants/15-03-SUMMARY.md — das Antwortformat von `GET /modules/catalog`
|
||||
</read_first>
|
||||
<action>
|
||||
Stelle den Datenabruf in `marketplace/page.tsx` und in `marketplace/[slug]/page.tsx` auf den einen Aufruf `GET /modules/catalog` um, der je Modul den vollständigen Datensatz plus die beiden Flags für mandantenweite Aktivierung und Benutzerzugriff liefert. Die bisherige Kombination aus zwei Aufrufen entfällt.
|
||||
|
||||
Der Wechsel ist notwendig, nicht kosmetisch: seit Plan 15-01 antwortet `GET /modules/active` benutzergefiltert, und aus dieser Antwort allein könnte der Marketplace für einen USER nicht mehr zwischen "nicht aktiviert" und "aktiviert, aber nicht freigegeben" unterscheiden. Genau diese Unterscheidung ist der Kern von D-08.
|
||||
|
||||
Dass beide Flags aus derselben Antwort kommen, löst zugleich den Zwischenzustand: es gibt keinen Moment, in dem eine Karte bereits gerendert und anklickbar ist, ihr Sperrhinweis aber noch fehlt und nachträglich erscheint.
|
||||
|
||||
Erweitere `MarketplaceCard` um die Prop `hasAccess: boolean`. Bei `isActive && !hasAccess` gilt: ein zusätzliches drittes Badge mit dem Text aus `marketplace.statusNoAccess` erscheint in derselben Badge-Reihe, in Bernstein (`bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400`). Bernstein und nicht Rot ist eine bewusste Wahl: es ist kein Fehlerzustand, sondern ein Informationszustand.
|
||||
|
||||
Die Badge-Reihe bekommt zusätzlich Umbruchverhalten. Der bestehende Container fasst zwei Badges nebeneinander; mit einem dritten und einem langen Modulnamen läuft er bei schmaler Karte über. Die Titelkürzung deckt nur den Titel ab, nicht die Badge-Reihe — deshalb bricht die Reihe um, statt aus der Karte zu laufen.
|
||||
|
||||
Der äussere Kartencontainer bekommt in diesem Zustand `opacity-60 cursor-not-allowed` und verliert die Hover-Effekte, damit sichtbar ist, dass die Karte nicht interaktiv ist. Ein Klick navigiert nicht zum Moduldetail, sondern löst über die bestehende Toast-Komponente den Text aus `marketplace.toastNoAccess` aus — wortgleich zur 403-Seite, damit derselbe Sachverhalt nicht zwei Formulierungen bekommt. Es entsteht kein neues Toast-System.
|
||||
|
||||
Für ADMIN und SUPER_ADMIN ist `hasAccess` bei jedem aktiven Modul wahr, weil die API den Rollen-Kurzschluss anwendet. Das Badge erscheint für sie damit nie, ohne dass die Karte selbst eine Rollenabfrage braucht.
|
||||
|
||||
Der Katalog bleibt vollständig sichtbar: nicht freigegebene Module werden gekennzeichnet, nicht ausgeblendet. Der Marketplace ist ein Schaufenster, und die Zugriffsentscheidung liegt bei der API.
|
||||
|
||||
Erweitere `MarketplaceCard.test.tsx` um Tests für: bei aktivem Modul ohne Zugriff erscheint das dritte Badge und der Klick löst den Toast statt einer Navigation aus; bei aktivem Modul mit Zugriff erscheint das Badge nicht und der Klick navigiert; bei nicht aktiviertem Modul bleibt das bisherige Verhalten unverändert; die Badge-Reihe trägt Umbruchverhalten.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>pnpm --filter @tessera/web test -- MarketplaceCard</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `pnpm --filter @tessera/web test -- MarketplaceCard` ist grün und enthält je einen Test für die vier oben genannten Fälle.
|
||||
- `grep -c 'hasAccess' "apps/web/src/app/(portal)/marketplace/components/MarketplaceCard.tsx"` gibt mindestens `3` aus (Props-Schnittstelle, Destrukturierung, Verwendung).
|
||||
- `grep -c 'statusNoAccess' "apps/web/src/app/(portal)/marketplace/components/MarketplaceCard.tsx"` gibt mindestens `1` aus.
|
||||
- `grep -c 'amber' "apps/web/src/app/(portal)/marketplace/components/MarketplaceCard.tsx"` gibt mindestens `2` aus (Hell- und Dunkelvariante).
|
||||
- `grep -c 'flex-wrap' "apps/web/src/app/(portal)/marketplace/components/MarketplaceCard.tsx"` gibt mindestens `1` aus.
|
||||
- `grep -c 'modules/catalog' "apps/web/src/app/(portal)/marketplace/page.tsx" "apps/web/src/app/(portal)/marketplace/[slug]/page.tsx"` belegt die Umstellung auf beiden Seiten.
|
||||
- `grep -c 'fetch(' "apps/web/src/app/(portal)/marketplace/page.tsx"` ist gegenüber dem Stand vor diesem Task um mindestens `1` gesunken — der zweigeteilte Abruf ist durch einen einzigen ersetzt.
|
||||
- Manuell im Browser als USER: ein aktiviertes Modul ohne Freigabe trägt in der Übersicht das Sperr-Badge, ist ausgegraut und zeigt beim Klick den Hinweis-Toast; dasselbe Modul als ADMIN trägt kein Badge und ist anklickbar. Bei sehr schmalem Fenster bleiben beide Badges innerhalb der Karte.
|
||||
- `pnpm --filter @tessera/web test` läuft vollständig grün und `pnpm --filter @tessera/web run build` schliesst fehlerfrei ab.
|
||||
</acceptance_criteria>
|
||||
<done>Der Marketplace unterscheidet sichtbar zwischen nicht aktiviert, aktiviert-ohne-Freigabe und zugänglich, zeigt gesperrte Module weiterhin an und lässt sie nicht öffnen.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| Browser-URL → Modulseiten-Route | Der Slug in der URL ist Benutzereingabe; die Route entscheidet serverseitig über Auslieferung |
|
||||
| Next.js-Server → API | Die Server-Action leitet ausschliesslich das Sitzungs-Cookie weiter, keinen selbst gebildeten Identitätsnachweis |
|
||||
| Marketplace-Karte → Anzeigezustand | Die Sperre der Karte ist Anzeige, nicht Durchsetzung |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|----------|-------------|-----------------|
|
||||
| T-15-05 | Elevation of Privilege | Frontend-seitiges Ausblenden als Sicherheitsgrenze behandeln | high | mitigate | Die Modulseite prüft serverseitig vor dem Rendern und liefert bei fehlender Freigabe kein Modul-Bundle aus; die eigentliche Durchsetzung bleibt der `ModuleGuard` auf jedem Modul-Endpoint. Sidebar- und Marketplace-Zustand sind ausdrücklich nur Spiegelung |
|
||||
| T-15-29 | Elevation of Privilege | Fehlerhafter Netzwerkpfad in `checkModuleAccess` öffnet die Seite | high | mitigate | Die Funktion schliesst im Zweifel: fehlendes Cookie, nicht erfolgreiche Antwort und geworfener Fehler ergeben alle ein negatives Ergebnis; zwei eigene Testfälle belegen das |
|
||||
| T-15-30 | Elevation of Privilege | Ein beliebiger Slug aus der URL löst einen Import aus | medium | mitigate | Die Whitelist-Prüfung der Modulregistrierung wandert unverändert in die Client-Hülle und bleibt eine von der Freigabeprüfung unabhängige zweite Absicherung |
|
||||
| T-15-08 | Information Disclosure | `GET /modules/catalog` zeigt jedem angemeldeten Benutzer den Modulkatalog | low | accept | Bewusste Entscheidung aus D-08: der Katalog bleibt Schaufenster und liefert ausschliesslich Modul-Metadaten sowie zwei Boolesche, keine Modulinhalte |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- `pnpm --filter @tessera/web test` vollständig grün.
|
||||
- `pnpm --filter @tessera/web run build` fehlerfrei.
|
||||
- Manuell als USER ohne Freigabe: Modul fehlt in der Sidebar, die direkte URL liefert die 403-Seite, die Marketplace-Karte trägt das Sperr-Badge und der Klick zeigt den Toast, und ein Aufruf des Modul-Endpoints der API antwortet mit HTTP 403. Alle vier Ebenen zeigen dasselbe Bild — das ist der Nachweis von PERM-04.
|
||||
- Manuell als ADMIN desselben Mandanten: alle vier Ebenen zeigen das Modul als zugänglich.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Ohne Freigabe fehlt das Modul in der Sidebar, ist die Modulseite serverseitig gesperrt und antwortet die Modul-API mit 403 (PERM-04).
|
||||
- Die 403-Seite nennt, was zu tun ist, statt nur zu sperren (D-07).
|
||||
- Der Marketplace kennzeichnet gesperrte Module, statt sie auszublenden (D-08).
|
||||
- Kein Zwischenzustand zeigt eine gesperrte Karte kurzzeitig als anklickbar.
|
||||
</success_criteria>
|
||||
|
||||
## Artifacts this phase produces
|
||||
|
||||
Von diesem Plan erzeugt beziehungsweise verändert:
|
||||
|
||||
- `checkModuleAccess` (`apps/web/src/lib/module-access-actions.ts`)
|
||||
- `apps/web/src/app/(portal)/modules/[category]/[moduleSlug]/page.tsx` — Umbau zur Server-Komponente mit 403-Zustand
|
||||
- `apps/web/src/app/(portal)/modules/[category]/[moduleSlug]/module-shell.tsx` — die ausgelagerte Client-Hülle
|
||||
- `apps/web/src/app/(portal)/modules/[category]/[moduleSlug]/module-access.test.tsx`
|
||||
- `MarketplaceCard` — Prop `hasAccess`, Sperr-Badge, nicht anklickbarer Zustand, Toast statt Navigation
|
||||
- `marketplace/page.tsx` und `marketplace/[slug]/page.tsx` — Umstellung auf `GET /modules/catalog`
|
||||
- `MarketplaceCard.test.tsx` — erweitert
|
||||
|
||||
Dieser Plan legt keine neuen i18n-Schlüssel an — alle verwendeten Schlüssel entstehen in Plan 15-06. Die phasenweite Gesamtliste steht in `15-01-PLAN.md`.
|
||||
|
||||
<output>
|
||||
Erstelle `.planning/phases/15-modul-berechtigungen-gruppen-user-grants/15-08-SUMMARY.md`, wenn der Plan abgeschlossen ist.
|
||||
</output>
|
||||
Reference in New Issue
Block a user