257 lines
8.0 KiB
Python
257 lines
8.0 KiB
Python
from __future__ import annotations
|
|
|
|
import unittest
|
|
|
|
from sqlalchemy import create_engine
|
|
from sqlalchemy.orm import Session, sessionmaker
|
|
|
|
from govoplan_core.auth import ApiPrincipal
|
|
from govoplan_core.core.access import PrincipalRef
|
|
from govoplan_core.core.policy import PolicyDecision
|
|
from govoplan_core.db.base import Base
|
|
from govoplan_workflow.backend.db.models import (
|
|
WorkflowDefinition,
|
|
WorkflowDefinitionRevision,
|
|
)
|
|
from govoplan_workflow.backend.schemas import (
|
|
WorkflowDefinitionCreateRequest,
|
|
WorkflowDefinitionDeriveRequest,
|
|
WorkflowDefinitionUpdateRequest,
|
|
)
|
|
from govoplan_workflow.backend.service import (
|
|
WorkflowConflictError,
|
|
activate_definition,
|
|
create_definition,
|
|
definition_response,
|
|
derive_definition,
|
|
update_definition,
|
|
)
|
|
from test_service import sample_graph
|
|
|
|
|
|
POLICY_CAPABILITY = "policy.definitionGovernance"
|
|
|
|
|
|
def principal() -> ApiPrincipal:
|
|
return ApiPrincipal(
|
|
principal=PrincipalRef(
|
|
account_id="account-1",
|
|
membership_id="membership-1",
|
|
tenant_id="tenant-1",
|
|
scopes=frozenset(
|
|
{
|
|
"workflow:definition:read",
|
|
"workflow:definition:write",
|
|
"workflow:instance:start",
|
|
}
|
|
),
|
|
),
|
|
account=object(),
|
|
user=object(),
|
|
)
|
|
|
|
|
|
class DefinitionPolicy:
|
|
def resolve_definition_action(self, *, request):
|
|
local = request.definition_scope.scope_type == "tenant"
|
|
inherited = (
|
|
request.definition_scope.scope_type == "system"
|
|
and request.inherit_to_lower_scopes
|
|
)
|
|
allowed = local or inherited
|
|
if request.action == "edit":
|
|
allowed = local
|
|
elif request.action in {"reuse", "derive"}:
|
|
allowed = allowed and request.allow_reuse
|
|
elif request.action == "run":
|
|
allowed = (
|
|
allowed
|
|
and request.definition_kind == "flow"
|
|
and request.status == "active"
|
|
and request.allow_run
|
|
)
|
|
elif request.action == "automate":
|
|
allowed = (
|
|
allowed
|
|
and request.definition_kind == "flow"
|
|
and request.allow_automation
|
|
)
|
|
return PolicyDecision(
|
|
allowed=allowed,
|
|
reason=None if allowed else "Definition action denied.",
|
|
)
|
|
|
|
|
|
class Registry:
|
|
def has_capability(self, name: str) -> bool:
|
|
return name == POLICY_CAPABILITY
|
|
|
|
def capability(self, name: str):
|
|
if not self.has_capability(name):
|
|
raise KeyError(name)
|
|
return DefinitionPolicy()
|
|
|
|
|
|
class WorkflowGovernanceTests(unittest.TestCase):
|
|
def setUp(self) -> None:
|
|
self.engine = create_engine("sqlite:///:memory:")
|
|
Base.metadata.create_all(
|
|
self.engine,
|
|
tables=[
|
|
WorkflowDefinition.__table__,
|
|
WorkflowDefinitionRevision.__table__,
|
|
],
|
|
)
|
|
self.Session = sessionmaker(bind=self.engine)
|
|
self.session: Session = self.Session()
|
|
self.principal = principal()
|
|
self.registry = Registry()
|
|
|
|
def tearDown(self) -> None:
|
|
self.session.close()
|
|
Base.metadata.drop_all(
|
|
self.engine,
|
|
tables=[
|
|
WorkflowDefinitionRevision.__table__,
|
|
WorkflowDefinition.__table__,
|
|
],
|
|
)
|
|
self.engine.dispose()
|
|
|
|
def test_derivation_pins_template_revision_and_provenance(self) -> None:
|
|
template = create_definition(
|
|
self.session,
|
|
tenant_id="tenant-1",
|
|
actor_id="system-admin",
|
|
payload=WorkflowDefinitionCreateRequest(
|
|
key="permit-review",
|
|
name="Permit review template",
|
|
graph=sample_graph(),
|
|
scope_type="system",
|
|
definition_kind="template",
|
|
inherit_to_lower_scopes=True,
|
|
allow_reuse=True,
|
|
),
|
|
)
|
|
derived = derive_definition(
|
|
self.session,
|
|
tenant_id="tenant-1",
|
|
actor_id="account-1",
|
|
principal=self.principal,
|
|
registry=self.registry,
|
|
source_definition_id=template.id,
|
|
payload=WorkflowDefinitionDeriveRequest(
|
|
name="Tenant permit review",
|
|
allow_start=True,
|
|
),
|
|
)
|
|
self.session.commit()
|
|
|
|
response = definition_response(
|
|
self.session,
|
|
derived,
|
|
principal=self.principal,
|
|
registry=self.registry,
|
|
)
|
|
self.assertEqual(template.id, derived.derived_from_definition_id)
|
|
self.assertEqual(1, derived.derived_from_revision)
|
|
self.assertEqual(
|
|
template.revisions[0].content_hash,
|
|
derived.derived_from_hash,
|
|
)
|
|
self.assertEqual(
|
|
"system",
|
|
response.governance.derivation_provenance["source_scope"][
|
|
"scope_type"
|
|
],
|
|
)
|
|
self.assertFalse(response.governance.automation_runtime_available)
|
|
|
|
def test_template_cannot_be_activated(self) -> None:
|
|
template = create_definition(
|
|
self.session,
|
|
tenant_id="tenant-1",
|
|
actor_id="account-1",
|
|
payload=WorkflowDefinitionCreateRequest(
|
|
name="Reusable review",
|
|
graph=sample_graph(),
|
|
definition_kind="template",
|
|
allow_reuse=True,
|
|
),
|
|
)
|
|
|
|
with self.assertRaises(WorkflowConflictError):
|
|
activate_definition(
|
|
self.session,
|
|
tenant_id="tenant-1",
|
|
definition_id=template.id,
|
|
actor_id="account-1",
|
|
)
|
|
|
|
def test_derived_limits_cannot_be_broadened_transitively(self) -> None:
|
|
template = create_definition(
|
|
self.session,
|
|
tenant_id="tenant-1",
|
|
actor_id="account-1",
|
|
payload=WorkflowDefinitionCreateRequest(
|
|
name="Restricted review",
|
|
graph=sample_graph(),
|
|
definition_kind="template",
|
|
allow_reuse=True,
|
|
allow_automation=False,
|
|
inherit_to_lower_scopes=False,
|
|
),
|
|
)
|
|
derived = derive_definition(
|
|
self.session,
|
|
tenant_id="tenant-1",
|
|
actor_id="account-1",
|
|
principal=self.principal,
|
|
registry=self.registry,
|
|
source_definition_id=template.id,
|
|
payload=WorkflowDefinitionDeriveRequest(
|
|
name="Tenant review",
|
|
allow_reuse=True,
|
|
allow_automation=True,
|
|
inherit_to_lower_scopes=True,
|
|
),
|
|
)
|
|
update_definition(
|
|
self.session,
|
|
tenant_id="tenant-1",
|
|
definition_id=derived.id,
|
|
actor_id="account-1",
|
|
payload=WorkflowDefinitionUpdateRequest(
|
|
name=derived.name,
|
|
graph=sample_graph(),
|
|
expected_revision=1,
|
|
allow_reuse=True,
|
|
allow_automation=True,
|
|
inherit_to_lower_scopes=True,
|
|
),
|
|
)
|
|
grandchild = derive_definition(
|
|
self.session,
|
|
tenant_id="tenant-1",
|
|
actor_id="account-1",
|
|
principal=self.principal,
|
|
registry=self.registry,
|
|
source_definition_id=derived.id,
|
|
payload=WorkflowDefinitionDeriveRequest(
|
|
name="User review",
|
|
scope_type="user",
|
|
scope_id="membership-1",
|
|
allow_automation=True,
|
|
inherit_to_lower_scopes=True,
|
|
),
|
|
)
|
|
|
|
self.assertFalse(derived.allow_automation)
|
|
self.assertFalse(derived.inherit_to_lower_scopes)
|
|
self.assertFalse(grandchild.allow_automation)
|
|
self.assertFalse(grandchild.inherit_to_lower_scopes)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|