from __future__ import annotations from collections.abc import Mapping from govoplan_core.core.policy import ( FunctionAssignmentGovernanceDecision, FunctionAssignmentGovernanceRequest, PolicySourceStep, ) from govoplan_core.security.module_permissions import scopes_grant_compatible SUPPORTED_PROFILES = { "holder_grant", "holder_with_authority_clearance", "authority_only", "unavailable", } class FunctionAssignmentGovernancePolicyProvider: def resolve_function_assignment_action( self, session: object | None = None, *, request: FunctionAssignmentGovernanceRequest, ) -> FunctionAssignmentGovernanceDecision: del session policy = _policy(request.function_settings) profile = ( str(policy.get(f"{request.kind}_profile") or "unavailable") .strip() .casefold() ) if profile not in SUPPORTED_PROFILES: return _decision( request, policy, profile="unavailable", allowed=False, reason=f"Unsupported function assignment profile: {profile}.", requirements=("valid_profile",), ) required_steps = _required_steps( request.kind, profile, recipient_acceptance=_bool( policy.get("recipient_acceptance_required"), default=request.kind == "grant", ), ) authority_function_id = _text(policy.get("authority_function_id")) requirements: list[str] = [] if "authority" in required_steps and authority_function_id is None: requirements.append("authority_function") evidence_required = _bool( policy.get("evidence_required"), default=False, ) if evidence_required and not request.context.get("has_evidence"): requirements.append("evidence") allowed, reason = _action_decision( request, profile=profile, required_steps=required_steps, ) if profile == "unavailable": allowed = False reason = "Function assignment requests and grants are disabled." if requirements and request.action == "submit": allowed = False reason = _requirements_reason(requirements) return _decision( request, policy, profile=profile, allowed=allowed, reason=reason, required_steps=required_steps, authority_function_id=authority_function_id, evidence_required=evidence_required, requirements=tuple(requirements), ) def _action_decision( request: FunctionAssignmentGovernanceRequest, *, profile: str, required_steps: tuple[str, ...], ) -> tuple[bool, str | None]: context = request.context action = request.action if action == "submit": if request.kind == "request": if not bool(context.get("candidate_is_actor")): return False, "A function request must target the requesting identity." if profile == "authority_only": allowed = bool(context.get("actor_is_authority")) return ( allowed, None if allowed else "Only the designated authority may initiate this assignment.", ) return True, None if profile == "authority_only": allowed = bool(context.get("actor_is_authority")) reason = "Only the designated authority may initiate this grant." else: allowed = bool(context.get("actor_is_holder")) reason = "An effective function holder must initiate this grant." return allowed, None if allowed else reason if action == "approve_holder": allowed = "holder" in required_steps and bool(context.get("actor_is_holder")) return allowed, None if allowed else "A current holder must approve." if action == "approve_authority": allowed = "authority" in required_steps and bool( context.get("actor_is_authority") ) return allowed, None if allowed else "The designated authority must approve." if action == "accept_recipient": allowed = "recipient" in required_steps and bool( context.get("candidate_is_actor") ) return allowed, None if allowed else "The candidate must accept this grant." if action in {"reject", "request_changes"}: if request.current_state == "awaiting_holder": allowed = bool(context.get("actor_is_holder")) reason = "A current holder must act at the holder review step." elif request.current_state == "awaiting_authority": allowed = bool(context.get("actor_is_authority")) reason = "The designated authority must act at the authority step." elif request.current_state == "awaiting_recipient": allowed = bool(context.get("candidate_is_actor")) reason = "Only the candidate may act at recipient acceptance." else: allowed = False reason = "The current state does not accept this review action." return allowed, None if allowed else reason if action == "respond": allowed = request.current_state == "changes_requested" and ( bool(context.get("actor_is_initiator")) or bool(context.get("candidate_is_actor")) ) return ( allowed, None if allowed else "Only the initiator or candidate may respond to requested changes.", ) if action == "withdraw": allowed = bool(context.get("actor_is_initiator")) return ( allowed, None if allowed else "Only the initiator may withdraw this change.", ) if action == "recover": allowed = _has_scope(request, "idm:function_change:admin") return ( allowed, None if allowed else "Administrative recovery permission is required.", ) if action == "apply": allowed = bool(context.get("approvals_complete")) return allowed, None if allowed else "Required decisions are incomplete." return False, f"Unsupported function assignment action: {action}." def _required_steps( kind: str, profile: str, *, recipient_acceptance: bool, ) -> tuple[str, ...]: if profile == "holder_grant": steps = ["holder"] elif profile == "holder_with_authority_clearance": steps = ["holder", "authority"] elif profile == "authority_only": steps = ["authority"] else: steps = [] if kind == "grant" and recipient_acceptance: steps.append("recipient") return tuple(steps) def _decision( request: FunctionAssignmentGovernanceRequest, policy: Mapping[str, object], *, profile: str, allowed: bool, reason: str | None, required_steps: tuple[str, ...] = (), authority_function_id: str | None = None, evidence_required: bool = False, requirements: tuple[str, ...] = (), ) -> FunctionAssignmentGovernanceDecision: recipient_required = "recipient" in required_steps return FunctionAssignmentGovernanceDecision( allowed=allowed, reason=reason, profile=profile, required_steps=required_steps, authority_function_id=authority_function_id, evidence_required=evidence_required, recipient_acceptance_required=recipient_required, separation_of_duties=_bool( policy.get("separation_of_duties"), default=True, ), quorum=_bounded_int(policy.get("quorum"), default=1, minimum=1, maximum=20), maximum_validity_days=_optional_positive_int( policy.get("maximum_validity_days"), maximum=3650, ), request_expiry_hours=_bounded_int( policy.get("request_expiry_hours"), default=336, minimum=1, maximum=8760, ), source_path=( PolicySourceStep( scope_type="tenant", scope_id=request.tenant_id, label="Organization function assignment policy", applied_fields=tuple(sorted(policy)), policy=policy, ), ), requirements=requirements, details={ "function_id": request.function_id, "kind": request.kind, "action": request.action, }, ) def _policy(settings: Mapping[str, object]) -> dict[str, object]: raw = settings.get("assignment_governance") return dict(raw) if isinstance(raw, Mapping) else {} def _requirements_reason(requirements: list[str]) -> str: labels = { "authority_function": "a designated authority function", "evidence": "the required evidence", "valid_profile": "a supported governance profile", } return ( "Submission requires " + ", ".join(labels.get(item, item.replace("_", " ")) for item in requirements) + "." ) def _has_scope( request: FunctionAssignmentGovernanceRequest, scope: str, ) -> bool: return scopes_grant_compatible(request.actor.scopes, scope) def _text(value: object) -> str | None: text = str(value).strip() if value is not None else "" return text or None def _bool(value: object, *, default: bool) -> bool: return value if isinstance(value, bool) else default def _bounded_int( value: object, *, default: int, minimum: int, maximum: int, ) -> int: try: number = int(value) if value is not None else default except (TypeError, ValueError): return default return max(minimum, min(number, maximum)) def _optional_positive_int(value: object, *, maximum: int) -> int | None: if value is None: return None try: number = int(value) except (TypeError, ValueError): return None return max(1, min(number, maximum)) __all__ = [ "FunctionAssignmentGovernancePolicyProvider", "SUPPORTED_PROFILES", ]