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

182 lines
6.6 KiB
Python

from __future__ import annotations
from collections.abc import Mapping, Sequence
from dataclasses import dataclass
import hashlib
from sqlalchemy.orm import Session
from govoplan_core.core.distribution_lists import (
DistributionChannelPolicyDecision,
DistributionChannelPolicyRequest,
)
from govoplan_policy.backend.policy_overrides import resolution_policy_overrides
CHANNELS = frozenset({"email", "postal", "internal_mail", "portal"})
@dataclass(frozen=True, slots=True)
class DistributionChannelPolicyResolution:
allowed_channels: frozenset[str]
source_path: tuple[Mapping[str, object], ...] = ()
diagnostics: tuple[Mapping[str, object], ...] = ()
class DistributionChannelPolicyProvider:
def resolve_distribution_channel(
self,
session: object,
principal: object,
*,
request: DistributionChannelPolicyRequest,
) -> DistributionChannelPolicyDecision:
resolution = _resolve(session, principal, request)
channel = request.candidate.channel
allowed = channel in resolution.allowed_channels
malformed = any(
item.get("code") == "distribution_channel_policy.invalid"
for item in resolution.diagnostics
)
if malformed and not allowed:
reason_code = "policy.invalid_fail_closed"
explanation = (
"Distribution is blocked because an applicable channel policy is invalid."
)
elif allowed:
reason_code = "policy.channel_allowed"
explanation = f"The {channel} channel is permitted by the effective Policy."
else:
reason_code = "policy.channel_blocked"
explanation = f"The {channel} channel is blocked by the effective Policy."
return DistributionChannelPolicyDecision(
allowed=allowed,
reason_code=reason_code,
explanation=explanation,
source_path=resolution.source_path,
requirements=(() if allowed else (f"policy.channel.{channel}",)),
details={
"allowed_channels": sorted(resolution.allowed_channels),
"diagnostics": [dict(item) for item in resolution.diagnostics],
},
)
def resolve_distribution_channel_policy_rows(
rows: object,
) -> DistributionChannelPolicyResolution:
allowed_channels = set(CHANNELS)
source_path: list[Mapping[str, object]] = []
diagnostics: list[Mapping[str, object]] = []
for row in rows if isinstance(rows, (list, tuple)) else ():
policy, malformed = validate_distribution_channel_policy(row.policy)
if malformed:
allowed_channels.clear()
applied_fields: tuple[str, ...] = ("configuration_status",)
source_policy: Mapping[str, object] = {
"configuration_status": "invalid_fail_closed",
"target_key": row.target_key,
}
diagnostics.append(
{
"code": "distribution_channel_policy.invalid",
"severity": "error",
"scope": row.scope_key,
"target_key": row.target_key,
}
)
else:
configured = policy.get("allowed_channels")
blocked = policy.get("blocked_channels", ())
if isinstance(configured, tuple):
allowed_channels.intersection_update(configured)
if isinstance(blocked, tuple):
allowed_channels.difference_update(blocked)
applied_fields = tuple(sorted(policy))
source_policy = {
key: list(value) if isinstance(value, tuple) else value
for key, value in policy.items()
}
source_policy = {**source_policy, "target_key": row.target_key}
source_path.append(
{
"scope_type": row.scope_type,
"scope_id": row.scope_id,
"label": f"{row.scope_type.capitalize()} distribution-channel policy",
"applied_fields": list(applied_fields),
"policy": dict(source_policy),
}
)
return DistributionChannelPolicyResolution(
allowed_channels=frozenset(allowed_channels),
source_path=tuple(source_path),
diagnostics=tuple(diagnostics),
)
def validate_distribution_channel_policy(
value: object,
) -> tuple[dict[str, tuple[str, ...]], bool]:
if not isinstance(value, Mapping):
return {}, True
supported = {"allowed_channels", "blocked_channels"}
if any(str(key) not in supported for key in value):
return {}, True
policy: dict[str, tuple[str, ...]] = {}
for key, raw in value.items():
if not isinstance(raw, Sequence) or isinstance(raw, (str, bytes)):
return {}, True
channels = tuple(dict.fromkeys(str(item).strip() for item in raw))
if any(channel not in CHANNELS for channel in channels):
return {}, True
policy[str(key)] = channels
return policy, False
def _resolve(
session: object,
principal: object,
request: DistributionChannelPolicyRequest,
) -> DistributionChannelPolicyResolution:
if not isinstance(session, Session):
return DistributionChannelPolicyResolution(allowed_channels=CHANNELS)
group_ids = tuple(getattr(principal, "group_ids", ()) or ())
account_id = str(getattr(principal, "account_id", "") or "")
membership_id = str(getattr(principal, "membership_id", "") or "")
target_keys = ["*", _target_key("list", request.list_id)]
if request.purpose:
target_keys.extend(
(
_target_key("purpose", request.purpose),
_target_key(
f"list:{request.list_id}:purpose",
request.purpose,
),
)
)
rows = resolution_policy_overrides(
session,
policy_family="distribution_channels",
target_keys=target_keys,
tenant_id=request.tenant_id,
group_ids=group_ids,
user_ids=(account_id, membership_id),
)
return resolve_distribution_channel_policy_rows(rows)
def _target_key(prefix: str, value: str) -> str:
candidate = f"{prefix}:{value}"
if len(candidate) <= 120:
return candidate
digest = hashlib.sha256(value.encode("utf-8")).hexdigest()
return f"{prefix[:78]}:sha256:{digest[:32]}"
__all__ = [
"CHANNELS",
"DistributionChannelPolicyProvider",
"DistributionChannelPolicyResolution",
"resolve_distribution_channel_policy_rows",
"validate_distribution_channel_policy",
]