from __future__ import annotations from collections.abc import Iterable from typing import Any from sqlalchemy import or_ from sqlalchemy.orm import Session from govoplan_policy.backend.db.models import PolicyOverride POLICY_SCOPE_TYPES = frozenset({"system", "tenant", "group", "user", "campaign"}) POLICY_FAMILIES = frozenset( { "campaign_archive_encryption", "datasource_visibility", "definition", "distribution_channels", "view", } ) class PolicyOverrideError(ValueError): pass def normalize_policy_target(policy_family: str, target_key: str) -> tuple[str, str]: family = policy_family.strip().casefold() target = target_key.strip().casefold() if family not in POLICY_FAMILIES: raise PolicyOverrideError( "Policy family must be campaign_archive_encryption, datasource_visibility, definition, distribution_channels, or view" ) if not target or len(target) > 120: raise PolicyOverrideError("Policy target must contain 1 to 120 characters") if family == "view" and target != "*": raise PolicyOverrideError("View policy uses the shared '*' target") return family, target def normalize_policy_scope( *, tenant_id: str, scope_type: str, scope_id: str | None, ) -> tuple[str | None, str | None, str]: clean_scope = scope_type.strip().casefold() clean_id = str(scope_id or "").strip() or None if clean_scope not in POLICY_SCOPE_TYPES: raise PolicyOverrideError( "Policy scope must be system, tenant, group, user, or campaign" ) if clean_scope == "system": if clean_id is not None: raise PolicyOverrideError("System policy cannot declare a scope ID") return None, None, "system" if clean_scope == "tenant": if clean_id not in {None, tenant_id}: raise PolicyOverrideError("Tenant policy must target the active tenant") return tenant_id, tenant_id, f"tenant:{tenant_id}" if clean_id is None: raise PolicyOverrideError( f"{clean_scope.capitalize()} policy requires a scope ID" ) return tenant_id, clean_id, f"{clean_scope}:{tenant_id}:{clean_id}" def get_policy_override( session: Session, *, policy_family: str, target_key: str, tenant_id: str, scope_type: str, scope_id: str | None = None, ) -> PolicyOverride | None: family, target = normalize_policy_target(policy_family, target_key) _, _, scope_key = normalize_policy_scope( tenant_id=tenant_id, scope_type=scope_type, scope_id=scope_id, ) return ( session.query(PolicyOverride) .filter( PolicyOverride.policy_family == family, PolicyOverride.target_key == target, PolicyOverride.scope_key == scope_key, ) .first() ) def set_policy_override( session: Session, *, policy_family: str, target_key: str, tenant_id: str, scope_type: str, scope_id: str | None, policy: Any, actor_id: str | None, ) -> PolicyOverride: family, target = normalize_policy_target(policy_family, target_key) row_tenant_id, clean_scope_id, scope_key = normalize_policy_scope( tenant_id=tenant_id, scope_type=scope_type, scope_id=scope_id, ) row = ( session.query(PolicyOverride) .filter( PolicyOverride.policy_family == family, PolicyOverride.target_key == target, PolicyOverride.scope_key == scope_key, ) .first() ) if row is None: row = PolicyOverride( policy_family=family, target_key=target, tenant_id=row_tenant_id, scope_type=scope_type.strip().casefold(), scope_id=clean_scope_id, scope_key=scope_key, policy=policy, created_by=actor_id, updated_by=actor_id, ) session.add(row) else: row.policy = policy row.revision += 1 row.updated_by = actor_id session.flush() return row def delete_policy_override(session: Session, row: PolicyOverride) -> None: session.delete(row) session.flush() def resolution_policy_overrides( session: Session, *, policy_family: str, target_keys: Iterable[str], tenant_id: str, group_ids: Iterable[str] = (), user_ids: Iterable[str] = (), campaign_ids: Iterable[str] = (), ) -> tuple[PolicyOverride, ...]: family = policy_family.strip().casefold() targets = tuple( dict.fromkeys( normalize_policy_target(family, target)[1] for target in target_keys ) ) groups = tuple(sorted({str(value) for value in group_ids if str(value)})) users = tuple(sorted({str(value) for value in user_ids if str(value)})) campaigns = tuple( sorted({str(value) for value in campaign_ids if str(value)}) ) scope_filters = [ PolicyOverride.scope_key == "system", PolicyOverride.scope_key == f"tenant:{tenant_id}", ] if groups: scope_filters.append( PolicyOverride.scope_key.in_( [f"group:{tenant_id}:{group_id}" for group_id in groups] ) ) if users: scope_filters.append( PolicyOverride.scope_key.in_( [f"user:{tenant_id}:{user_id}" for user_id in users] ) ) if campaigns: scope_filters.append( PolicyOverride.scope_key.in_( [ f"campaign:{tenant_id}:{campaign_id}" for campaign_id in campaigns ] ) ) rows = ( session.query(PolicyOverride) .filter( PolicyOverride.policy_family == family, PolicyOverride.target_key.in_(targets), or_(*scope_filters), ) .all() ) target_order = {target: index for index, target in enumerate(targets)} scope_order = { "system": 0, "tenant": 1, "group": 2, "user": 2, "campaign": 3, } return tuple( sorted( rows, key=lambda row: ( scope_order.get(row.scope_type, 99), row.scope_id or "", target_order.get(row.target_key, 99), row.id, ), ) ) __all__ = [ "POLICY_FAMILIES", "POLICY_SCOPE_TYPES", "PolicyOverrideError", "delete_policy_override", "get_policy_override", "normalize_policy_scope", "normalize_policy_target", "resolution_policy_overrides", "set_policy_override", ]