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.
160 lines
4.9 KiB
Python
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),
|
|
}
|