Update Membership RBAC Decisions and Enhance Admin Rights Management
All checks were successful
Deploy Development / deploy (push) Successful in 44s
Test Suite / pytest-backend (push) Successful in 43s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m13s
All checks were successful
Deploy Development / deploy (push) Successful in 44s
Test Suite / pytest-backend (push) Successful in 43s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m13s
- 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.
This commit is contained in:
parent
b68185842e
commit
9d52aeab67
|
|
@ -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.
|
||||
|
|
|
|||
94
backend/capability_enforcement_audit.py
Normal file
94
backend/capability_enforcement_audit.py
Normal file
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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(...),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
156
docs/working/RBAC_ENFORCEMENT_ROADMAP.md
Normal file
156
docs/working/RBAC_ENFORCEMENT_ROADMAP.md
Normal file
|
|
@ -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.
|
||||
|
|
@ -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 (
|
||||
<div style={{ marginTop: '4px', fontSize: '0.68rem', lineHeight: 1.35 }}>
|
||||
<span style={{ color: tone }} title={enforcement.detail}>
|
||||
{enforcement.implemented ? '● ' : '○ '}
|
||||
{enforcement.label}
|
||||
</span>
|
||||
{featureConsume ? (
|
||||
<div style={{ color: featureConsume.implemented ? 'var(--accent-dark)' : 'var(--text3)' }}>
|
||||
Kontingent: {featureConsume.label}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function CapabilityNameCell({ cap }) {
|
||||
return (
|
||||
<td style={{ padding: '6px', verticalAlign: 'top' }}>
|
||||
<div style={{ fontWeight: 500, color: 'var(--text1)' }}>{cap.name || cap.id}</div>
|
||||
<code style={{ fontSize: '0.68rem', color: 'var(--text3)' }}>{cap.id}</code>
|
||||
{cap.linked_feature_id ? (
|
||||
<div style={{ color: 'var(--text3)', fontSize: '0.68rem' }}>
|
||||
Kontingent-ID: {cap.linked_feature_id}
|
||||
</div>
|
||||
) : null}
|
||||
<EnforcementBadge enforcement={cap.enforcement} featureConsume={cap.feature_consume} />
|
||||
</td>
|
||||
)
|
||||
}
|
||||
|
||||
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() {
|
|||
<div className="page-padding app-page">
|
||||
<AdminPageNav />
|
||||
<h1 style={{ marginTop: '1rem', fontSize: '1.35rem' }}>Rollen & Rechte</h1>
|
||||
<p style={{ color: 'var(--text2)', maxWidth: '48rem', lineHeight: 1.55 }}>
|
||||
<strong>Rollen → Fähigkeiten (Capabilities):</strong> Wer darf welche Funktion nutzen?
|
||||
<p style={{ color: 'var(--text2)', maxWidth: '52rem', lineHeight: 1.55 }}>
|
||||
<strong>Rechte:</strong> Wer darf welche Funktion nutzen? Haken = Grant für diese Rolle.
|
||||
<br />
|
||||
<strong>Kontingente:</strong> Wie viel darf ein Verein verbrauchen (an Fähigkeiten gekoppelt
|
||||
über <code>linked_feature_id</code>)?
|
||||
<strong>Kontingente:</strong> Wie viel darf ein Verein verbrauchen (an Rechten gekoppelt).
|
||||
<br />
|
||||
Vereinspläne bündeln nur Kontingent-Werte — sie ersetzen keine Berechtigungen.
|
||||
<span style={{ fontSize: '0.85rem' }}>
|
||||
● = an API angebunden · ○ = nur Legacy oder noch nicht durchgesetzt. Roadmap:{' '}
|
||||
<code>docs/working/RBAC_ENFORCEMENT_ROADMAP.md</code>
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<div
|
||||
|
|
@ -310,13 +366,13 @@ export default function AdminRightsPage() {
|
|||
{!loading && tab === 'portal' && capMatrix ? (
|
||||
<div className="card" style={{ marginTop: '1rem', overflowX: 'auto' }}>
|
||||
<p style={{ fontSize: '0.875rem', color: 'var(--text2)', margin: '0 0 12px' }}>
|
||||
Plattform-Funktionen (<code>domain=platform</code>). 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.
|
||||
</p>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.8rem' }}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ textAlign: 'left', padding: '6px', minWidth: '200px' }}>Fähigkeit</th>
|
||||
<th style={{ textAlign: 'left', padding: '6px', minWidth: '220px' }}>Recht</th>
|
||||
{(capMatrix.portal_roles || []).map((r) => (
|
||||
<th key={r} style={{ textAlign: 'center', padding: '6px' }}>
|
||||
{PORTAL_ROLE_LABEL[r] || r}
|
||||
|
|
@ -327,15 +383,7 @@ export default function AdminRightsPage() {
|
|||
<tbody>
|
||||
{portalCapabilities.map((cap) => (
|
||||
<tr key={cap.id} style={{ borderTop: '1px solid var(--border)' }}>
|
||||
<td style={{ padding: '6px' }}>
|
||||
<code style={{ fontSize: '0.75rem' }}>{cap.id}</code>
|
||||
<div style={{ color: 'var(--text3)' }}>{cap.name}</div>
|
||||
{cap.linked_feature_id ? (
|
||||
<div style={{ color: 'var(--text3)', fontSize: '0.7rem' }}>
|
||||
Kontingent: {cap.linked_feature_id}
|
||||
</div>
|
||||
) : null}
|
||||
</td>
|
||||
<CapabilityNameCell cap={cap} />
|
||||
{(capMatrix.portal_roles || []).map((role) => {
|
||||
const on = portalGrantSet.has(`${role}::${cap.id}`)
|
||||
return (
|
||||
|
|
@ -361,118 +409,75 @@ export default function AdminRightsPage() {
|
|||
<div style={{ marginTop: '1rem', display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
||||
<div className="card" style={{ overflowX: 'auto' }}>
|
||||
<p style={{ fontSize: '0.875rem', color: 'var(--text2)', margin: '0 0 12px' }}>
|
||||
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 <em>alle</em> = noch nicht rollenbeschränkt (gilt für jedes aktive Mitglied).
|
||||
Erster Klick auf <em>alle</em> schränkt auf die gewählte Rolle ein; „Alle Mitglieder“
|
||||
hebt die Einschränkung wieder auf.
|
||||
</p>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.75rem' }}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ textAlign: 'left', padding: '6px', minWidth: '180px' }}>Fähigkeit</th>
|
||||
<th style={{ textAlign: 'left', padding: '6px', minWidth: '220px' }}>Recht</th>
|
||||
{(capMatrix.club_roles || []).map((r) => (
|
||||
<th key={r} style={{ textAlign: 'center', padding: '6px' }}>
|
||||
{CLUB_ROLE_LABEL[r] || r}
|
||||
</th>
|
||||
))}
|
||||
<th style={{ textAlign: 'left', padding: '6px', minWidth: '100px' }}>Freigabe</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{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 (
|
||||
<tr key={cap.id} style={{ borderTop: '1px solid var(--border)' }}>
|
||||
<td style={{ padding: '6px' }}>
|
||||
<code>{cap.id}</code>
|
||||
<div style={{ color: 'var(--text3)' }}>{cap.name}</div>
|
||||
{cap.linked_feature_id ? (
|
||||
<div style={{ color: 'var(--text3)', fontSize: '0.7rem' }}>
|
||||
Kontingent: {cap.linked_feature_id}
|
||||
</div>
|
||||
) : null}
|
||||
</td>
|
||||
<CapabilityNameCell cap={cap} />
|
||||
{(capMatrix.club_roles || []).map((role) => {
|
||||
const on = clubGrantSet.has(`${role}::${cap.id}`)
|
||||
return (
|
||||
<td key={role} style={{ textAlign: 'center', padding: '6px' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={on}
|
||||
disabled={busy}
|
||||
onChange={() => toggleClubGrant(role, cap.id, on)}
|
||||
/>
|
||||
{restricted ? (
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={on}
|
||||
disabled={busy}
|
||||
aria-label={`${cap.name} für ${CLUB_ROLE_LABEL[role] || role}`}
|
||||
onChange={() => toggleClubGrant(role, cap.id, on)}
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
disabled={busy}
|
||||
title={`Standard: alle Mitglieder. Klick = nur ${CLUB_ROLE_LABEL[role] || role}`}
|
||||
style={{ fontSize: '0.7rem', padding: '2px 6px' }}
|
||||
onClick={() => toggleClubGrant(role, cap.id, false)}
|
||||
>
|
||||
alle
|
||||
</button>
|
||||
)}
|
||||
</td>
|
||||
)
|
||||
})}
|
||||
<td style={{ padding: '6px', whiteSpace: 'nowrap' }}>
|
||||
{restricted ? (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
disabled={busy}
|
||||
style={{ fontSize: '0.68rem', padding: '2px 6px' }}
|
||||
onClick={() => openClubCapabilityForAllMembers(cap.id)}
|
||||
>
|
||||
Alle Mitglieder
|
||||
</button>
|
||||
) : null}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<form
|
||||
className="card"
|
||||
onSubmit={async (e) => {
|
||||
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)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<h2 style={{ fontSize: '1rem', margin: '0 0 0.75rem' }}>Vereinsrollen-Grant hinzufügen</h2>
|
||||
<div className="form-row" style={{ display: 'flex', flexWrap: 'wrap', gap: '0.75rem' }}>
|
||||
<label style={{ flex: '1 1 140px' }}>
|
||||
<span className="form-label">Rolle</span>
|
||||
<select
|
||||
className="form-input"
|
||||
value={newClubGrant.role_code}
|
||||
onChange={(e) =>
|
||||
setNewClubGrant((p) => ({ ...p, role_code: e.target.value }))
|
||||
}
|
||||
>
|
||||
{(capMatrix.club_roles || []).map((r) => (
|
||||
<option key={r} value={r}>
|
||||
{CLUB_ROLE_LABEL[r] || r}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label style={{ flex: '2 1 240px' }}>
|
||||
<span className="form-label">Fähigkeit</span>
|
||||
<select
|
||||
className="form-input"
|
||||
value={newClubGrant.capability_id}
|
||||
onChange={(e) =>
|
||||
setNewClubGrant((p) => ({ ...p, capability_id: e.target.value }))
|
||||
}
|
||||
>
|
||||
<option value="">— wählen —</option>
|
||||
{clubScopedCapabilities.map((c) => (
|
||||
<option key={c.id} value={c.id}>
|
||||
{c.id} — {c.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<div style={{ alignSelf: 'flex-end' }}>
|
||||
<button type="submit" className="btn btn-primary" disabled={busy}>
|
||||
Hinzufügen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user