275 lines
27 KiB
Markdown
275 lines
27 KiB
Markdown
---
|
||
phase: 15-modul-berechtigungen-gruppen-user-grants
|
||
plan: 07
|
||
type: execute
|
||
wave: 4
|
||
depends_on: ["15-03", "15-06"]
|
||
files_modified:
|
||
- apps/web/src/app/(portal)/admin/modules/grants/page.tsx
|
||
- apps/web/src/app/(portal)/admin/modules/grants/grants-matrix.test.tsx
|
||
- apps/web/src/app/(portal)/admin/modules/page.tsx
|
||
- apps/web/src/app/(portal)/admin/modules/components/ActivateModuleDialog.tsx
|
||
- apps/web/src/app/(portal)/admin/users/page.tsx
|
||
- apps/web/src/app/(portal)/admin/users/components/UserAccessModal.tsx
|
||
- apps/web/src/app/(portal)/admin/users/user-access-modal.test.tsx
|
||
autonomous: true
|
||
requirements: [PERM-03]
|
||
user_setup: []
|
||
|
||
estimate:
|
||
tokens: 80000
|
||
raw_tokens: 80000
|
||
tasks: 3
|
||
confidence: low
|
||
|
||
must_haves:
|
||
truths:
|
||
- "Gruppenfreigaben werden in einer Matrix Module mal Gruppen gepflegt; ein Häkchen setzt oder entzieht die Freigabe sofort, der gesamte Berechtigungsstand ist auf einen Blick sichtbar (D-15, PERM-03)."
|
||
- "Die Matrix ist über einen Button im Kopf von /admin/modules erreichbar und liegt als Unterseite unter /admin/modules/grants — es entsteht kein siebter Eintrag in der Admin-Navigation."
|
||
- "Einzelfreigaben werden im Benutzer-Detail unter /admin/users gepflegt, zusammen mit der Anzeige dessen, was der Benutzer über seine Gruppen erbt (D-16, PERM-03)."
|
||
- "Die Gruppenmitgliedschaften im Benutzer-Detail sind nur lesbar — bearbeitet werden sie ausschliesslich unter /admin/groups, es gibt keine zweite Bearbeitungsoberfläche für dieselbe Beziehung (D-16)."
|
||
- "Aktiviert ein Admin ein Modul, fragt ein Dialog, ob es sofort für die Standardgruppe freigegeben oder erst konfiguriert werden soll (D-10)."
|
||
- "Besitzt der Mandant keine markierte Standardgruppe, ist die Sofort-Freigabe im Aktivierungsdialog deaktiviert und ein Hinweistext nennt den Grund (D-13)."
|
||
- "Ein einzelner Freigabe-Toggle bekommt keinen Bestätigungsdialog: der Klick ist sofort wirksam und sofort umkehrbar, es geht kein Datensatz verloren."
|
||
- "Die Matrix macht transparent, dass ADMIN und SUPER_ADMIN immer Zugriff auf alle aktiven Module haben und die Matrix nur die Rolle USER betrifft (D-03)."
|
||
- "In der Matrix und im Benutzer-Detail sind ausschliesslich mandantenweit aktive Module wählbar — ein Grant auf ein inaktives Modul wäre wirkungslos (D-02)."
|
||
# --- UI-SPEC 'UI Considerations' — covered ---
|
||
- "empty (Permission-Matrix ohne aktive Module): Der leere Zustand rendert die dokumentierte Copy mit einem Link zurück auf /admin/modules."
|
||
- "empty (User-Detail ohne Gruppenmitgliedschaft): Die Chip-Liste bleibt leer und der dokumentierte Hinweistext erscheint."
|
||
- "error (Grant-Speicherfehler): Die dokumentierte Fehlermeldung erscheint im bestehenden error-Div-Muster mit Statuscode-Anzeige."
|
||
- "populated (Matrix mit vielen Modulen und Gruppen): Sticky Kopfzeile, sticky erste Spalte, Gruppierung nach Modulkategorie und ein clientseitiges Suchfeld über Modul- und Gruppennamen."
|
||
- "long-text (Modulnamen in der ersten Matrix-Spalte): Die sticky erste Spalte hat eine feste Mindestbreite und kürzt mit Auslassung, der vollständige Name steht im title-Attribut."
|
||
- "partial (Matrix-Zelle und Direkt-Checkbox): Schlägt der Request nach dem optimistischen Toggle fehl, springt die Checkbox auf den Serverzustand zurück; es darf keinen Zustand geben, in dem die Oberfläche eine Freigabe anzeigt, die in der Datenbank nicht existiert."
|
||
# --- UI-SPEC 'UI Considerations' — backstop ---
|
||
- statement: "empty (User-Detail-Modul-Zugriff-Tabelle, wenn der Mandant keine aktiven Module hat): Statt eines leeren Tabellenrahmens ohne jeden Hinweis erscheint der Hinweistext admin.users.grants.noActiveModules."
|
||
verification: backstop
|
||
- statement: "overflow (lange Gruppennamen in der Matrix-Kopfzeile): Die Spaltenköpfe kürzen mit Auslassung und tragen den vollständigen Namen im title-Attribut, analog zur Titelkürzung der Marketplace-Karte."
|
||
verification: backstop
|
||
- statement: "long-text (Modulname im Aktivierungsdialog): Ein langer interpolierter Modulname bricht innerhalb des max-w-sm-Dialogs um, statt den Dialog zu verbreitern."
|
||
verification: backstop
|
||
artifacts:
|
||
- "apps/web/src/app/(portal)/admin/modules/grants/page.tsx — Freigabe-Matrix"
|
||
- "apps/web/src/app/(portal)/admin/modules/components/ActivateModuleDialog.tsx"
|
||
- "apps/web/src/app/(portal)/admin/users/components/UserAccessModal.tsx"
|
||
- "apps/web/src/app/(portal)/admin/modules/grants/grants-matrix.test.tsx"
|
||
- "apps/web/src/app/(portal)/admin/users/user-access-modal.test.tsx"
|
||
key_links:
|
||
- "Matrix-Zelle → POST/DELETE /module-grants — jede Zelle ist ein Grant-Datensatz, es gibt keinen Sammel-Speichern-Button"
|
||
- "ActivateModuleDialog → POST /modules/:id/activate gefolgt von POST /module-grants — die Sofort-Freigabe ist zwei Aufrufe, nicht ein neuer Endpoint"
|
||
- "UserAccessModal → GET /module-grants/users/:userId — geerbte und direkte Rechte kommen aus einer Antwort"
|
||
- "Alle Texte stammen aus den in Plan 15-06 angelegten i18n-Schlüsseln — dieser Plan legt keine neuen an"
|
||
---
|
||
|
||
<objective>
|
||
Die beiden Oberflächen, über die Freigaben tatsächlich vergeben werden: die Matrix Module mal Gruppen als Gesamtübersicht und das Benutzer-Detail für Einzelfreigaben samt Anzeige der über Gruppen geerbten Rechte. Dazu die Rückfrage beim Aktivieren eines Moduls.
|
||
|
||
Purpose: Ohne die Matrix müsste ein Admin jede Freigabe einzeln über ein Formular pflegen und hätte nie den Gesamtstand vor Augen. Die Anzeige der geerbten Rechte im Benutzer-Detail ist der Teil, ohne den nicht erkennbar ist, warum jemand Zugriff hat — und damit auch nicht, was ein Entzug tatsächlich bewirkt. Die Rückfrage beim Aktivieren verhindert den häufigsten Stolperstein des neuen Modells: ein Admin aktiviert ein Modul und wundert sich, warum es niemand sieht.
|
||
Output: Die Matrix-Unterseite, ein Aktivierungsdialog mit drei Aktionen, ein Benutzer-Detail-Dialog und zwei Komponenten-Testsuites.
|
||
</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: Freigabe-Matrix Module mal Gruppen</name>
|
||
<files>apps/web/src/app/(portal)/admin/modules/grants/page.tsx, apps/web/src/app/(portal)/admin/modules/grants/grants-matrix.test.tsx, apps/web/src/app/(portal)/admin/modules/page.tsx</files>
|
||
<read_first>
|
||
- apps/web/src/app/(portal)/admin/modules/page.tsx — Zeilen 37–49 (Datenladen), Zeilen 80–112 (`toggleModule`: optimistisches Toggle mit Rücksetzen im Fehlerfall) und Zeilen 134–138 (Fehler-Div)
|
||
- apps/web/src/app/(portal)/admin/ldap/page.tsx — Zeilen 707–713: das Suchfeld-Markup, das für die Matrix-Filterung übernommen wird
|
||
- apps/web/src/app/(portal)/admin/users/page.tsx — Zugriffsprüfung über den Auth-Store und Tabellenmarkup
|
||
- apps/web/src/messages/de.json — die in Plan 15-06 angelegten Schlüssel unter `adminModules.grants` und `admin.groups.grants.matrixCheckboxLabel`
|
||
- .planning/phases/15-modul-berechtigungen-gruppen-user-grants/15-UI-SPEC.md — Surface Contract 2 sowie die Abschnitte Spacing, Typography und Color
|
||
- .planning/phases/15-modul-berechtigungen-gruppen-user-grants/15-03-SUMMARY.md — das Antwortformat von `GET /module-grants/matrix` und die Signatur der Grant-Routen
|
||
</read_first>
|
||
<action>
|
||
Lege `apps/web/src/app/(portal)/admin/modules/grants/page.tsx` als Client-Komponente an, mit derselben Zugriffsanzeige über den Auth-Store wie die übrigen Admin-Seiten. Ergänze in `apps/web/src/app/(portal)/admin/modules/page.tsx` im Seitenkopf einen Button mit dem Text aus `adminModules.grantsLink`, der auf `/admin/modules/grants` verlinkt. Die Matrix bleibt bewusst eine Unterseite von Module und bekommt keinen eigenen Navigationseintrag — dasselbe Verhältnis wie zwischen einem Modul und seiner Einstellungsseite.
|
||
|
||
Die Seite lädt einmal `GET /module-grants/matrix` und bekommt daraus die drei Listen Module, Gruppen und bestehende Grant-Paare in einer Antwort.
|
||
|
||
Oberhalb der Tabelle ein Suchfeld im Markup des bestehenden Discovery-Suchfelds, das clientseitig sowohl Modul- als auch Gruppennamen filtert.
|
||
|
||
Die Tabelle liegt in einem Container mit `overflow-x-auto overflow-y-auto max-h-[70vh] rounded-md border border-border`. Die erste Spalte trägt die Modulnamen und ist mit `sticky left-0 bg-card z-10` fixiert, die Kopfzeile mit den Gruppennamen mit `sticky top-0 bg-card z-10`. Damit bleibt bei vielen Modulen und Gruppen immer erkennbar, welche Zelle zu welchem Paar gehört.
|
||
|
||
Sowohl die Modulnamen der ersten Spalte als auch die Gruppennamen der Kopfzeile bekommen eine feste Mindestbreite, kürzen überlaufenden Text mit `truncate` und tragen den vollständigen Namen im `title`-Attribut — nach dem Vorbild der Titelkürzung in der Marketplace-Karte. Ohne das verbreitert ein langer Gruppenname die Matrix ins Unlesbare.
|
||
|
||
Die Zeilen sind nach Modulkategorie gruppiert: je Kategorie eine volle Breite überspannende Zwischenzeile mit `bg-muted/50` und dem Kategorienamen, darunter die Modulzeilen dieser Kategorie. Die Sortierung kommt aus der API-Antwort und wird im Browser nicht erneut umsortiert.
|
||
|
||
Jede Zelle enthält ein natives Kontrollkästchen ohne sichtbaren Text. Weil die Zelle selbst keinen Text trägt, bekommt jedes Kästchen ein `aria-label` aus `admin.groups.grants.matrixCheckboxLabel` mit Modul- und Gruppennamen als Interpolationsparameter — sonst ist die Matrix mit einem Screenreader nicht bedienbar.
|
||
|
||
Ein Klick schaltet sofort um: der Zustand wird optimistisch aktualisiert, im Hintergrund läuft `POST /module-grants` beziehungsweise `DELETE /module-grants` mit moduleId und groupId. Während des Requests zeigt ausschliesslich die betroffene Zelle einen Inline-Spinner, gesteuert über einen Zellen-Schlüssel aus moduleId und groupId — kein Vollseiten-Ladezustand. Schlägt der Request fehl, springt die Checkbox auf den Serverzustand zurück UND die Fehlermeldung aus `adminModules.grants.saveError` erscheint im bestehenden Fehler-Div-Muster mit Statuscode. Beides zusammen ist verpflichtend: es darf keinen Zustand geben, in dem die Oberfläche eine Freigabe anzeigt, die in der Datenbank nicht existiert.
|
||
|
||
Ein einzelner Toggle bekommt keinen Bestätigungsdialog. Der Klick ist sofort umkehrbar und es geht kein Datensatz verloren — anders als beim Löschen einer Gruppe, das deshalb dort einen Dialog hat.
|
||
|
||
Unter der Tabelle steht eine Fussnote in 12 Pixel und `text-muted-foreground` mit dem Text aus `adminModules.grants.adminNote`: dass ADMIN und SUPER_ADMIN immer Zugriff auf alle aktiven Module haben und die Matrix nur die Rolle USER betrifft. Ohne diesen Hinweis entsteht regelmässig die Frage, warum Admin-Konten in der Matrix keine Rolle spielen.
|
||
|
||
Hat der Mandant keine aktiven Module, rendert die Seite statt der Tabelle den leeren Zustand aus `adminModules.grants.emptyModules` mit einem Link auf `/admin/modules`.
|
||
|
||
Lege `grants-matrix.test.tsx` an mit Tests für: befüllte Matrix mit korrekt vorbelegten Kästchen, leerer Zustand ohne aktive Module, Rücksprung der Checkbox nach fehlgeschlagenem Request samt sichtbarer Fehlermeldung, Filterung über das Suchfeld, und das Vorhandensein eines `aria-label` an jedem Kästchen.
|
||
</action>
|
||
<verify>
|
||
<automated>pnpm --filter @tessera/web test -- grants-matrix</automated>
|
||
</verify>
|
||
<acceptance_criteria>
|
||
- `pnpm --filter @tessera/web test -- grants-matrix` ist grün und enthält je einen Test für die fünf oben genannten Fälle.
|
||
- `grep -c 'sticky left-0' "apps/web/src/app/(portal)/admin/modules/grants/page.tsx"` gibt mindestens `1` aus und `grep -c 'sticky top-0' "apps/web/src/app/(portal)/admin/modules/grants/page.tsx"` gibt mindestens `1` aus.
|
||
- `grep -c 'truncate' "apps/web/src/app/(portal)/admin/modules/grants/page.tsx"` gibt mindestens `2` aus (erste Spalte und Kopfzeile) und `grep -c 'title=' "apps/web/src/app/(portal)/admin/modules/grants/page.tsx"` gibt mindestens `2` aus.
|
||
- `grep -c 'aria-label' "apps/web/src/app/(portal)/admin/modules/grants/page.tsx"` gibt mindestens `1` aus.
|
||
- `grep -c 'adminNote' "apps/web/src/app/(portal)/admin/modules/grants/page.tsx"` gibt mindestens `1` aus.
|
||
- `grep -c 'grantsLink' "apps/web/src/app/(portal)/admin/modules/page.tsx"` gibt mindestens `1` aus.
|
||
- `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 gesamte Berechtigungsstand ist als Matrix sichtbar und jede Zelle setzt oder entzieht eine Freigabe sofort, ohne dass die Oberfläche je einen Zustand zeigt, den die Datenbank nicht kennt.</done>
|
||
</task>
|
||
|
||
<task type="auto">
|
||
<name>Task 2: Aktivierungsdialog mit drei Aktionen in /admin/modules</name>
|
||
<files>apps/web/src/app/(portal)/admin/modules/components/ActivateModuleDialog.tsx, apps/web/src/app/(portal)/admin/modules/page.tsx, apps/web/src/app/(portal)/admin/modules/grants/grants-matrix.test.tsx</files>
|
||
<read_first>
|
||
- apps/web/src/app/(portal)/admin/modules/page.tsx — `toggleModule` (Zeilen 80–112) und der Aktivieren-Button, dessen Verhalten hier verzweigt wird
|
||
- apps/web/src/app/(portal)/marketplace/components/ActivationDialog.tsx — das bestehende Dialog-Markup dieses Projekts, dessen Overlay- und Container-Stil übernommen wird
|
||
- apps/web/src/messages/de.json — die in Plan 15-06 angelegten Schlüssel unter `adminModules.activationDialog`
|
||
- .planning/phases/15-modul-berechtigungen-gruppen-user-grants/15-UI-SPEC.md — Surface Contract 6
|
||
- .planning/phases/15-modul-berechtigungen-gruppen-user-grants/15-03-SUMMARY.md — die Signatur von `POST /module-grants`
|
||
- .planning/phases/15-modul-berechtigungen-gruppen-user-grants/15-02-SUMMARY.md — das Antwortformat von `GET /groups`, aus dem die markierte Standardgruppe abgelesen wird
|
||
</read_first>
|
||
<action>
|
||
Lege `apps/web/src/app/(portal)/admin/modules/components/ActivateModuleDialog.tsx` an, im Overlay-Muster `fixed inset-0 z-50 flex items-center justify-center bg-black/50` mit einem Container `w-full max-w-sm rounded-lg border border-border bg-card p-6 shadow-lg` — derselbe Stil und dieselbe Breite wie der bestehende Dialog des Marketplace.
|
||
|
||
Titel aus `adminModules.activationDialog.title` mit dem Modulnamen als Interpolationsparameter, darunter der Fragetext aus `.body`. Sowohl Titel als auch Fliesstext bekommen Umbruchverhalten, damit ein langer Modulname den schmalen Dialog nicht verbreitert, sondern in die nächste Zeile läuft.
|
||
|
||
Drei Aktionen von links nach rechts: Abbrechen als Rahmen-Button, der komplett abbricht und das Modul inaktiv lässt; `configureLater` als Rahmen-Button, der ausschliesslich `POST /modules/:id/activate` aufruft; `grantNow` als primärer Button in der Akzentfarbe, der `POST /modules/:id/activate` aufruft und danach `POST /module-grants` mit der moduleId und der groupId der markierten Standardgruppe.
|
||
|
||
Die Sofort-Freigabe ist bewusst zwei Aufrufe hintereinander und kein neuer kombinierter Endpoint — die beiden Vorgänge sind fachlich getrennt und sollen es bleiben.
|
||
|
||
Der Dialog lädt beim Öffnen `GET /groups` und sucht darin die Gruppe mit gesetzter Standardmarkierung. Existiert keine, ist `grantNow` deaktiviert und darunter erscheint der Hinweistext aus `.noDefaultGroupHint`, der den Admin auf die Gruppenverwaltung verweist. Die Markierung darf laut D-13 abgeschaltet sein, und der Dialog muss diesen Zustand tragen können, statt einen wirkungslosen Button anzubieten.
|
||
|
||
Verzweige `toggleModule` in `page.tsx`: die Deaktivierung bleibt unverändert im bestehenden Pfad, die Aktivierung öffnet künftig diesen Dialog statt direkt zu aktivieren. Nach jeder der beiden aktivierenden Aktionen wird der Aktivierungszustand wie bisher optimistisch aktualisiert und die Sidebar-Aktualisierung angestossen; im Fehlerfall wird zurückgesetzt und die Meldung im bestehenden Fehler-Div angezeigt.
|
||
|
||
Ergänze `grants-matrix.test.tsx` oder eine eigene Testdatei um Tests für: alle drei Buttons sind vorhanden; bei fehlender Standardgruppe ist die Sofort-Freigabe deaktiviert und der Hinweistext sichtbar; ein Klick auf die Sofort-Freigabe löst beide Aufrufe in der richtigen Reihenfolge aus.
|
||
</action>
|
||
<verify>
|
||
<automated>pnpm --filter @tessera/web test -- grants-matrix</automated>
|
||
</verify>
|
||
<acceptance_criteria>
|
||
- Die drei Tests aus dem Action-Abschnitt sind vorhanden und grün.
|
||
- `grep -c 'max-w-sm' "apps/web/src/app/(portal)/admin/modules/components/ActivateModuleDialog.tsx"` gibt mindestens `1` aus.
|
||
- `grep -c 'noDefaultGroupHint' "apps/web/src/app/(portal)/admin/modules/components/ActivateModuleDialog.tsx"` gibt mindestens `1` aus.
|
||
- `grep -c 'disabled' "apps/web/src/app/(portal)/admin/modules/components/ActivateModuleDialog.tsx"` gibt mindestens `1` aus.
|
||
- `grep -c 'break-words\|break-all\|wrap' "apps/web/src/app/(portal)/admin/modules/components/ActivateModuleDialog.tsx"` gibt mindestens `1` aus — der Modulname bricht um statt zu verbreitern.
|
||
- `grep -c 'ActivateModuleDialog' "apps/web/src/app/(portal)/admin/modules/page.tsx"` gibt mindestens `2` aus (Import und Verwendung).
|
||
- `pnpm --filter @tessera/web test` läuft vollständig grün und `pnpm --filter @tessera/web run build` schliesst fehlerfrei ab.
|
||
</acceptance_criteria>
|
||
<done>Das Aktivieren eines Moduls fragt nach, ob sofort für die Standardgruppe freigegeben werden soll, und trägt den Fall, dass gar keine Standardgruppe markiert ist.</done>
|
||
</task>
|
||
|
||
<task type="auto">
|
||
<name>Task 3: Benutzer-Detail mit geerbten Rechten und Direkt-Freigaben</name>
|
||
<files>apps/web/src/app/(portal)/admin/users/components/UserAccessModal.tsx, apps/web/src/app/(portal)/admin/users/page.tsx, apps/web/src/app/(portal)/admin/users/user-access-modal.test.tsx</files>
|
||
<read_first>
|
||
- apps/web/src/app/(portal)/admin/users/page.tsx — die komplette Datei: die bestehenden Aktionsbuttons je Zeile (Zeilen 246–262), das Modal-Overlay-Muster (Zeilen 271–273) und die Fetch-Behandlung
|
||
- apps/web/src/app/(portal)/admin/ldap/page.tsx — Zeilen 957–974: das Chip-Markup, hier ohne Entfernen-Button verwendet
|
||
- apps/web/src/app/(portal)/admin/modules/grants/page.tsx — das in Task 1 entstandene optimistische Zellen-Toggle, dessen Verhalten die Direkt-Checkbox eins zu eins übernimmt
|
||
- apps/web/src/messages/de.json — die in Plan 15-06 angelegten Schlüssel unter `admin.users.grants`
|
||
- .planning/phases/15-modul-berechtigungen-gruppen-user-grants/15-UI-SPEC.md — Surface Contract 3
|
||
- .planning/phases/15-modul-berechtigungen-gruppen-user-grants/15-03-SUMMARY.md — das Antwortformat von `GET /module-grants/users/:userId`
|
||
</read_first>
|
||
<action>
|
||
Ergänze in `apps/web/src/app/(portal)/admin/users/page.tsx` je Tabellenzeile einen weiteren Aktionsbutton mit dem Text aus `admin.users.grants.detailsButton`, im selben Stil `px-2 py-1 text-xs` wie die bestehenden Buttons. Er öffnet die neue Komponente `components/UserAccessModal.tsx`.
|
||
|
||
Der Dialog nutzt dasselbe Overlay-Muster wie die bestehenden Modale, aber mit `max-w-2xl` statt `max-w-md`, weil er mehr Inhalt trägt. Es entsteht kein neues Dialog-Primitiv und kein Slide-over. Titel aus `admin.users.grants.modalTitle` mit dem Benutzernamen.
|
||
|
||
Der Dialog lädt einmal `GET /module-grants/users/:userId` und rendert daraus zwei Abschnitte.
|
||
|
||
Erster Abschnitt Gruppenmitgliedschaften: eine Chip-Liste im Markup der bestehenden Ausschlussliste der LDAP-Seite, aber ohne Entfernen-Button. Die Mitgliedschaften werden ausschliesslich unter `/admin/groups` bearbeitet — eine zweite Bearbeitungsoberfläche für dieselbe Beziehung würde zwei Wahrheiten über den Bearbeitungsort schaffen. Ist der Benutzer in keiner Gruppe, erscheint der Hinweistext aus `admin.users.grants.noGroups`.
|
||
|
||
Zweiter Abschnitt Modul-Zugriff: eine Tabelle mit drei Spalten. Modul; über welche Gruppen der Benutzer das Modul erbt, als Chip-Liste der Gruppennamen oder als Gedankenstrich, wenn er es über keine Gruppe erbt; und Direkt als Kontrollkästchen für einen direkten Grant. Gelistet werden ausschliesslich mandantenweit aktive Module, weil ein Grant ohne Aktivierung wirkungslos wäre.
|
||
|
||
Die Anzeige der geerbten Rechte ist der eigentliche Zweck dieses Dialogs. Ohne sie ist nicht erkennbar, warum ein Benutzer Zugriff hat, und ein Admin könnte einen Direkt-Grant entziehen und sich wundern, dass der Zugriff bestehen bleibt — weil er über eine Gruppe weiterhin besteht.
|
||
|
||
Das Direkt-Kästchen verhält sich identisch zur Matrix-Zelle: optimistisches Umschalten, `POST /module-grants` beziehungsweise `DELETE /module-grants` mit moduleId und userId im Hintergrund, Inline-Spinner nur in der betroffenen Zeile, Rücksprung auf den Serverzustand plus sichtbare Fehlermeldung aus `admin.users.grants.saveError` im Fehlerfall. Weil die Zelle keinen sichtbaren Text trägt, bekommt jedes Kästchen ein `aria-label` aus `admin.users.grants.directCheckboxLabel` mit Modul- und Benutzernamen.
|
||
|
||
Hat der Mandant überhaupt keine aktiven Module, erscheint statt eines leeren Tabellenrahmens der Hinweistext aus `admin.users.grants.noActiveModules`.
|
||
|
||
Lege `user-access-modal.test.tsx` an mit Tests für: Chip-Liste mit Gruppennamen und der Leerzustand; Modultabelle mit einem geerbten und einem nicht geerbten Modul; Rücksprung der Direkt-Checkbox nach fehlgeschlagenem Request samt sichtbarer Fehlermeldung; der Hinweistext bei einem Mandanten ohne aktive Module; das Vorhandensein eines `aria-label` an jedem Direkt-Kästchen.
|
||
</action>
|
||
<verify>
|
||
<automated>pnpm --filter @tessera/web test -- user-access-modal</automated>
|
||
</verify>
|
||
<acceptance_criteria>
|
||
- `pnpm --filter @tessera/web test -- user-access-modal` ist grün und enthält je einen Test für die fünf oben genannten Fälle.
|
||
- `grep -c 'max-w-2xl' "apps/web/src/app/(portal)/admin/users/components/UserAccessModal.tsx"` gibt mindestens `1` aus.
|
||
- `grep -c 'noActiveModules' "apps/web/src/app/(portal)/admin/users/components/UserAccessModal.tsx"` gibt mindestens `1` aus.
|
||
- `grep -c 'directCheckboxLabel' "apps/web/src/app/(portal)/admin/users/components/UserAccessModal.tsx"` gibt mindestens `1` aus.
|
||
- `grep -c 'module-grants/users' "apps/web/src/app/(portal)/admin/users/components/UserAccessModal.tsx"` gibt mindestens `1` aus — geerbte und direkte Rechte kommen aus einer Antwort.
|
||
- `grep -c 'detailsButton' "apps/web/src/app/(portal)/admin/users/page.tsx"` gibt mindestens `1` aus.
|
||
- `pnpm --filter @tessera/web test` läuft vollständig grün und `pnpm --filter @tessera/web run build` schliesst fehlerfrei ab.
|
||
</acceptance_criteria>
|
||
<done>Im Benutzer-Detail ist auf einen Blick sichtbar, was der Benutzer über Gruppen erbt und was ihm zusätzlich direkt gegeben wurde, und Einzelfreigaben lassen sich dort setzen und entziehen.</done>
|
||
</task>
|
||
|
||
</tasks>
|
||
|
||
<threat_model>
|
||
## Trust Boundaries
|
||
|
||
| Boundary | Description |
|
||
|----------|-------------|
|
||
| Browser → Grant-API | Jede Matrix-Zelle und jedes Direkt-Kästchen schickt moduleId plus groupId oder userId; die Mandantenprüfung passiert ausschliesslich serverseitig in Plan 15-03 |
|
||
| Optimistische Oberfläche → Serverzustand | Zwischen Klick und Antwort zeigt die Oberfläche kurzzeitig einen noch nicht bestätigten Zustand |
|
||
|
||
## STRIDE Threat Register
|
||
|
||
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
||
|-----------|----------|-----------|----------|-------------|-----------------|
|
||
| T-15-25 | Tampering | Die Oberfläche zeigt dauerhaft eine Freigabe an, die der Server abgelehnt hat | high | mitigate | Jeder fehlgeschlagene Request setzt die Checkbox auf den Serverzustand zurück UND zeigt eine sichtbare Fehlermeldung; je ein Testfall in beiden Testsuites belegt den Rücksprung. Ein optimistischer Zustand ohne Korrekturpfad wäre eine falsche Aussage über die Rechtelage |
|
||
| T-15-26 | Elevation of Privilege | Die clientseitige Rollenanzeige als Sicherheitsgrenze missverstehen | high | mitigate | Beide Seiten blenden nur aus; `RolesGuard` mit `@Roles(ADMIN, SUPER_ADMIN)` aus Plan 15-03 ist der Durchsetzungspunkt jeder aufgerufenen Route |
|
||
| T-15-27 | Tampering | Ein Grant auf ein mandantenweit inaktives Modul | medium | mitigate | Matrix und Benutzer-Detail listen ausschliesslich aktive Module; zusätzlich lehnt der Service aus Plan 15-03 einen Grant auf ein inaktives Modul mit HTTP 400 ab |
|
||
| T-15-28 | Repudiation | Ein Admin entzieht einen Direkt-Grant und hält den Zugriff für beendet, obwohl er über eine Gruppe fortbesteht | medium | mitigate | Die Spalte mit den gewährenden Gruppen steht direkt neben dem Direkt-Kästchen, sodass die Vererbung beim Entzug sichtbar ist (D-16) |
|
||
</threat_model>
|
||
|
||
<verification>
|
||
- `pnpm --filter @tessera/web test` vollständig grün.
|
||
- `pnpm --filter @tessera/web run build` fehlerfrei.
|
||
- Manuell im Browser: in der Matrix eine Freigabe setzen und wieder entziehen und die Wirkung mit einem Testbenutzer gegenprüfen; ein Modul aktivieren und beide Dialogwege durchspielen; im Benutzer-Detail einen Direkt-Grant setzen, während derselbe Zugriff über eine Gruppe besteht, und prüfen, dass die Gruppenspalte das anzeigt.
|
||
- Manuell mit gestoppter API: eine Matrix-Zelle und ein Direkt-Kästchen anklicken und prüfen, dass beide zurückspringen und eine Fehlermeldung erscheint.
|
||
- Manuell mit einem sehr langen Gruppen- und Modulnamen: Matrix-Kopfzeile und Aktivierungsdialog bleiben in ihren Massen.
|
||
</verification>
|
||
|
||
<success_criteria>
|
||
- Gruppenfreigaben werden in der Matrix gepflegt, Einzelfreigaben im Benutzer-Detail, beide mit sofort wirksamem Toggle (PERM-03, D-15, D-16).
|
||
- Die geerbten Rechte sind im Benutzer-Detail sichtbar.
|
||
- Das Aktivieren eines Moduls fragt nach der Sofort-Freigabe und trägt den Fall ohne Standardgruppe (D-10, D-13).
|
||
- Kein optimistischer Zustand überlebt einen fehlgeschlagenen Request.
|
||
</success_criteria>
|
||
|
||
## Artifacts this phase produces
|
||
|
||
Von diesem Plan erzeugt beziehungsweise verändert:
|
||
|
||
- Route `/admin/modules/grants` (`apps/web/src/app/(portal)/admin/modules/grants/page.tsx`)
|
||
- `ActivateModuleDialog` (`apps/web/src/app/(portal)/admin/modules/components/ActivateModuleDialog.tsx`)
|
||
- `UserAccessModal` (`apps/web/src/app/(portal)/admin/users/components/UserAccessModal.tsx`)
|
||
- `apps/web/src/app/(portal)/admin/modules/page.tsx` — Button zur Matrix und Verzweigung von `toggleModule`
|
||
- `apps/web/src/app/(portal)/admin/users/page.tsx` — vierter Aktionsbutton je Zeile
|
||
- `grants-matrix.test.tsx` und `user-access-modal.test.tsx`
|
||
|
||
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-07-SUMMARY.md`, wenn der Plan abgeschlossen ist.
|
||
</output>
|