From 9d52aeab67aa7fb6fe51b6ae3bd532ec66b73a43 Mon Sep 17 00:00:00 2001 From: Lars Date: Sun, 7 Jun 2026 15:27:37 +0200 Subject: [PATCH] Update Membership RBAC Decisions and Enhance Admin Rights Management - Updated the Membership RBAC Decisions document to reflect the latest implementation status and roadmap, including new features and enhancements. - Incremented application version to 0.8.200 and updated database schema version to 20260606083. - Added a new API endpoint to clear capability grants for club roles, improving admin rights management. - Enhanced the Admin Rights page in the frontend to display enforcement status and feature consumption details for capabilities. - Improved the user interface for better clarity on rights and capabilities management. --- .../MEMBERSHIP_RBAC_DECISIONS_2026-06.md | 57 +++-- backend/capability_enforcement_audit.py | 94 ++++++++ backend/routers/admin_rights.py | 33 ++- backend/version.py | 7 +- docs/working/RBAC_ENFORCEMENT_ROADMAP.md | 156 +++++++++++++ frontend/src/pages/AdminRightsPage.jsx | 217 +++++++++--------- frontend/src/utils/api.js | 8 + 7 files changed, 448 insertions(+), 124 deletions(-) create mode 100644 backend/capability_enforcement_audit.py create mode 100644 docs/working/RBAC_ENFORCEMENT_ROADMAP.md diff --git a/.claude/docs/technical/MEMBERSHIP_RBAC_DECISIONS_2026-06.md b/.claude/docs/technical/MEMBERSHIP_RBAC_DECISIONS_2026-06.md index df6d355..f86ac2f 100644 --- a/.claude/docs/technical/MEMBERSHIP_RBAC_DECISIONS_2026-06.md +++ b/.claude/docs/technical/MEMBERSHIP_RBAC_DECISIONS_2026-06.md @@ -99,8 +99,8 @@ Env-Schalter: `ACCOUNT_GATE_ENFORCE` (Default `1`, Endpoint-Helfer), `ACCOUNT_GA ## 2. Implementierungsstand (Ist, Codebase) -**DB-Schema:** `20260606079` (`backend/version.py`) -**Deploy-Referenz:** Dev mit M2-Logging verifiziert (`club-feature-usage.log`). +**DB-Schema:** `20260606083` · App **0.8.199** (`backend/version.py`) +**Roadmap (detailliert):** `docs/working/RBAC_ENFORCEMENT_ROADMAP.md` ### M1 — Feature-Schema v9c ✅ @@ -120,6 +120,7 @@ Env-Schalter: `ACCOUNT_GATE_ENFORCE` (Default `1`, Endpoint-Helfer), `ACCOUNT_GA | `club_feature_logger.py` → `club-feature-usage.log` | ✅ | | `probe_club_feature_access()` | ✅ | | Hooks: KI-Endpoints, `POST /exercises`, Medien-Upload, Planungs-KI | ✅ | +| Consume-Standard + `feature_usage` in Response (`ai_calls`) | ✅ | | `CLUB_FEATURE_ENFORCE=0` (Default) | ✅ | ### M3 — Account-Lifecycle + Capability-Grants ⚠️ teilweise @@ -134,23 +135,44 @@ Env-Schalter: `ACCOUNT_GATE_ENFORCE` (Default `1`, Endpoint-Helfer), `ACCOUNT_GA | Account-Gates auf **Schreib-/KI-Endpoints** | ✅ | Lesepfade für Bewerber noch offen | | `CAPABILITY_ENFORCE=0` (nur Log) | ✅ | — | | Onboarding UX: nur Bewerbung/Gründung | ✅ | Phase A: API-Middleware + `/onboarding` + reduzierte Nav | -| `club_creation_requests` (M7) | ❌ | — | +| `club_creation_requests` (M7) | ✅ Basis | Capabilities + Admin-Freigabe | +| Quota-Bypass via Capability-Grants (083) | ✅ | kein paralleles Exemption-Schema | | Custom Roles / Co-Trainer | ❌ | bewusst v2 | | Legacy-Helfer entfernt | ❌ | bewusst parallel | -### Bewusst zurückgestellt (Roadmap) +### M4 — Anzeige ✅ teilweise + +| Deliverable | Status | +|-------------|--------| +| `GET /api/me/entitlements` | ✅ | +| `EntitlementsContext`, `hasCapability()` | ✅ (UI nutzt noch kaum) | +| `FeatureUsageBadge` | ✅ nur KI im Übungsformular | +| `featureUsageSync` in `request()` | ✅ | + +### M5 — Hard-Block + vollständiger Verbrauch ⚠️ + +| Deliverable | Status | +|-------------|--------| +| `consume_club_feature_with_usage` Standard | ✅ `ai_calls` | +| `CLUB_FEATURE_ENFORCE=1` produktiv | ❌ Default 0 | +| Consume `exercises`, `exercise_media`, … | ❌ | + +### M6 — Admin UI Rollen & Rechte ⚠️ + +| Deliverable | Status | +|-------------|--------| +| `/admin/rights` Capability-Matrix (Portal + Verein) | ✅ | +| Klartext zuerst, Enforcement-Badge | ✅ 2026-06-07 | +| Kontingent-Bypass + Vereinspläne (Seed) | ✅ | +| Neue Pläne / Rollen anlegen (CRUD) | ❌ | + +### Bewusst zurückgestellt | ID | Inhalt | |----|--------| | M0 | CI-Isolation / Test-DB | -| M5 | Hard-Block Kontingente | -| M6 | Superadmin Admin-UI (Pläne, Capability-Matrix) | -| M7 | Vereinsgründung beantragen | | M8 | Stripe | - -### Hinweis: M4 im Repo (über M3 hinaus) - -Falls bereits deployed: `GET /api/me/entitlements`, `EntitlementsContext`, `FeatureUsageBadge` — gehört zur **Anzeige-Phase 3**, nicht zum M3-Kern. Siehe `entitlements` Modul v1.0.0. +| v2 | Trainer-Budgets, Custom Roles | --- @@ -178,9 +200,9 @@ Request | **A** | **Onboarding-Gates vollständig** | ✅ umgesetzt (API + Frontend `/onboarding`) | | **B** | **M7 Vereinsgründung beantragen** | **Als Nächstes** — zweiter Pfad für `verified_pending_club` | | **C** | **M5 Hard-Block `ai_calls`** | Free-Plan `0` wird real; Badge (M4) liefert Erklärung | -| **D** | **M6 Superadmin-UI** | Pläne + Capability-Matrix ohne SQL | -| **E** | Systemrolle `co_trainer` + Entitlements im Frontend | Entscheidung 1.2 risikoarm | -| **F** | Member-Budgets (v2) | Entscheidung 1.4 | +| **D** | **M6 voll** | Pläne-CRUD, Rollen-CRUD | ⚠️ Matrix da | +| **E** | Entitlements im Frontend (`hasCapability`) | Entscheidung 1.2 risikoarm | +| **F** | `co_trainer` + Member-Budgets (v2) | Entscheidung 1.4 | M0 parallel, nicht blockierend. @@ -204,7 +226,14 @@ M0 parallel, nicht blockierend. | `backend/capabilities.py` | Capability-Auflösung | | `backend/account_lifecycle.py` | Account-Gates | +## 7. Superadmin im Verein (FAQ) + +Siehe **`docs/working/RBAC_ENFORCEMENT_ROADMAP.md` §4**: Plattform-Admin (`admin`, `superadmin`) erhält **Capability-Bypass** für Vereins-Funktionen ohne `club_admin`-Mitgliedschaft. Mandant über aktiven Verein wählen; Kontingente via Bypass. Einzelne Legacy-Pfade (z. B. Löschen `visibility=club`) sind noch nicht vereinheitlicht — Ziel Phase 3. + +--- + **Changelog** - 2026-06-06: Initial — Entscheidungen Onboarding, Rollen-Risiko, Kontingente, Trainer-Budget v2; Ist-Stand M1–M3; Roadmap A–F. - 2026-06-06: Phase A — `account_onboarding_gate.py`, Frontend `/onboarding`, reduzierte Navigation. +- 2026-06-07: M4–M6 Ist-Stand, Roadmap-Verweis, Superadmin-FAQ; Admin-Matrix UX + Enforcement-Audit. diff --git a/backend/capability_enforcement_audit.py b/backend/capability_enforcement_audit.py new file mode 100644 index 0000000..36c1158 --- /dev/null +++ b/backend/capability_enforcement_audit.py @@ -0,0 +1,94 @@ +""" +Audit: Welche Capabilities sind an Endpoints angebunden? + +Für Admin-Matrix (Rollen & Rechte) und Roadmap — bei neuem probe_capability hier eintragen. +""" +from __future__ import annotations + +from typing import Any, Dict + +# Endpoints rufen probe_capability auf (Log; Block nur bei CAPABILITY_ENFORCE=1) +WIRED_PROBE = frozenset( + { + "exercises.ai.suggest", + "exercises.ai.regenerate", + "exercises.create", + "exercises.media.upload", + "planning.ai.suggest", + "planning.ai.progression_path", + "club.creation_request.read_own", + "club.creation_request.create", + "club.creation_request.withdraw", + "platform.club_creation.approve", + } +) + +# Kontingent-Verbrauch nach Erfolg (consume_club_feature_with_usage) +FEATURE_CONSUME_WIRED = frozenset( + { + "ai_calls", + } +) + + +def enforcement_status_for_capability(capability_id: str) -> Dict[str, Any]: + """ + Anzeige-Status für Superadmin-Matrix. + + level: probe | legacy | platform | open | none + """ + cid = (capability_id or "").strip() + if cid in WIRED_PROBE: + return { + "level": "probe", + "label": "API vorbereitet (Log)", + "detail": "probe_capability am Endpoint; Hard-Block erst mit CAPABILITY_ENFORCE=1", + "implemented": True, + } + if cid.startswith("platform."): + if cid == "platform.admin.access": + return { + "level": "platform", + "label": "Plattform (Router-Guard)", + "detail": "RequireAdmin / Superadmin-Checks", + "implemented": True, + } + if cid in WIRED_PROBE: + pass + return { + "level": "platform", + "label": "Plattform (teilweise)", + "detail": "Meist Router-Guard; Capability-Probe nur wo eingetragen", + "implemented": cid in WIRED_PROBE, + } + if cid.startswith("club."): + return { + "level": "open", + "label": "Onboarding", + "detail": "Account-State / eigene Flows", + "implemented": cid in WIRED_PROBE, + } + # Vereins-Capabilities ohne Probe: Legacy club_tenancy (can_plan_in_club, has_club_role, …) + return { + "level": "legacy", + "label": "Nur Legacy-Rollen", + "detail": "Noch kein probe_capability — prüft can_plan_in_club / club_admin im Code", + "implemented": False, + } + + +def feature_consume_status(feature_id: str) -> Dict[str, Any]: + fid = (feature_id or "").strip() + if fid in FEATURE_CONSUME_WIRED: + return { + "level": "consume", + "label": "Verbrauch aktiv", + "detail": "consume_club_feature_with_usage + feature_usage in Response", + "implemented": True, + } + return { + "level": "inventory", + "label": "Bestand / Probe", + "detail": "Probe oder Live-Zählung; kein Consume nach Aktion", + "implemented": False, + } diff --git a/backend/routers/admin_rights.py b/backend/routers/admin_rights.py index 585df13..ebd5d3c 100644 --- a/backend/routers/admin_rights.py +++ b/backend/routers/admin_rights.py @@ -15,6 +15,10 @@ from club_quota_bypass import ( list_quota_bypass_grants, quota_bypass_capability_id_for_feature, ) +from capability_enforcement_audit import ( + enforcement_status_for_capability, + feature_consume_status, +) from club_tenancy import is_superadmin from db import get_db, get_cursor, r2d @@ -94,7 +98,13 @@ def get_capability_matrix(session: dict = Depends(require_auth)): ORDER BY domain, id """ ) - capabilities = [r2d(r) for r in cur.fetchall()] + capabilities = [] + for row in cur.fetchall(): + cap = r2d(row) + cap["enforcement"] = enforcement_status_for_capability(cap.get("id")) + if cap.get("linked_feature_id"): + cap["feature_consume"] = feature_consume_status(cap["linked_feature_id"]) + capabilities.append(cap) cur.execute( """ @@ -224,6 +234,27 @@ def add_club_role_capability_grant( return r2d(row) +@router.delete("/capability-grants/club-roles/by-capability") +def clear_club_capability_grants( + capability_id: str = Query(...), + session: dict = Depends(require_auth), +): + """Alle Rollen-Grants einer Capability entfernen → wieder offen für alle Mitglieder.""" + _require_superadmin(session) + cap_id = capability_id.strip() + with get_db() as conn: + cur = get_cursor(conn) + cur.execute( + """ + DELETE FROM club_role_capability_grants + WHERE capability_id = %s + """, + (cap_id,), + ) + conn.commit() + return {"ok": True, "capability_id": cap_id} + + @router.delete("/capability-grants/club-roles") def delete_club_role_capability_grant( role_code: str = Query(...), diff --git a/backend/version.py b/backend/version.py index d54ef57..b6cbba4 100644 --- a/backend/version.py +++ b/backend/version.py @@ -1,6 +1,6 @@ # Shinkan Jinkendo Version Information -APP_VERSION = "0.8.199" +APP_VERSION = "0.8.200" BUILD_DATE = "2026-06-07" DB_SCHEMA_VERSION = "20260606083" @@ -16,9 +16,10 @@ MODULE_VERSIONS = { "club_join_requests": "1.0.1", # Depends(get_tenant_context) "club_creation_requests": "1.0.1", # superseded wenn freigegebener Verein gelöscht "admin_users": "1.0.0", # GET /api/admin/users - "club_features": "1.6.0", # Standard consume_club_feature_with_usage + merge_feature_usage_into_response + "club_features": "1.6.0", + "admin_rights": "1.1.0", # Matrix UX, Enforcement-Audit, clear club grants by capability + "capability_enforcement_audit": "1.0.0", "club_quota_bypass": "1.0.0", # platform.club_quota.bypass* + Admin-Grants-API - "admin_rights": "1.0.0", # M6: Rollen/Rechte — Capabilities, Bypass, Vereins-Kontingente "entitlements": "1.2.0", # capability_quota_bypass in Feature-Map für /me/entitlements "platform_media_storage": "1.0.0", # GET/PUT /api/admin/platform-media-storage (Superadmin-Pfad unter MEDIA_ROOT) "media_rights": "1.3.1", # acting_profile_id in write_audit_log_entry auf Optional[int] (P-13 anonyme Meldungen) diff --git a/docs/working/RBAC_ENFORCEMENT_ROADMAP.md b/docs/working/RBAC_ENFORCEMENT_ROADMAP.md new file mode 100644 index 0000000..56ad18a --- /dev/null +++ b/docs/working/RBAC_ENFORCEMENT_ROADMAP.md @@ -0,0 +1,156 @@ +# RBAC, Kontingente & Enforcement — Roadmap + +**Stand:** 2026-06-07 · App **0.8.199** · Schema **20260606083** +**Bezüge:** `MEMBERSHIP_RBAC_DECISIONS_2026-06.md`, `CAPABILITY_CATALOG.v1.md`, `CLUB_MEMBERSHIP_AND_FEATURES.v1.md` + +Diese Roadmap bündelt **was fertig ist**, **was als Standard gilt** und **was noch fehlt** — ohne Insellösungen pro Feature. + +--- + +## 1. Architektur-Standard (verbindlich) + +### Request-Kette (Ziel) + +``` +Auth → Account-State → TenantContext + → probe_capability (Recht) + → probe_club_feature_access (Kontingent) + → Governance (Objekt) + → Business-Logik + → consume_club_feature_with_usage + merge_feature_usage_into_response +``` + +### Frontend-Standard + +- `GET /api/me/entitlements` = einzige Quelle für Rechte + Kontingente in der UI +- `request()` synchronisiert `feature_usage` aus API-Responses automatisch (`featureUsageSync.js`) +- Keine parallelen `if (club_admin)` für **Sicherheit** (UX-Fallback nur übergangsweise) + +### Admin + +- **Rollen & Rechte** (`/admin/rights`): Matrix mit Klartext zuerst, technische ID darunter +- Umsetzungsstand pro Recht: `capability_enforcement_audit.py` → Feld `enforcement` in der Matrix + +--- + +## 2. Ist-Stand nach Meilenstein + +| Meilenstein | Inhalt | Status | +|-------------|--------|--------| +| **M1** | Feature-Schema, Pläne, Seeds | ✅ | +| **M2** | Feature-Probe + JSON-Log | ✅ | +| **M3** | Capabilities, Account-Lifecycle, Tenant | ✅ (Legacy parallel) | +| **M4** | `/me/entitlements`, Badge (KI) | ✅ teilweise | +| **M5** | Hard-Block + vollständiger Consume | ⚠️ nur `ai_calls` consume; Enforce **aus** | +| **M6** | Admin UI Rollen & Rechte | ⚠️ Matrix + Kontingente; kein Plan-/Rollen-CRUD | +| **M7** | Vereinsgründung beantragen | ✅ Basis + Capabilities | +| **M8** | Stripe | ❌ | +| **Sync** | `feature_usage` + `request()` | ✅ | + +--- + +## 3. Roadmap (empfohlene Reihenfolge) + +### Phase 1 — Durchsetzung sichtbar machen (kurz) + +| # | Paket | Lieferumfang | Aufwand | +|---|--------|--------------|---------| +| 1.1 | **Admin-Matrix UX** | Alle Rechte, Haken-Matrix, Umsetzungs-Badge | ✅ 2026-06-07 | +| 1.2 | **Doku-Sync** | Diese Roadmap, `MEMBERSHIP_RBAC` §2 | ✅ | +| 1.3 | **Audit pflegen** | Bei jedem `probe_capability` → `capability_enforcement_audit.py` | laufend | + +### Phase 2 — Kontingente vollständig (M5) + +| # | Paket | Lieferumfang | +|---|--------|--------------| +| 2.1 | **Consume erweitern** | `exercises`, `exercise_media` nach Standard-Helfer | +| 2.2 | **Badges** | `FeatureUsageBadge` an Create/Upload, nicht nur KI | +| 2.3 | **Dev: Enforce** | `CLUB_FEATURE_ENFORCE=1` auf Dev, Free `ai_calls=0` testen | +| 2.4 | **Prod-Rollout** | Enforce schrittweise; Kommunikation an Vereine | + +### Phase 3 — Capabilities an alle Endpoints (C3–C4) + +| # | Paket | Lieferumfang | +|---|--------|--------------| +| 3.1 | **Endpoint-Audit** | `ACCESS_LAYER_ENDPOINT_AUDIT.md` — jeder Schreib-Pfad | +| 3.2 | **probe_capability** | `exercises.update/delete`, `planning.*`, `org.*`, Medien-Bibliothek, … | +| 3.3 | **CAPABILITY_ENFORCE=1** | Nach Audit auf Dev, dann Prod | +| 3.4 | **Legacy abbauen** | `can_plan_in_club` nur noch als Fallback, dokumentiert | + +### Phase 4 — Frontend auf Entitlements (Phase E) + +| # | Paket | Lieferumfang | +|---|--------|--------------| +| 4.1 | **Navigation** | Menüpunkte aus `hasCapability()` | +| 4.2 | **Buttons** | KI, Anlegen, Löschen, Planung — aus Entitlements | +| 4.3 | **Rollen-Labels** | Anzeige `club_roles` statt technischer IDs | + +### Phase 5 — Admin & Produkt (M6 voll) + +| # | Paket | Lieferumfang | +|---|--------|--------------| +| 5.1 | **Pläne-CRUD** | Neue Vereinspläne anlegen, nicht nur Seed | +| 5.2 | **Systemrolle Co-Trainer** | Seed + Matrix | +| 5.3 | **Trainer-Budgets** | v2 — `club_member_feature_budgets` | + +### Phase 6 — Abrechnung (M8) + +Stripe / Rechnung — bewusst nach funktionierendem Enforce. + +--- + +## 4. Superadmin & Vereinsrechte (Entscheidung) + +**Kurz: Superadmin braucht keine Vereinsrolle `club_admin` für die meiste Arbeit.** + +| Ebene | Verhalten | +|-------|-----------| +| **Capabilities (neu)** | `admin` und `superadmin` = `platform_admin_bypass` für **alle Vereins-Capabilities** (`capabilities.py`) — unabhängig von `club_member_roles` | +| **Legacy-Helfer** | `can_plan_in_club`, `can_manage_club_org` → `True` für Plattform-Admin ohne Mitgliedschaft | +| **Mandant** | Aktiver Verein über `X-Active-Club-Id` / `active_club_id` — **keine** Mitgliedschaft nötig für Plattform-Admin | +| **Kontingente** | Superadmin: Quota-Bypass (Capability-Grant); zählt nicht gegen Vereins-Kontingent | +| **Ausnahmen Legacy** | Einzelne Pfade prüfen noch **nur** `has_club_role(…, 'club_admin')` ohne Plattform-Bypass — z. B. Löschen von `visibility=club`-Übungen. → Phase 3 bereinigen | + +**Empfehlung:** Superadmin **nicht** zwingend als `club_admin` in jeden Verein eintragen. Optional Mitgliedschaft nur für realistische Audit-Tests oder Vereinsorga-Simulation. Produktiv: Mandant per Club-Switcher wählen. + +`admin` (Portal-Admin): gleicher Capability-Bypass für Vereins-Funktionen; Portal-Capabilities nur mit explizitem Grant in der Matrix. + +--- + +## 5. Vereinsrollen-Matrix — Semantik (Admin-UI) + +| Zustand | Bedeutung | UI | +|---------|-----------|-----| +| **Keine Grants** in DB | Alle aktiven Mitglieder (wenn `min_account_state` reicht) | Zellen zeigen „alle“ | +| **Mindestens ein Grant** | Nur angehakte Rollen | Checkboxen | +| **„Alle Mitglieder“** | Löscht alle Grants der Zeile | Zurück zum offenen Zustand | + +Das ersetzt das frühere Formular „Vereinsrollen-Grant hinzufügen“, das nur bereits eingeschränkte Rechte sichtbar machte. + +--- + +## 6. Offene Lücken (Checkliste) + +- [ ] `CAPABILITY_ENFORCE=1` in Produktion +- [ ] `CLUB_FEATURE_ENFORCE=1` in Produktion +- [ ] Consume für alle Features mit Verbrauch (nicht nur `ai_calls`) +- [ ] `probe_capability` auf >90 % der Schreib-Endpoints +- [ ] Frontend ohne Legacy-Rollen-Guards +- [ ] Multipart-Uploads an `featureUsageSync` anbinden +- [ ] Legacy-Löschpfade mit Plattform-Bypass harmonisieren +- [ ] `HANDOVER.md` / `PROJECT_STATUS` Versionsstand aktualisieren + +--- + +## 7. Referenzen + +| Datei | Zweck | +|-------|--------| +| `backend/capability_enforcement_audit.py` | Matrix-Badges „angebunden / Legacy“ | +| `backend/club_features.py` | Consume-Standard | +| `frontend/src/utils/featureUsageSync.js` | Entitlements-Sync | +| `frontend/src/pages/AdminRightsPage.jsx` | Konfiguration | + +**Changelog** + +- 2026-06-07: Initial nach Session Rollen/Kontingente — Standard, Roadmap Phasen 1–6, Superadmin-Klärung, Matrix-Semantik. diff --git a/frontend/src/pages/AdminRightsPage.jsx b/frontend/src/pages/AdminRightsPage.jsx index 375343c..e3d6eef 100644 --- a/frontend/src/pages/AdminRightsPage.jsx +++ b/frontend/src/pages/AdminRightsPage.jsx @@ -45,6 +45,48 @@ function formatLimitHint(feature) { return '' } +function EnforcementBadge({ enforcement, featureConsume }) { + if (!enforcement) return null + const tone = + enforcement.implemented + ? 'var(--accent-dark)' + : enforcement.level === 'legacy' + ? 'var(--danger)' + : 'var(--text3)' + return ( +
+ + {enforcement.implemented ? '● ' : '○ '} + {enforcement.label} + + {featureConsume ? ( +
+ Kontingent: {featureConsume.label} +
+ ) : null} +
+ ) +} + +function CapabilityNameCell({ cap }) { + return ( + +
{cap.name || cap.id}
+ {cap.id} + {cap.linked_feature_id ? ( +
+ Kontingent-ID: {cap.linked_feature_id} +
+ ) : null} + + + ) +} + +function clubGrantsForCapability(capMatrix, capabilityId) { + return (capMatrix?.club_role_grants || []).filter((g) => g.capability_id === capabilityId) +} + /** * Superadmin: Rollen → Fähigkeiten (Capabilities) und Vereins-Kontingente konfigurieren. */ @@ -63,7 +105,6 @@ export default function AdminRightsPage() { const [capMatrix, setCapMatrix] = useState(null) const [bypassData, setBypassData] = useState(null) - const [newClubGrant, setNewClubGrant] = useState({ role_code: 'trainer', capability_id: '' }) const [newBypassPortal, setNewBypassPortal] = useState({ portal_role: 'helpdesk', feature_id: '' }) const [newBypassProfile, setNewBypassProfile] = useState({ profile_id: '', @@ -222,6 +263,19 @@ export default function AdminRightsPage() { } } + const openClubCapabilityForAllMembers = async (capabilityId) => { + setBusy(true) + setError('') + try { + await api.clearAdminRightsClubCapabilityGrants(capabilityId) + await loadCapMatrix() + } catch (e) { + setError(e.message || String(e)) + } finally { + setBusy(false) + } + } + const submitBypassPortal = async (e) => { e.preventDefault() setBusy(true) @@ -268,13 +322,15 @@ export default function AdminRightsPage() {

Rollen & Rechte

-

- Rollen → Fähigkeiten (Capabilities): Wer darf welche Funktion nutzen? +

+ Rechte: Wer darf welche Funktion nutzen? Haken = Grant für diese Rolle.
- Kontingente: Wie viel darf ein Verein verbrauchen (an Fähigkeiten gekoppelt - über linked_feature_id)? + Kontingente: Wie viel darf ein Verein verbrauchen (an Rechten gekoppelt).
- Vereinspläne bündeln nur Kontingent-Werte — sie ersetzen keine Berechtigungen. + + ● = an API angebunden · ○ = nur Legacy oder noch nicht durchgesetzt. Roadmap:{' '} + docs/working/RBAC_ENFORCEMENT_ROADMAP.md +

- Plattform-Funktionen (domain=platform). Jede Funktion im Produkt soll sich - hier anmelden und bei Anzeige und Ausführung prüfen. + Plattform-Funktionen — Anzeige primär nach Klartext. Technische ID und + Umsetzungsstand darunter.

- + {(capMatrix.portal_roles || []).map((r) => ( {portalCapabilities.map((cap) => ( - + {(capMatrix.portal_roles || []).map((role) => { const on = portalGrantSet.has(`${role}::${cap.id}`) return ( @@ -361,118 +409,75 @@ export default function AdminRightsPage() {

- Vereinsrollen → Fähigkeiten. Ohne Grant-Eintrag gilt die Fähigkeit für alle aktiven - Vereinsmitglieder; gesetzte Grants schränken auf die angehakten Rollen ein. + Vereinsrollen: Alle Rechte in der Matrix. Haken = diese Rolle hat das Recht. + Zeile mit alle = noch nicht rollenbeschränkt (gilt für jedes aktive Mitglied). + Erster Klick auf alle schränkt auf die gewählte Rolle ein; „Alle Mitglieder“ + hebt die Einschränkung wieder auf.

FähigkeitRecht {PORTAL_ROLE_LABEL[r] || r} @@ -327,15 +383,7 @@ export default function AdminRightsPage() {
- {cap.id} -
{cap.name}
- {cap.linked_feature_id ? ( -
- Kontingent: {cap.linked_feature_id} -
- ) : null} -
- + {(capMatrix.club_roles || []).map((r) => ( ))} + - {clubScopedCapabilities - .filter((cap) => - (capMatrix.club_role_grants || []).some((g) => g.capability_id === cap.id), - ) - .map((cap) => ( + {clubScopedCapabilities.map((cap) => { + const restricted = clubGrantsForCapability(capMatrix, cap.id).length > 0 + return ( - + {(capMatrix.club_roles || []).map((role) => { const on = clubGrantSet.has(`${role}::${cap.id}`) return ( ) })} + - ))} + ) + })}
FähigkeitRecht {CLUB_ROLE_LABEL[r] || r} Freigabe
- {cap.id} -
{cap.name}
- {cap.linked_feature_id ? ( -
- Kontingent: {cap.linked_feature_id} -
- ) : null} -
- toggleClubGrant(role, cap.id, on)} - /> + {restricted ? ( + toggleClubGrant(role, cap.id, on)} + /> + ) : ( + + )} + {restricted ? ( + + ) : null} +
- -
{ - e.preventDefault() - if (!newClubGrant.capability_id) return - setBusy(true) - setError('') - try { - await api.addAdminRightsClubRoleGrant( - newClubGrant.role_code, - newClubGrant.capability_id, - ) - setNewClubGrant((p) => ({ ...p, capability_id: '' })) - await loadCapMatrix() - } catch (err) { - setError(err.message || String(err)) - } finally { - setBusy(false) - } - }} - > -

Vereinsrollen-Grant hinzufügen

-
- - -
- -
-
-
) : null} diff --git a/frontend/src/utils/api.js b/frontend/src/utils/api.js index 77d98af..28fa2eb 100644 --- a/frontend/src/utils/api.js +++ b/frontend/src/utils/api.js @@ -290,6 +290,13 @@ export async function deleteAdminRightsClubRoleGrant(roleCode, capabilityId) { return request(`/api/admin/rights/capability-grants/club-roles?${q}`, { method: 'DELETE' }) } +export async function clearAdminRightsClubCapabilityGrants(capabilityId) { + const q = new URLSearchParams({ capability_id: capabilityId }) + return request(`/api/admin/rights/capability-grants/club-roles/by-capability?${q}`, { + method: 'DELETE', + }) +} + export async function listAdminRightsQuotaBypass() { return request('/api/admin/rights/quota-bypass') } @@ -1010,6 +1017,7 @@ export const api = { deleteAdminRightsPortalGrant, addAdminRightsClubRoleGrant, deleteAdminRightsClubRoleGrant, + clearAdminRightsClubCapabilityGrants, listAdminRightsQuotaBypass, addAdminRightsQuotaBypassPortal, deleteAdminRightsQuotaBypassPortal,