234 lines
6.6 KiB
Python
234 lines
6.6 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", "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",
|
|
]
|