from __future__ import annotations from collections.abc import Mapping from typing import Literal from govoplan_core.auth import ApiPrincipal, has_scope from govoplan_core.core.policy import ( DefinitionGovernanceAction, DefinitionGovernanceRequest, DefinitionScopeRef, PolicyDecision, PolicySourceStep, definition_governance_policy, ) from govoplan_core.core.workflows import workflow_runtime_worker from govoplan_workflow_engine.backend.db.models import WorkflowDefinition WorkflowAction = Literal[ "view", "edit", "start", "reuse", "derive", "automate", ] WORKFLOW_ACTIONS: tuple[WorkflowAction, ...] = ( "view", "edit", "start", "reuse", "derive", "automate", ) def normalize_definition_scope( principal: ApiPrincipal, *, scope_type: str, scope_id: str | None, administrative: bool, ) -> tuple[str | None, str, str | None, str]: clean_type = scope_type.strip().casefold() clean_id = str(scope_id or "").strip() or None if clean_type == "system": if not has_scope(principal, "system:governance:write"): raise PermissionError( "System definitions require system governance permission." ) if clean_id is not None: raise ValueError("System definitions do not carry a scope ID.") return None, "system", None, "system" if clean_type == "tenant": if clean_id not in {None, principal.tenant_id}: raise PermissionError( "Definitions can only be created for the active tenant." ) return ( principal.tenant_id, "tenant", principal.tenant_id, f"tenant:{principal.tenant_id}", ) if clean_type == "group": if not clean_id: raise ValueError("Group definitions require a group ID.") if clean_id not in principal.group_ids and not administrative: raise PermissionError( "Definitions can only be created for one of the actor's groups." ) return principal.tenant_id, "group", clean_id, f"group:{clean_id}" if clean_type == "user": clean_id = clean_id or principal.account_id if ( clean_id not in {principal.membership_id, principal.account_id} and not administrative ): raise PermissionError( "Definitions can only be created for the current user." ) if clean_id == principal.membership_id: clean_id = principal.account_id return principal.tenant_id, "user", clean_id, f"user:{clean_id}" raise ValueError( "Definition scope must be system, tenant, group, or user." ) def definition_decision( definition: WorkflowDefinition, *, principal: ApiPrincipal, registry: object | None, action: WorkflowAction, ) -> PolicyDecision: policy_action: DefinitionGovernanceAction = ( "run" if action == "start" else action ) request = DefinitionGovernanceRequest( module_id="workflow", definition_ref=f"workflow-definition:{definition.id}", tenant_id=principal.tenant_id, definition_scope=DefinitionScopeRef( scope_type=definition.scope_type, # type: ignore[arg-type] scope_id=definition.scope_id, ), target_scope=_target_scope(definition, principal), definition_kind=definition.definition_kind, # type: ignore[arg-type] action=policy_action, actor=principal.to_platform_principal(), status=definition.status, inherit_to_lower_scopes=definition.inherit_to_lower_scopes, allow_run=definition.allow_start, allow_reuse=definition.allow_reuse, allow_automation=definition.allow_automation, context=_ancestor_context(definition), ) provider = definition_governance_policy(registry) if provider is not None: return provider.resolve_definition_action(request=request) return _tenant_local_fallback(request, displayed_action=action) def definition_governance_payload( definition: WorkflowDefinition, *, principal: ApiPrincipal, registry: object | None, ) -> dict[str, object]: runtime_available = workflow_runtime_worker(registry) is not None actions = { action: definition_decision( definition, principal=principal, registry=registry, action=action, ).to_dict() for action in WORKFLOW_ACTIONS } return { "scope_type": definition.scope_type, "scope_id": definition.scope_id, "definition_kind": definition.definition_kind, "inherit_to_lower_scopes": definition.inherit_to_lower_scopes, "allow_start": definition.allow_start, "allow_reuse": definition.allow_reuse, "allow_automation": definition.allow_automation, "derived_from_definition_id": ( definition.derived_from_definition_id ), "derived_from_revision": definition.derived_from_revision, "derived_from_hash": definition.derived_from_hash, "derivation_provenance": dict(definition.derivation_provenance), "actions": actions, "automation_runtime_available": runtime_available, "automation_runtime_reason": ( None if runtime_available else "Automatic reconciliation requires the Workflow runtime worker." ), } def require_definition_action( definition: WorkflowDefinition, *, principal: ApiPrincipal, registry: object | None, action: WorkflowAction, ) -> PolicyDecision: decision = definition_decision( definition, principal=principal, registry=registry, action=action, ) if not decision.allowed: raise PermissionError( decision.reason or f"Definition action is not allowed: {action}" ) return decision def _target_scope( definition: WorkflowDefinition, principal: ApiPrincipal, ) -> DefinitionScopeRef: if ( definition.scope_type == "group" and definition.scope_id in principal.group_ids ): return DefinitionScopeRef("group", definition.scope_id) if definition.scope_type == "user" and definition.scope_id in { principal.membership_id, principal.account_id, }: return DefinitionScopeRef("user", definition.scope_id) return DefinitionScopeRef("tenant", principal.tenant_id) def _ancestor_context( definition: WorkflowDefinition, ) -> dict[str, object]: provenance = definition.derivation_provenance limits = provenance.get("source_effective_limits") source = provenance.get("source_scope") context: dict[str, object] = {} if isinstance(limits, Mapping): context["ancestor_limits"] = { "inherit_to_lower_scopes": bool( limits.get("inherit_to_lower_scopes", True) ), "allow_run": bool(limits.get("allow_start")), "allow_reuse": bool(limits.get("allow_reuse")), "allow_automation": bool(limits.get("allow_automation")), } if isinstance(source, Mapping): context["ancestor_source"] = dict(source) return context def _tenant_local_fallback( request: DefinitionGovernanceRequest, *, displayed_action: WorkflowAction, ) -> PolicyDecision: ancestor = request.context.get("ancestor_limits") ancestor_limits = ancestor if isinstance(ancestor, Mapping) else {} effective_limits = { "inherit_to_lower_scopes": ( request.inherit_to_lower_scopes and _fallback_ancestor_flag( ancestor_limits, "inherit_to_lower_scopes", ) ), "allow_run": request.allow_run and _fallback_ancestor_flag(ancestor_limits, "allow_run"), "allow_reuse": request.allow_reuse and _fallback_ancestor_flag(ancestor_limits, "allow_reuse"), "allow_automation": request.allow_automation and _fallback_ancestor_flag( ancestor_limits, "allow_automation", ), } local = ( request.definition_scope.scope_type == "tenant" and request.definition_scope.scope_id == request.tenant_id and request.actor.tenant_id == request.tenant_id ) allowed = False reason: str | None = None if not local: reason = ( "Inherited definitions require the Policy module; only local " "tenant definitions are available." ) elif request.action in {"view", "edit"}: allowed = True elif request.action == "run": allowed = ( request.definition_kind == "flow" and request.status == "active" and effective_limits["allow_run"] ) reason = ( None if allowed else "Only active local flows with starting enabled can start." ) else: reason = "Definition reuse and automation require the Policy module." return PolicyDecision( allowed=allowed, reason=reason, source_path=( PolicySourceStep( scope_type=request.definition_scope.scope_type, scope_id=request.definition_scope.scope_id, label="Tenant-local conservative fallback", applied_fields=( "definition_kind", "status", "allow_run", ), policy={ "policy_module_available": False, "definition_kind": request.definition_kind, "status": request.status, "allow_start": request.allow_run, }, ), ), requirements=( () if allowed else (f"workflow.definition.{displayed_action}",) ), details={ "fallback": "tenant_local", "action": displayed_action, "effective_limits": effective_limits, }, ) def _fallback_ancestor_flag( limits: Mapping[str, object], key: str, ) -> bool: value = limits.get(key) return True if value is None else value is True __all__ = [ "WORKFLOW_ACTIONS", "definition_decision", "definition_governance_payload", "normalize_definition_scope", "require_definition_action", ]