"""In-memory capability registry with startup sync to PostgreSQL.""" from __future__ import annotations from dataclasses import dataclass from db import get_connection RoleGrant = tuple[str, str] # (role_scope, role_code) @dataclass(frozen=True) class CapabilityRegistration: key: str module: str description: str default_grants: tuple[RoleGrant, ...] = () _REGISTRY: dict[str, CapabilityRegistration] = {} def register_capability(registration: CapabilityRegistration) -> None: if not registration.key or not registration.key.strip(): raise ValueError("Capability key is required") if not registration.module or not registration.module.strip(): raise ValueError("Capability module is required") if not registration.description or not registration.description.strip(): raise ValueError("Capability description is required") if registration.key in _REGISTRY: raise ValueError(f"Capability already registered: {registration.key}") _REGISTRY[registration.key] = registration def get_registered_capabilities() -> tuple[CapabilityRegistration, ...]: return tuple(_REGISTRY.values()) def clear_registry_for_tests() -> None: """Test helper — reset in-memory registry.""" _REGISTRY.clear() def sync_rights_registry_to_db() -> int: """Upsert capabilities and ensure default grants from registry.""" if not _REGISTRY: print("[rights_registry] Keine Capabilities registriert — Sync übersprungen") return 0 conn = get_connection() try: with conn.cursor() as cur: for reg in _REGISTRY.values(): cur.execute( """ INSERT INTO capabilities (capability_key, module, description, is_active) VALUES (%s, %s, %s, TRUE) ON CONFLICT (capability_key) DO UPDATE SET module = EXCLUDED.module, description = EXCLUDED.description, is_active = TRUE, updated_at = NOW() """, (reg.key, reg.module, reg.description), ) for role_scope, role_code in reg.default_grants: cur.execute( """ INSERT INTO role_capability_grants (role_scope, role_code, capability_key) VALUES (%s, %s, %s) ON CONFLICT (role_scope, role_code, capability_key) DO NOTHING """, (role_scope, role_code, reg.key), ) conn.commit() print(f"[rights_registry] Sync OK — {len(_REGISTRY)} Capability(s)") return 0 except Exception as exc: conn.rollback() print(f"[rights_registry] Sync FAIL: {exc}") return 1 finally: conn.close() def load_grants_for_roles( *, portal_role: str, tenant_role: str | None, ) -> set[str]: """Resolve capability keys granted by portal and optional tenant role.""" conn = get_connection() try: keys: set[str] = set() with conn.cursor() as cur: cur.execute( """ SELECT c.capability_key FROM role_capability_grants g JOIN capabilities c ON c.capability_key = g.capability_key WHERE g.role_scope = 'portal' AND g.role_code = %s AND c.is_active = TRUE """, (portal_role,), ) keys.update(row[0] for row in cur.fetchall()) if tenant_role: cur.execute( """ SELECT c.capability_key FROM role_capability_grants g JOIN capabilities c ON c.capability_key = g.capability_key WHERE g.role_scope = 'tenant' AND g.role_code = %s AND c.is_active = TRUE """, (tenant_role,), ) keys.update(row[0] for row in cur.fetchall()) return keys finally: conn.close()