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
|
## 9. Abgrenzung & Drift-Schutz
|
||||||
|
|
||||||
1. **Neue Nutzerfunktion** → zuerst Capability-ID hier eintragen, dann Endpoint.
|
1. **Neue Nutzerfunktion** → `register_capability()` in `rights_registrations/<modul>.py`, dann Endpoint mit `probe_capability`. Namenskonvention hier dokumentieren — **kein** Bulk-Seed in Migrationen.
|
||||||
2. **Kein** paralleles `if (user.role === 'trainer')` für Sicherheit — nur UX-Fallback.
|
2. **Kontingent** → `register_feature()` im selben Modul; Consume über `consume_club_feature_with_usage`.
|
||||||
3. Capability ≠ Feature: `exercises.ai.suggest` (darf ich?) vs. `ai_calls` (wie viel übrig?).
|
3. **Kein** paralleles `if (user.role === 'trainer')` für Sicherheit — nur UX-Fallback.
|
||||||
4. Plattform-Admin-Bypass dokumentieren und auditieren (`platform_admin` sieht Mandant, nicht automatisch alle Quotas).
|
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}")
|
print(f"[FAIL] Migration-Laufzeitfehler: {e}")
|
||||||
sys.exit(1)
|
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
|
from routers.auth import limiter as auth_rate_limiter
|
||||||
|
|
||||||
# OpenAPI: in Produktion standardmäßig aus (Schema nicht öffentlich). Notfall: PUBLIC_OPENAPI=1
|
# 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 = get_cursor(conn)
|
||||||
cur.execute(
|
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
|
FROM capabilities
|
||||||
WHERE active = true
|
WHERE active = true AND module IS NOT NULL
|
||||||
ORDER BY domain, id
|
ORDER BY module, domain, id
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
capabilities = []
|
capabilities = []
|
||||||
|
|
@ -130,6 +130,11 @@ def get_capability_matrix(session: dict = Depends(require_auth)):
|
||||||
"capabilities": capabilities,
|
"capabilities": capabilities,
|
||||||
"portal_grants": portal_grants,
|
"portal_grants": portal_grants,
|
||||||
"club_role_grants": club_role_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(
|
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
|
FROM features
|
||||||
WHERE app = 'shinkan' AND active = true AND enforcement_subject = 'club'
|
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()]
|
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
|
# Shinkan Jinkendo Version Information
|
||||||
|
|
||||||
APP_VERSION = "0.8.200"
|
APP_VERSION = "0.8.201"
|
||||||
BUILD_DATE = "2026-06-07"
|
BUILD_DATE = "2026-06-07"
|
||||||
DB_SCHEMA_VERSION = "20260606083"
|
DB_SCHEMA_VERSION = "20260606084"
|
||||||
|
|
||||||
MODULE_VERSIONS = {
|
MODULE_VERSIONS = {
|
||||||
"legal_documents": "1.4.0", # Admin: Live-Vorschau pro Abschnitt + modale Vollvorschau (Editor + Dokumentenliste)
|
"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
|
"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
|
"profiles": "1.8.1", # GET /profiles/me: account_state + club_roles
|
||||||
"tenant_context": "1.1.0", # M3: account_state + email_verified im TenantContext
|
"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
|
"account_lifecycle": "1.1.0", # Phase A: account_onboarding_gate API-Middleware
|
||||||
"clubs": "0.4.2", # delete_club: Gründungsanträge → superseded
|
"clubs": "0.4.2", # delete_club: Gründungsanträge → superseded
|
||||||
"club_memberships": "1.0.1", # Depends(get_tenant_context)
|
"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)
|
## 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)
|
### 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).
|
<strong>Kontingente:</strong> Wie viel darf ein Verein verbrauchen (an Rechten gekoppelt).
|
||||||
<br />
|
<br />
|
||||||
<span style={{ fontSize: '0.85rem' }}>
|
<span style={{ fontSize: '0.85rem' }}>
|
||||||
● = an API angebunden · ○ = nur Legacy oder noch nicht durchgesetzt. Roadmap:{' '}
|
Es erscheinen nur <strong>vom Modul registrierte</strong> Rechte (nicht der alte
|
||||||
<code>docs/working/RBAC_ENFORCEMENT_ROADMAP.md</code>
|
Vollkatalog). ● = an API angebunden · ○ = registriert, Endpoint fehlt noch.{' '}
|
||||||
|
<code>docs/working/RIGHTS_AND_FEATURES_REGISTRY.md</code>
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user