feat(idm): govern delegation chains and timed escalation
Module Package Release / publish-packages (push) Successful in 11s
Module Package Release / publish-packages (push) Successful in 11s
This commit is contained in:
@@ -244,6 +244,10 @@ def _change_item(
|
||||
workflow_current_step_id=change.workflow_current_step_id,
|
||||
resulting_assignment_id=change.resulting_assignment_id,
|
||||
expires_at=change.expires_at,
|
||||
review_deadline_at=change.review_deadline_at,
|
||||
escalated_at=change.escalated_at,
|
||||
escalation_from_state=change.escalation_from_state,
|
||||
escalation_target_function_id=change.escalation_target_function_id,
|
||||
outcome_reason=change.outcome_reason,
|
||||
resource_revision=change.resource_revision,
|
||||
etag=change.strong_etag,
|
||||
@@ -297,6 +301,18 @@ def _record_change_audit(
|
||||
"workflow_instance_id": change.workflow_instance_id,
|
||||
"evidence": list(change.evidence),
|
||||
"resulting_assignment_id": change.resulting_assignment_id,
|
||||
"review_deadline_at": (
|
||||
change.review_deadline_at.isoformat()
|
||||
if change.review_deadline_at
|
||||
else None
|
||||
),
|
||||
"escalated_at": (
|
||||
change.escalated_at.isoformat() if change.escalated_at else None
|
||||
),
|
||||
"escalation_from_state": change.escalation_from_state,
|
||||
"escalation_target_function_id": (
|
||||
change.escalation_target_function_id
|
||||
),
|
||||
"resource_revision": change.resource_revision,
|
||||
},
|
||||
commit=False,
|
||||
@@ -370,6 +386,18 @@ def get_function_assignment_capability(
|
||||
decision and decision.recipient_acceptance_required
|
||||
),
|
||||
maximum_validity_days=(decision.maximum_validity_days if decision else None),
|
||||
delegation_allowed=bool(decision and decision.delegation_allowed),
|
||||
maximum_delegation_depth=(
|
||||
decision.maximum_delegation_depth if decision else 0
|
||||
),
|
||||
maximum_delegated_validity_days=(
|
||||
decision.maximum_delegated_validity_days if decision else None
|
||||
),
|
||||
escalation_rules=(
|
||||
[rule.to_dict() for rule in decision.escalation_rules]
|
||||
if decision
|
||||
else []
|
||||
),
|
||||
workflow_available=registry.has_capability(CAPABILITY_WORKFLOW_ORCHESTRATION),
|
||||
policy_available=registry.has_capability(
|
||||
CAPABILITY_POLICY_FUNCTION_ASSIGNMENT_GOVERNANCE
|
||||
|
||||
@@ -516,6 +516,50 @@ def get_idm_settings(
|
||||
return _settings_item(item) if item is not None else _default_settings(tenant_id)
|
||||
|
||||
|
||||
def _validate_function_governance_defaults(settings: dict[str, Any]) -> None:
|
||||
raw = settings.get("function_assignment_governance_defaults")
|
||||
if raw is None:
|
||||
return
|
||||
if not isinstance(raw, dict):
|
||||
raise _invalid("Function assignment governance defaults must be an object.")
|
||||
if "delegation_allowed" in raw and not isinstance(
|
||||
raw.get("delegation_allowed"), bool
|
||||
):
|
||||
raise _invalid("delegation_allowed must be true or false.")
|
||||
for key, minimum, maximum in (
|
||||
("maximum_delegation_depth", 1, 20),
|
||||
("maximum_delegated_validity_days", 1, 3650),
|
||||
):
|
||||
value = raw.get(key)
|
||||
if value is None:
|
||||
continue
|
||||
if not isinstance(value, int) or isinstance(value, bool) or not minimum <= value <= maximum:
|
||||
raise _invalid(f"{key} must be between {minimum} and {maximum}.")
|
||||
escalation = raw.get("escalation")
|
||||
if escalation is None:
|
||||
return
|
||||
if not isinstance(escalation, dict):
|
||||
raise _invalid("Escalation defaults must be an object.")
|
||||
unsupported = set(escalation) - {"holder", "authority", "recipient"}
|
||||
if unsupported:
|
||||
raise _invalid(
|
||||
"Unsupported escalation review step: " + ", ".join(sorted(unsupported))
|
||||
)
|
||||
for step, value in escalation.items():
|
||||
if not isinstance(value, dict):
|
||||
raise _invalid(f"The {step} escalation rule must be an object.")
|
||||
target = value.get("target_function_id")
|
||||
timeout = value.get("timeout_hours")
|
||||
if not isinstance(target, str) or not target.strip():
|
||||
raise _invalid(f"The {step} escalation target function is required.")
|
||||
if (
|
||||
not isinstance(timeout, int)
|
||||
or isinstance(timeout, bool)
|
||||
or not 1 <= timeout <= 8760
|
||||
):
|
||||
raise _invalid(f"The {step} escalation timeout must be 1 to 8760 hours.")
|
||||
|
||||
|
||||
@router.patch("/settings", response_model=IdmSettingsItem)
|
||||
def update_idm_settings(
|
||||
payload: IdmSettingsUpdateRequest,
|
||||
@@ -544,6 +588,16 @@ def update_idm_settings(
|
||||
if "settings" in fields:
|
||||
if payload.settings is None:
|
||||
raise _invalid("Settings cannot be empty.")
|
||||
_validate_function_governance_defaults(payload.settings)
|
||||
defaults = payload.settings.get("function_assignment_governance_defaults")
|
||||
escalation = defaults.get("escalation") if isinstance(defaults, dict) else None
|
||||
if isinstance(escalation, dict):
|
||||
for rule in escalation.values():
|
||||
if isinstance(rule, dict):
|
||||
_organization_function(
|
||||
str(rule.get("target_function_id")),
|
||||
tenant_id,
|
||||
)
|
||||
item.settings = payload.settings
|
||||
session.flush()
|
||||
result = _settings_item(item)
|
||||
|
||||
@@ -324,6 +324,10 @@ class FunctionAssignmentChangeItem(BaseModel):
|
||||
workflow_current_step_id: str | None = None
|
||||
resulting_assignment_id: str | None = None
|
||||
expires_at: datetime | None = None
|
||||
review_deadline_at: datetime | None = None
|
||||
escalated_at: datetime | None = None
|
||||
escalation_from_state: str | None = None
|
||||
escalation_target_function_id: str | None = None
|
||||
outcome_reason: str | None = None
|
||||
resource_revision: int
|
||||
etag: str
|
||||
@@ -355,5 +359,9 @@ class FunctionAssignmentCapabilityItem(BaseModel):
|
||||
evidence_required: bool = False
|
||||
recipient_acceptance_required: bool = False
|
||||
maximum_validity_days: int | None = None
|
||||
delegation_allowed: bool = False
|
||||
maximum_delegation_depth: int = 0
|
||||
maximum_delegated_validity_days: int | None = None
|
||||
escalation_rules: list[dict[str, Any]] = Field(default_factory=list)
|
||||
workflow_available: bool = False
|
||||
policy_available: bool = False
|
||||
|
||||
Reference in New Issue
Block a user