feat(policy): add delegation and escalation contracts
Module Package Release / publish-packages (push) Failing after 6s
Module Package Release / publish-packages (push) Failing after 6s
This commit is contained in:
@@ -14,6 +14,7 @@ consistent while each module still owns its domain rules.
|
|||||||
| Governance defaults | `govoplan-admin` plus `govoplan-access` materializer | admin settings, governance template routes, access materialization capability | System governance can block tenant-local groups, roles, and API keys. |
|
| Governance defaults | `govoplan-admin` plus `govoplan-access` materializer | admin settings, governance template routes, access materialization capability | System governance can block tenant-local groups, roles, and API keys. |
|
||||||
| Delegation and ownership policy | access/campaign/mail/files modules | capability checks and owner-scoped APIs | Source provenance should use this contract when policies become externally explainable. |
|
| Delegation and ownership policy | access/campaign/mail/files modules | capability checks and owner-scoped APIs | Source provenance should use this contract when policies become externally explainable. |
|
||||||
| Definition governance | `govoplan-policy` | capability `policy.definitionGovernance` | Resolves view, edit, run/start, reuse, derive, and automate for system, tenant, group, and user Dataflow/Workflow definitions. |
|
| Definition governance | `govoplan-policy` | capability `policy.definitionGovernance` | Resolves view, edit, run/start, reuse, derive, and automate for system, tenant, group, and user Dataflow/Workflow definitions. |
|
||||||
|
| Function assignment governance | `govoplan-policy` | capability `policy.functionAssignmentGovernance` | Returns current review steps, delegation depth/validity ceilings, and explicit timed-escalation targets consumed by IDM. |
|
||||||
|
|
||||||
## Policy Decision
|
## Policy Decision
|
||||||
|
|
||||||
@@ -126,6 +127,22 @@ When the capability is absent, modules must not silently emulate cross-scope
|
|||||||
inheritance. Their conservative fallback is limited to local tenant
|
inheritance. Their conservative fallback is limited to local tenant
|
||||||
definitions and disables reuse, derivation, and automation.
|
definitions and disables reuse, derivation, and automation.
|
||||||
|
|
||||||
|
## Function Assignment Delegation And Escalation
|
||||||
|
|
||||||
|
`FunctionAssignmentGovernanceDecision` is the versioned cross-module contract
|
||||||
|
for request/grant review. In addition to the required holder, authority, and
|
||||||
|
recipient steps, it returns `delegation_allowed`,
|
||||||
|
`maximum_delegation_depth`, `maximum_delegated_validity_days`, and typed
|
||||||
|
`FunctionAssignmentEscalationRule` entries. Each escalation entry binds one
|
||||||
|
review step to an exact target function and timeout.
|
||||||
|
|
||||||
|
The decision is a current ceiling, not durable authorization. IDM must recheck
|
||||||
|
the complete assignment-source chain and all recorded decisions before final
|
||||||
|
application. An elapsed timeout creates explicit state and evidence; it must
|
||||||
|
never be interpreted as approval or as permission to silently substitute an
|
||||||
|
approver. Missing providers, malformed rules, invalid chains, or tightened
|
||||||
|
limits fail closed with an explainable reason.
|
||||||
|
|
||||||
## Bounded Impact-Subject Providers
|
## Bounded Impact-Subject Providers
|
||||||
|
|
||||||
Policy impact previews discover optional subject providers through capability
|
Policy impact previews discover optional subject providers through capability
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "govoplan-core"
|
name = "govoplan-core"
|
||||||
version = "0.1.28"
|
version = "0.1.29"
|
||||||
description = "Reusable GovOPlaN platform core, access, tenancy, and RBAC components."
|
description = "Reusable GovOPlaN platform core, access, tenancy, and RBAC components."
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
|
|||||||
@@ -41,10 +41,12 @@ ViewGovernanceAction = Literal[
|
|||||||
"workflow_activate",
|
"workflow_activate",
|
||||||
]
|
]
|
||||||
FunctionAssignmentChangeKind = Literal["request", "grant"]
|
FunctionAssignmentChangeKind = Literal["request", "grant"]
|
||||||
|
FunctionAssignmentReviewStep = Literal["holder", "authority", "recipient"]
|
||||||
FunctionAssignmentGovernanceAction = Literal[
|
FunctionAssignmentGovernanceAction = Literal[
|
||||||
"submit",
|
"submit",
|
||||||
"approve_holder",
|
"approve_holder",
|
||||||
"approve_authority",
|
"approve_authority",
|
||||||
|
"approve_escalation",
|
||||||
"accept_recipient",
|
"accept_recipient",
|
||||||
"request_changes",
|
"request_changes",
|
||||||
"respond",
|
"respond",
|
||||||
@@ -419,6 +421,20 @@ class FunctionAssignmentGovernanceRequest:
|
|||||||
context: Mapping[str, Any] = field(default_factory=dict)
|
context: Mapping[str, Any] = field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class FunctionAssignmentEscalationRule:
|
||||||
|
step: FunctionAssignmentReviewStep
|
||||||
|
target_function_id: str
|
||||||
|
timeout_hours: int
|
||||||
|
|
||||||
|
def to_dict(self) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"step": self.step,
|
||||||
|
"target_function_id": self.target_function_id,
|
||||||
|
"timeout_hours": self.timeout_hours,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, slots=True)
|
@dataclass(frozen=True, slots=True)
|
||||||
class FunctionAssignmentGovernanceDecision:
|
class FunctionAssignmentGovernanceDecision:
|
||||||
allowed: bool
|
allowed: bool
|
||||||
@@ -431,6 +447,10 @@ class FunctionAssignmentGovernanceDecision:
|
|||||||
separation_of_duties: bool = True
|
separation_of_duties: bool = True
|
||||||
quorum: int = 1
|
quorum: int = 1
|
||||||
maximum_validity_days: int | None = None
|
maximum_validity_days: int | None = None
|
||||||
|
delegation_allowed: bool = False
|
||||||
|
maximum_delegation_depth: int = 0
|
||||||
|
maximum_delegated_validity_days: int | None = None
|
||||||
|
escalation_rules: tuple[FunctionAssignmentEscalationRule, ...] = ()
|
||||||
request_expiry_hours: int = 336
|
request_expiry_hours: int = 336
|
||||||
source_path: tuple[PolicySourceStep, ...] = ()
|
source_path: tuple[PolicySourceStep, ...] = ()
|
||||||
requirements: tuple[str, ...] = ()
|
requirements: tuple[str, ...] = ()
|
||||||
@@ -448,12 +468,24 @@ class FunctionAssignmentGovernanceDecision:
|
|||||||
"separation_of_duties": self.separation_of_duties,
|
"separation_of_duties": self.separation_of_duties,
|
||||||
"quorum": self.quorum,
|
"quorum": self.quorum,
|
||||||
"maximum_validity_days": self.maximum_validity_days,
|
"maximum_validity_days": self.maximum_validity_days,
|
||||||
|
"delegation_allowed": self.delegation_allowed,
|
||||||
|
"maximum_delegation_depth": self.maximum_delegation_depth,
|
||||||
|
"maximum_delegated_validity_days": (
|
||||||
|
self.maximum_delegated_validity_days
|
||||||
|
),
|
||||||
|
"escalation_rules": [rule.to_dict() for rule in self.escalation_rules],
|
||||||
"request_expiry_hours": self.request_expiry_hours,
|
"request_expiry_hours": self.request_expiry_hours,
|
||||||
"source_path": [step.to_dict() for step in self.source_path],
|
"source_path": [step.to_dict() for step in self.source_path],
|
||||||
"requirements": list(self.requirements),
|
"requirements": list(self.requirements),
|
||||||
"details": dict(self.details),
|
"details": dict(self.details),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def escalation_rule(
|
||||||
|
self,
|
||||||
|
step: FunctionAssignmentReviewStep,
|
||||||
|
) -> FunctionAssignmentEscalationRule | None:
|
||||||
|
return next((rule for rule in self.escalation_rules if rule.step == step), None)
|
||||||
|
|
||||||
|
|
||||||
@runtime_checkable
|
@runtime_checkable
|
||||||
class FunctionAssignmentGovernancePolicy(Protocol):
|
class FunctionAssignmentGovernancePolicy(Protocol):
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ from govoplan_core.core.configuration_safety import (
|
|||||||
ui_managed_configuration_fields_requiring_approval,
|
ui_managed_configuration_fields_requiring_approval,
|
||||||
)
|
)
|
||||||
from govoplan_core.core.policy import (
|
from govoplan_core.core.policy import (
|
||||||
|
FunctionAssignmentEscalationRule,
|
||||||
|
FunctionAssignmentGovernanceDecision,
|
||||||
PolicyDecision,
|
PolicyDecision,
|
||||||
PolicySourceStep,
|
PolicySourceStep,
|
||||||
parse_policy_source_path,
|
parse_policy_source_path,
|
||||||
@@ -19,6 +21,34 @@ from govoplan_core.core.policy import (
|
|||||||
|
|
||||||
|
|
||||||
class PolicyContractTests(unittest.TestCase):
|
class PolicyContractTests(unittest.TestCase):
|
||||||
|
def test_function_assignment_policy_serializes_delegation_and_escalation(self) -> None:
|
||||||
|
decision = FunctionAssignmentGovernanceDecision(
|
||||||
|
allowed=True,
|
||||||
|
delegation_allowed=True,
|
||||||
|
maximum_delegation_depth=2,
|
||||||
|
maximum_delegated_validity_days=30,
|
||||||
|
escalation_rules=(
|
||||||
|
FunctionAssignmentEscalationRule(
|
||||||
|
step="authority",
|
||||||
|
target_function_id="function-escalation",
|
||||||
|
timeout_hours=48,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
payload = decision.to_dict()
|
||||||
|
|
||||||
|
self.assertEqual(2, payload["maximum_delegation_depth"])
|
||||||
|
self.assertEqual(30, payload["maximum_delegated_validity_days"])
|
||||||
|
self.assertEqual(
|
||||||
|
"function-escalation",
|
||||||
|
payload["escalation_rules"][0]["target_function_id"],
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
"function-escalation",
|
||||||
|
decision.escalation_rule("authority").target_function_id,
|
||||||
|
)
|
||||||
|
|
||||||
def test_policy_source_paths_are_stable_and_round_trip(self) -> None:
|
def test_policy_source_paths_are_stable_and_round_trip(self) -> None:
|
||||||
self.assertEqual(policy_source_path("system"), "system")
|
self.assertEqual(policy_source_path("system"), "system")
|
||||||
self.assertEqual(policy_source_path("tenant", "tenant-1"), "tenant:tenant-1")
|
self.assertEqual(policy_source_path("tenant", "tenant-1"), "tenant:tenant-1")
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@govoplan/core-webui",
|
"name": "@govoplan/core-webui",
|
||||||
"version": "0.1.28",
|
"version": "0.1.29",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@govoplan/core-webui",
|
"name": "@govoplan/core-webui",
|
||||||
"version": "0.1.28",
|
"version": "0.1.29",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@govoplan/access-webui": "file:../../govoplan-access/webui",
|
"@govoplan/access-webui": "file:../../govoplan-access/webui",
|
||||||
"@govoplan/addresses-webui": "file:../../govoplan-addresses/webui",
|
"@govoplan/addresses-webui": "file:../../govoplan-addresses/webui",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@govoplan/core-webui",
|
"name": "@govoplan/core-webui",
|
||||||
"version": "0.1.28",
|
"version": "0.1.29",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
|
|||||||
Reference in New Issue
Block a user