Implement Registry-First Approach for Rights and Capabilities Management
All checks were successful
Deploy Development / deploy (push) Successful in 44s
Test Suite / pytest-backend (push) Successful in 42s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 35s
Test Suite / playwright-tests (push) Successful in 1m51s
All checks were successful
Deploy Development / deploy (push) Successful in 44s
Test Suite / pytest-backend (push) Successful in 42s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 35s
Test Suite / playwright-tests (push) Successful in 1m51s
- Updated the capability catalog to reflect a registry-first approach, requiring modules to register rights and quotas upon implementation. - Enhanced the backend to synchronize the rights registry with the database, ensuring only registered capabilities and features are displayed in the admin matrix. - Modified SQL queries in the admin rights router to filter capabilities and features based on module registration. - Updated documentation to clarify the new rights and features registry process, replacing the previous catalog-first method. - Incremented application version to 0.8.201 and updated database schema version to 20260606084 to reflect these changes.
This commit is contained in:
parent
9d52aeab67
commit
4130a63dfe
|
|
@ -304,10 +304,13 @@ assert_club_feature(tenant, "ai_calls", club_id=tenant.effective_club_id) # sie
|
|||
|
||||
## 9. Abgrenzung & Drift-Schutz
|
||||
|
||||
1. **Neue Nutzerfunktion** → zuerst Capability-ID hier eintragen, dann Endpoint.
|
||||
2. **Kein** paralleles `if (user.role === 'trainer')` für Sicherheit — nur UX-Fallback.
|
||||
3. Capability ≠ Feature: `exercises.ai.suggest` (darf ich?) vs. `ai_calls` (wie viel übrig?).
|
||||
4. Plattform-Admin-Bypass dokumentieren und auditieren (`platform_admin` sieht Mandant, nicht automatisch alle Quotas).
|
||||
1. **Neue Nutzerfunktion** → `register_capability()` in `rights_registrations/<modul>.py`, dann Endpoint mit `probe_capability`. Namenskonvention hier dokumentieren — **kein** Bulk-Seed in Migrationen.
|
||||
2. **Kontingent** → `register_feature()` im selben Modul; Consume über `consume_club_feature_with_usage`.
|
||||
3. **Kein** paralleles `if (user.role === 'trainer')` für Sicherheit — nur UX-Fallback.
|
||||
4. Capability ≠ Feature: `exercises.ai.suggest` (darf ich?) vs. `ai_calls` (wie viel übrig?).
|
||||
5. Plattform-Admin-Bypass dokumentieren und auditieren (`platform_admin` sieht Mandant, nicht automatisch alle Quotas).
|
||||
|
||||
Siehe **`docs/working/RIGHTS_AND_FEATURES_REGISTRY.md`** (Registry-first, ersetzt Katalog-first aus 079).
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -52,6 +52,20 @@ else:
|
|||
print(f"[FAIL] Migration-Laufzeitfehler: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
# Registry-first: Module → DB (nur registrierte Rechte/Kontingente in Admin-Matrix)
|
||||
if os.getenv("SKIP_DB_MIGRATE", "").strip().lower() not in ("1", "true", "yes"):
|
||||
try:
|
||||
from rights_registry import sync_rights_registry_to_db
|
||||
|
||||
counts = sync_rights_registry_to_db()
|
||||
print(
|
||||
f"[OK] Rights registry sync: {counts['capabilities']} capabilities, "
|
||||
f"{counts['features']} features"
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"[FAIL] Rights registry sync: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
from routers.auth import limiter as auth_rate_limiter
|
||||
|
||||
# OpenAPI: in Produktion standardmäßig aus (Schema nicht öffentlich). Notfall: PUBLIC_OPENAPI=1
|
||||
|
|
|
|||
15
backend/migrations/084_rights_registry_module.sql
Normal file
15
backend/migrations/084_rights_registry_module.sql
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
-- Migration 084: Modul-Registrierung für Rechte & Kontingente (Registry-first)
|
||||
-- capabilities/features mit module=NULL = Legacy-Katalog-Seed (nicht in Admin-Matrix).
|
||||
-- module IS NOT NULL = vom Modul bei Implementierung registriert.
|
||||
|
||||
ALTER TABLE capabilities
|
||||
ADD COLUMN IF NOT EXISTS module TEXT;
|
||||
|
||||
ALTER TABLE features
|
||||
ADD COLUMN IF NOT EXISTS module TEXT;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_capabilities_module
|
||||
ON capabilities(module) WHERE module IS NOT NULL AND active = true;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_features_module
|
||||
ON features(module) WHERE module IS NOT NULL AND active = true;
|
||||
9
backend/rights_registrations/__init__.py
Normal file
9
backend/rights_registrations/__init__.py
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
"""
|
||||
Modul-Registrierungen für Rechte & Kontingente.
|
||||
|
||||
Neues Feature: eigene Datei oder Eintrag hier importieren — kein Eintrag in 079-Katalog-Migration.
|
||||
"""
|
||||
from rights_registrations import club_creation # noqa: F401
|
||||
from rights_registrations import exercises # noqa: F401
|
||||
from rights_registrations import planning # noqa: F401
|
||||
from rights_registrations import platform # noqa: F401
|
||||
38
backend/rights_registrations/club_creation.py
Normal file
38
backend/rights_registrations/club_creation.py
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
from rights_registry import CapabilityRegistration, register_capability
|
||||
|
||||
register_capability(
|
||||
CapabilityRegistration(
|
||||
id="club.creation_request.create",
|
||||
name="Vereinsgründung beantragen",
|
||||
domain="club",
|
||||
module="club_creation_requests",
|
||||
min_account_state="verified_pending_club",
|
||||
)
|
||||
)
|
||||
register_capability(
|
||||
CapabilityRegistration(
|
||||
id="club.creation_request.read_own",
|
||||
name="Eigene Gründungsanträge",
|
||||
domain="club",
|
||||
module="club_creation_requests",
|
||||
min_account_state="verified_pending_club",
|
||||
)
|
||||
)
|
||||
register_capability(
|
||||
CapabilityRegistration(
|
||||
id="club.creation_request.withdraw",
|
||||
name="Gründungsantrag zurückziehen",
|
||||
domain="club",
|
||||
module="club_creation_requests",
|
||||
min_account_state="verified_pending_club",
|
||||
)
|
||||
)
|
||||
register_capability(
|
||||
CapabilityRegistration(
|
||||
id="platform.club_creation.approve",
|
||||
name="Vereinsgründung freigeben",
|
||||
domain="platform",
|
||||
module="club_creation_requests",
|
||||
min_account_state="platform_admin",
|
||||
)
|
||||
)
|
||||
90
backend/rights_registrations/exercises.py
Normal file
90
backend/rights_registrations/exercises.py
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
"""Übungen-Modul: nur Rechte/Kontingente mit echter Endpoint-Verdrahtung."""
|
||||
from rights_registry import CapabilityRegistration, FeatureRegistration, register_capability, register_feature
|
||||
|
||||
_CLUB_WRITE_ROLES = (
|
||||
"club_admin",
|
||||
"trainer",
|
||||
"content_editor",
|
||||
"division_lead",
|
||||
)
|
||||
|
||||
register_feature(
|
||||
FeatureRegistration(
|
||||
id="ai_calls",
|
||||
name="KI-Aufrufe",
|
||||
module="exercises",
|
||||
category="ai",
|
||||
limit_type="count",
|
||||
reset_period="monthly",
|
||||
default_limit=0,
|
||||
description="KI-Aufrufe pro Monat (Suggest, Regenerate)",
|
||||
)
|
||||
)
|
||||
|
||||
register_capability(
|
||||
CapabilityRegistration(
|
||||
id="exercises.ai.suggest",
|
||||
name="KI-Vorschlag Übung",
|
||||
domain="exercises",
|
||||
module="exercises",
|
||||
linked_feature_id="ai_calls",
|
||||
default_club_grants=tuple((r, "exercises.ai.suggest") for r in _CLUB_WRITE_ROLES),
|
||||
)
|
||||
)
|
||||
register_capability(
|
||||
CapabilityRegistration(
|
||||
id="exercises.ai.regenerate",
|
||||
name="KI neu generieren",
|
||||
domain="exercises",
|
||||
module="exercises",
|
||||
linked_feature_id="ai_calls",
|
||||
default_club_grants=tuple((r, "exercises.ai.regenerate") for r in _CLUB_WRITE_ROLES),
|
||||
)
|
||||
)
|
||||
register_capability(
|
||||
CapabilityRegistration(
|
||||
id="exercises.create",
|
||||
name="Übung anlegen",
|
||||
domain="exercises",
|
||||
module="exercises",
|
||||
linked_feature_id="exercises",
|
||||
default_club_grants=tuple((r, "exercises.create") for r in _CLUB_WRITE_ROLES),
|
||||
)
|
||||
)
|
||||
register_capability(
|
||||
CapabilityRegistration(
|
||||
id="exercises.media.upload",
|
||||
name="Übungsmedien hochladen",
|
||||
domain="exercises",
|
||||
module="exercises",
|
||||
linked_feature_id="exercise_media",
|
||||
default_club_grants=(
|
||||
("club_admin", "exercises.media.upload"),
|
||||
("trainer", "exercises.media.upload"),
|
||||
("content_editor", "exercises.media.upload"),
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
register_feature(
|
||||
FeatureRegistration(
|
||||
id="exercises",
|
||||
name="Übungen (Bestand)",
|
||||
module="exercises",
|
||||
category="content",
|
||||
limit_type="count",
|
||||
reset_period="never",
|
||||
default_limit=0,
|
||||
)
|
||||
)
|
||||
register_feature(
|
||||
FeatureRegistration(
|
||||
id="exercise_media",
|
||||
name="Übungsmedien",
|
||||
module="exercises",
|
||||
category="media",
|
||||
limit_type="count",
|
||||
reset_period="never",
|
||||
default_limit=0,
|
||||
)
|
||||
)
|
||||
24
backend/rights_registrations/planning.py
Normal file
24
backend/rights_registrations/planning.py
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
from rights_registry import CapabilityRegistration, FeatureRegistration, register_capability, register_feature
|
||||
|
||||
_PLANNING_ROLES = ("club_admin", "trainer", "division_lead")
|
||||
|
||||
register_capability(
|
||||
CapabilityRegistration(
|
||||
id="planning.ai.suggest",
|
||||
name="Planungs-KI Suggest",
|
||||
domain="planning",
|
||||
module="planning_exercise_suggest",
|
||||
linked_feature_id="ai_calls",
|
||||
default_club_grants=tuple((r, "planning.ai.suggest") for r in _PLANNING_ROLES),
|
||||
)
|
||||
)
|
||||
register_capability(
|
||||
CapabilityRegistration(
|
||||
id="planning.ai.progression_path",
|
||||
name="Planungs-KI Progressionspfad",
|
||||
domain="planning",
|
||||
module="planning_exercise_suggest",
|
||||
linked_feature_id="ai_calls",
|
||||
default_club_grants=tuple((r, "planning.ai.progression_path") for r in _PLANNING_ROLES),
|
||||
)
|
||||
)
|
||||
21
backend/rights_registrations/platform.py
Normal file
21
backend/rights_registrations/platform.py
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
"""Plattform-Modul: Admin-Zugang und Quota-Bypass (083)."""
|
||||
from rights_registry import CapabilityRegistration, register_capability
|
||||
|
||||
register_capability(
|
||||
CapabilityRegistration(
|
||||
id="platform.admin.access",
|
||||
name="Plattform-Admin-Bereich",
|
||||
domain="platform",
|
||||
module="platform",
|
||||
min_account_state="platform_admin",
|
||||
)
|
||||
)
|
||||
register_capability(
|
||||
CapabilityRegistration(
|
||||
id="platform.club_quota.bypass",
|
||||
name="Vereins-Kontingent-Bypass",
|
||||
domain="quota_bypass",
|
||||
module="platform",
|
||||
min_account_state="platform_admin",
|
||||
)
|
||||
)
|
||||
159
backend/rights_registry.py
Normal file
159
backend/rights_registry.py
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
"""
|
||||
Registry-first: Module melden Rechte (capabilities) und Kontingente (features) bei Implementierung an.
|
||||
|
||||
Kein vollständiger Vorab-Katalog — nur was ein Modul wirklich liefert, erscheint konfigurierbar
|
||||
in Admin „Rollen & Rechte“ (Filter: module IS NOT NULL).
|
||||
|
||||
Spez: docs/working/RIGHTS_AND_FEATURES_REGISTRY.md
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict, List, Optional, Sequence, Tuple
|
||||
|
||||
from db import get_db, get_cursor
|
||||
|
||||
GrantPair = Tuple[str, str] # (role_code, capability_id)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CapabilityRegistration:
|
||||
id: str
|
||||
name: str
|
||||
domain: str
|
||||
module: str
|
||||
min_account_state: str = "active_member"
|
||||
linked_feature_id: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
default_club_grants: Sequence[GrantPair] = field(default_factory=tuple)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FeatureRegistration:
|
||||
id: str
|
||||
name: str
|
||||
module: str
|
||||
category: str = "general"
|
||||
limit_type: str = "count"
|
||||
reset_period: str = "never"
|
||||
default_limit: Optional[int] = None
|
||||
description: Optional[str] = None
|
||||
enforcement_subject: str = "club"
|
||||
|
||||
|
||||
_CAPABILITY_REGISTRY: Dict[str, CapabilityRegistration] = {}
|
||||
_FEATURE_REGISTRY: Dict[str, FeatureRegistration] = {}
|
||||
|
||||
|
||||
def register_capability(defn: CapabilityRegistration) -> None:
|
||||
"""Modul deklariert ein Recht — wird beim Startup in die DB synchronisiert."""
|
||||
if not defn.module or not defn.id:
|
||||
raise ValueError("CapabilityRegistration: module und id sind Pflicht")
|
||||
_CAPABILITY_REGISTRY[defn.id] = defn
|
||||
|
||||
|
||||
def register_feature(defn: FeatureRegistration) -> None:
|
||||
"""Modul deklariert ein Vereins-Kontingent."""
|
||||
if not defn.module or not defn.id:
|
||||
raise ValueError("FeatureRegistration: module und id sind Pflicht")
|
||||
_FEATURE_REGISTRY[defn.id] = defn
|
||||
|
||||
|
||||
def registered_capabilities() -> Dict[str, CapabilityRegistration]:
|
||||
return dict(_CAPABILITY_REGISTRY)
|
||||
|
||||
|
||||
def registered_features() -> Dict[str, FeatureRegistration]:
|
||||
return dict(_FEATURE_REGISTRY)
|
||||
|
||||
|
||||
def _upsert_capability(cur, defn: CapabilityRegistration) -> None:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO capabilities (
|
||||
id, name, description, domain, min_account_state,
|
||||
linked_feature_id, active, module, updated_at
|
||||
)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, true, %s, NOW())
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
name = EXCLUDED.name,
|
||||
description = EXCLUDED.description,
|
||||
domain = EXCLUDED.domain,
|
||||
min_account_state = EXCLUDED.min_account_state,
|
||||
linked_feature_id = EXCLUDED.linked_feature_id,
|
||||
active = true,
|
||||
module = EXCLUDED.module,
|
||||
updated_at = NOW()
|
||||
""",
|
||||
(
|
||||
defn.id,
|
||||
defn.name,
|
||||
defn.description,
|
||||
defn.domain,
|
||||
defn.min_account_state,
|
||||
defn.linked_feature_id,
|
||||
defn.module,
|
||||
),
|
||||
)
|
||||
for role_code, cap_id in defn.default_club_grants:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO club_role_capability_grants (role_code, capability_id)
|
||||
VALUES (%s, %s)
|
||||
ON CONFLICT DO NOTHING
|
||||
""",
|
||||
(role_code, cap_id),
|
||||
)
|
||||
|
||||
|
||||
def _upsert_feature(cur, defn: FeatureRegistration) -> None:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO features (
|
||||
id, app, name, description, category, limit_type,
|
||||
reset_period, default_limit, enforcement_subject, active, module
|
||||
)
|
||||
VALUES (%s, 'shinkan', %s, %s, %s, %s, %s, %s, %s, true, %s)
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
name = EXCLUDED.name,
|
||||
description = EXCLUDED.description,
|
||||
category = EXCLUDED.category,
|
||||
limit_type = EXCLUDED.limit_type,
|
||||
reset_period = EXCLUDED.reset_period,
|
||||
default_limit = EXCLUDED.default_limit,
|
||||
enforcement_subject = EXCLUDED.enforcement_subject,
|
||||
active = true,
|
||||
module = EXCLUDED.module
|
||||
""",
|
||||
(
|
||||
defn.id,
|
||||
defn.name,
|
||||
defn.description,
|
||||
defn.category,
|
||||
defn.limit_type,
|
||||
defn.reset_period,
|
||||
defn.default_limit,
|
||||
defn.enforcement_subject,
|
||||
defn.module,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def sync_rights_registry_to_db() -> Dict[str, int]:
|
||||
"""
|
||||
Startup: registrierte Module → DB. Admin-Matrix zeigt nur Einträge mit module.
|
||||
"""
|
||||
import rights_registrations # noqa: F401 — lädt alle Modul-Registrierungen
|
||||
|
||||
with get_db() as conn:
|
||||
cur = get_cursor(conn)
|
||||
for defn in _CAPABILITY_REGISTRY.values():
|
||||
_upsert_capability(cur, defn)
|
||||
for defn in _FEATURE_REGISTRY.values():
|
||||
_upsert_feature(cur, defn)
|
||||
conn.commit()
|
||||
|
||||
return {
|
||||
"capabilities": len(_CAPABILITY_REGISTRY),
|
||||
"features": len(_FEATURE_REGISTRY),
|
||||
}
|
||||
|
|
@ -92,10 +92,10 @@ def get_capability_matrix(session: dict = Depends(require_auth)):
|
|||
cur = get_cursor(conn)
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT id, name, domain, min_account_state, linked_feature_id
|
||||
SELECT id, name, domain, min_account_state, linked_feature_id, module
|
||||
FROM capabilities
|
||||
WHERE active = true
|
||||
ORDER BY domain, id
|
||||
WHERE active = true AND module IS NOT NULL
|
||||
ORDER BY module, domain, id
|
||||
"""
|
||||
)
|
||||
capabilities = []
|
||||
|
|
@ -130,6 +130,11 @@ def get_capability_matrix(session: dict = Depends(require_auth)):
|
|||
"capabilities": capabilities,
|
||||
"portal_grants": portal_grants,
|
||||
"club_role_grants": club_role_grants,
|
||||
"registry_only": True,
|
||||
"hint": (
|
||||
"Nur vom Modul registrierte Rechte (capabilities.module). "
|
||||
"Legacy-Katalog-Seed ohne module erscheint nicht."
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -453,10 +458,11 @@ def get_club_plans_matrix(session: dict = Depends(require_auth)):
|
|||
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT id, name, description, category, limit_type, reset_period, default_limit
|
||||
SELECT id, name, description, category, limit_type, reset_period, default_limit, module
|
||||
FROM features
|
||||
WHERE app = 'shinkan' AND active = true AND enforcement_subject = 'club'
|
||||
ORDER BY category, id
|
||||
AND module IS NOT NULL
|
||||
ORDER BY module, category, id
|
||||
"""
|
||||
)
|
||||
features = [r2d(r) for r in cur.fetchall()]
|
||||
|
|
|
|||
33
backend/tests/test_rights_registry.py
Normal file
33
backend/tests/test_rights_registry.py
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
"""Registry-first: Modul-Registrierungen."""
|
||||
import rights_registrations # noqa: F401
|
||||
from rights_registry import (
|
||||
CapabilityRegistration,
|
||||
registered_capabilities,
|
||||
registered_features,
|
||||
register_capability,
|
||||
)
|
||||
|
||||
|
||||
def test_exercises_module_registers_wired_capabilities():
|
||||
assert "exercises.ai.suggest" in registered_capabilities()
|
||||
assert registered_capabilities()["exercises.ai.suggest"].module == "exercises"
|
||||
|
||||
|
||||
def test_register_capability_requires_module():
|
||||
try:
|
||||
register_capability(
|
||||
CapabilityRegistration(
|
||||
id="test.no.module",
|
||||
name="Test",
|
||||
domain="test",
|
||||
module="",
|
||||
)
|
||||
)
|
||||
assert False, "expected ValueError"
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
|
||||
def test_registered_features_include_ai_calls():
|
||||
assert "ai_calls" in registered_features()
|
||||
assert registered_features()["ai_calls"].module == "exercises"
|
||||
|
|
@ -1,15 +1,16 @@
|
|||
# Shinkan Jinkendo Version Information
|
||||
|
||||
APP_VERSION = "0.8.200"
|
||||
APP_VERSION = "0.8.201"
|
||||
BUILD_DATE = "2026-06-07"
|
||||
DB_SCHEMA_VERSION = "20260606083"
|
||||
DB_SCHEMA_VERSION = "20260606084"
|
||||
|
||||
MODULE_VERSIONS = {
|
||||
"legal_documents": "1.4.0", # Admin: Live-Vorschau pro Abschnitt + modale Vollvorschau (Editor + Dokumentenliste)
|
||||
"auth": "1.2.3", # P-05b: reset-password min_length=8 via Pydantic PasswordResetConfirm
|
||||
"profiles": "1.8.1", # GET /profiles/me: account_state + club_roles
|
||||
"tenant_context": "1.1.0", # M3: account_state + email_verified im TenantContext
|
||||
"capabilities": "1.1.0", # quota_bypass-Domain + profile_capability_grants in check_capability
|
||||
"capabilities": "1.2.0", # Registry-first: module-Spalte; Admin nur registrierte Rechte
|
||||
"rights_registry": "1.0.0", # register_capability/feature + startup sync
|
||||
"account_lifecycle": "1.1.0", # Phase A: account_onboarding_gate API-Middleware
|
||||
"clubs": "0.4.2", # delete_club: Gründungsanträge → superseded
|
||||
"club_memberships": "1.0.1", # Depends(get_tenant_context)
|
||||
|
|
|
|||
|
|
@ -9,6 +9,13 @@ Diese Roadmap bündelt **was fertig ist**, **was als Standard gilt** und **was n
|
|||
|
||||
## 1. Architektur-Standard (verbindlich)
|
||||
|
||||
### Registry-first (korrigiert 2026-06-07)
|
||||
|
||||
**Nicht:** vollständiger Capability-Katalog in Migration 079.
|
||||
**Sondern:** Module registrieren Rechte/Kontingente bei Implementierung → `docs/working/RIGHTS_AND_FEATURES_REGISTRY.md`.
|
||||
|
||||
Admin-Matrix zeigt nur `capabilities.module IS NOT NULL` — keine vorgetäuschte Vollständigkeit.
|
||||
|
||||
### Request-Kette (Ziel)
|
||||
|
||||
```
|
||||
|
|
|
|||
90
docs/working/RIGHTS_AND_FEATURES_REGISTRY.md
Normal file
90
docs/working/RIGHTS_AND_FEATURES_REGISTRY.md
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
# Rechte & Kontingente — Registry-first (Zielarchitektur)
|
||||
|
||||
**Stand:** 2026-06-07 · **Status:** verbindlich (korrigiert Katalog-first aus Migration 079)
|
||||
|
||||
---
|
||||
|
||||
## 1. Problem mit dem Katalog-first-Ansatz
|
||||
|
||||
Migration `079_capabilities.sql` hat **~70 Rechte vorab** in die DB geschrieben — aus einer Spekulation über die fertige App. Das ist für ein System im Aufbau **verkehrt herum**:
|
||||
|
||||
- Vollständige Liste ist **nicht möglich** und nicht wünschenswert
|
||||
- Die Matrix **suggeriert** Funktionen, die es am Endpoint noch nicht gibt
|
||||
- Module **registrieren sich nicht** — alles war manueller Seed
|
||||
|
||||
**Korrektur:** Registry-first — wie bei anderen Registries im Projekt (z. B. Platzhalter-Pflicht).
|
||||
|
||||
---
|
||||
|
||||
## 2. Zielbild
|
||||
|
||||
```
|
||||
Modul implementiert Feature
|
||||
→ register_capability() / register_feature() in rights_registrations/<modul>.py
|
||||
→ Startup: sync_rights_registry_to_db()
|
||||
→ Admin „Rollen & Rechte“ zeigt nur Einträge mit module IS NOT NULL
|
||||
→ Endpoint: probe_capability + probe/consume Kontingent
|
||||
```
|
||||
|
||||
| Achse | Registrierung | Konfiguration Admin |
|
||||
|-------|---------------|---------------------|
|
||||
| **Recht** | `CapabilityRegistration` | Matrix Vereins-/Portal-Rollen |
|
||||
| **Kontingent** | `FeatureRegistration` | Vereinspläne / Limits |
|
||||
|
||||
Kein neuer Eintrag in `079`-artigen Bulk-Migrations für fachliche Rechte.
|
||||
|
||||
---
|
||||
|
||||
## 3. Implementierung (Code)
|
||||
|
||||
| Pfad | Rolle |
|
||||
|------|--------|
|
||||
| `backend/rights_registry.py` | `register_capability`, `register_feature`, `sync_rights_registry_to_db` |
|
||||
| `backend/rights_registrations/*.py` | Pro Modul nur **tatsächlich verdrahtete** Rechte/Kontingente |
|
||||
| `backend/main.py` | Sync nach Migrationen |
|
||||
| Migration `084_rights_registry_module.sql` | Spalte `module` auf `capabilities` + `features` |
|
||||
| `admin_rights.py` | Matrix-Query: `WHERE module IS NOT NULL` |
|
||||
|
||||
### Neues Modul anbinden (Pflicht)
|
||||
|
||||
1. Datei `rights_registrations/mein_modul.py` anlegen
|
||||
2. `register_capability` / `register_feature` aufrufen
|
||||
3. In `rights_registrations/__init__.py` importieren
|
||||
4. Endpoint: `probe_capability` + ggf. `consume_club_feature_with_usage`
|
||||
5. `capability_enforcement_audit.WIRED_PROBE` ergänzen
|
||||
|
||||
**Kein** Eintrag in `CAPABILITY_CATALOG` als Voraussetzung für DB — der Katalog wird zur **Dokumentation** der Namenskonvention, nicht zur Seed-Quelle.
|
||||
|
||||
---
|
||||
|
||||
## 4. Legacy-Katalog (079)
|
||||
|
||||
- Bleibt in der DB (`module IS NULL`) für Übergang / `check_capability`-Kompatibilität
|
||||
- Erscheint **nicht** mehr in der Admin-Matrix
|
||||
- Wird nicht erweitert — neue Rechte nur über Registry
|
||||
- Langfristig: ungenutzte Seed-Zeilen deaktivieren oder archivieren
|
||||
|
||||
---
|
||||
|
||||
## 5. Aktuell registrierte Module (Start)
|
||||
|
||||
| Modul | Rechte | Kontingente |
|
||||
|-------|--------|-------------|
|
||||
| `exercises` | KI suggest/regenerate, create, media.upload | `ai_calls`, `exercises`, `exercise_media` |
|
||||
| `planning_exercise_suggest` | planning.ai.* | (nutzt `ai_calls`) |
|
||||
| `club_creation_requests` | Gründung + approve | — |
|
||||
| `platform` | admin.access, quota.bypass | — |
|
||||
|
||||
Weitere Module folgen **mit ihrer Implementierung**, nicht vorher.
|
||||
|
||||
---
|
||||
|
||||
## 6. Referenzen
|
||||
|
||||
- `docs/working/RBAC_ENFORCEMENT_ROADMAP.md` — Enforcement nach Verdrahtung
|
||||
- `MEMBERSHIP_RBAC_DECISIONS_2026-06.md` — Produktentscheidungen
|
||||
- `CLUB_MEMBERSHIP_AND_FEATURES.v1.md` — Kontingent-Semantik
|
||||
|
||||
**Changelog**
|
||||
|
||||
- 2026-06-07: Registry-first als verbindliche Korrektur; Migration 084; Pilot-Registrierungen.
|
||||
|
|
@ -328,8 +328,9 @@ export default function AdminRightsPage() {
|
|||
<strong>Kontingente:</strong> Wie viel darf ein Verein verbrauchen (an Rechten gekoppelt).
|
||||
<br />
|
||||
<span style={{ fontSize: '0.85rem' }}>
|
||||
● = an API angebunden · ○ = nur Legacy oder noch nicht durchgesetzt. Roadmap:{' '}
|
||||
<code>docs/working/RBAC_ENFORCEMENT_ROADMAP.md</code>
|
||||
Es erscheinen nur <strong>vom Modul registrierte</strong> Rechte (nicht der alte
|
||||
Vollkatalog). ● = an API angebunden · ○ = registriert, Endpoint fehlt noch.{' '}
|
||||
<code>docs/working/RIGHTS_AND_FEATURES_REGISTRY.md</code>
|
||||
</span>
|
||||
</p>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user