Persist hierarchical definition policy overrides
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
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", "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 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",
|
||||
]
|
||||
Reference in New Issue
Block a user