shinkan-jinkendo/backend/rights_registry.py
Lars 4130a63dfe
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
Implement Registry-First Approach for Rights and Capabilities Management
- 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.
2026-06-07 15:36:31 +02:00

160 lines
4.9 KiB
Python

"""
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),
}