Files
govoplan-policy/src/govoplan_policy/backend/policy_overrides.py
T

205 lines
5.9 KiB
Python

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"})
POLICY_FAMILIES = frozenset({"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 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, or user")
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] = (),
) -> 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)}))
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]
)
)
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": 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",
]