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", ]