""" RBAC Engine =========== Loads roles from roles_config.yaml and resolves permissions for a given role. This is the core evaluation logic — kept pure (no FastAPI dependencies). """ import os import yaml from functools import lru_cache from typing import Set, Optional from src.core.rbac.permissions_registry import ALL_PERMISSIONS _CONFIG_PATH = os.path.join(os.path.dirname(__file__), "roles_config.yaml") @lru_cache(maxsize=1) def _load_roles_config() -> dict: """Load and parse roles_config.yaml once, then cache in memory.""" with open(_CONFIG_PATH, "r") as f: data = yaml.safe_load(f) return data.get("roles", {}) def reload_roles(): """Force reload — call this if you hot-update roles_config.yaml at runtime.""" _load_roles_config.cache_clear() def get_role_permissions(role_name: str) -> Set[str]: """Return the full permission set for a given role slug.""" roles = _load_roles_config() role_def = roles.get(role_name, {}) raw_perms = role_def.get("permissions", []) valid = set() for perm in raw_perms: if perm in ALL_PERMISSIONS: valid.add(perm) else: # Unknown permission string in yaml — warn but don't crash import warnings warnings.warn( f"[RBAC] Unknown permission '{perm}' in role '{role_name}' — skipping.", stacklevel=2, ) return valid def has_permission(role_name: str, permission: str) -> bool: """Check if a role has a specific permission.""" return permission in get_role_permissions(role_name) def has_any_permission(role_name: str, permissions: list[str]) -> bool: """Check if a role has AT LEAST ONE of the listed permissions.""" role_perms = get_role_permissions(role_name) return any(p in role_perms for p in permissions) def has_all_permissions(role_name: str, permissions: list[str]) -> bool: """Check if a role has ALL of the listed permissions.""" role_perms = get_role_permissions(role_name) return all(p in role_perms for p in permissions) def list_all_roles() -> list[dict]: """Return all defined roles with their metadata and permissions.""" roles = _load_roles_config() result = [] for slug, data in roles.items(): result.append({ "slug": slug, "description": data.get("description", ""), "permissions": data.get("permissions", []), "permission_count": len(data.get("permissions", [])), }) return result def mask_pii_fields(role_name: str, data: dict, field_map: dict) -> dict: """ Mask any PII fields in `data` that the role is not permitted to see. Args: role_name: The current user's role slug data: The flat dict of record data field_map: Maps data field names to pii permission strings e.g. {"phone": "pii.applicant.phone", "aadhaar": "pii.applicant.aadhaar"} Returns: A copy of `data` with unauthorized PII fields replaced with "***" """ role_perms = get_role_permissions(role_name) result = dict(data) for field_key, perm_key in field_map.items(): if field_key in result and perm_key not in role_perms: result[field_key] = "***" return result