Add governed reusable workflow definitions
This commit is contained in:
@@ -6,6 +6,11 @@ from sqlalchemy.orm import Session
|
||||
from govoplan_core.audit.logging import audit_event
|
||||
from govoplan_core.auth import ApiPrincipal, get_api_principal, has_scope
|
||||
from govoplan_core.db.session import get_session
|
||||
from govoplan_workflow.backend.governance import (
|
||||
definition_decision,
|
||||
normalize_definition_scope,
|
||||
require_definition_action,
|
||||
)
|
||||
from govoplan_workflow.backend.manifest import (
|
||||
ADMIN_SCOPE,
|
||||
DEFINITION_READ_SCOPE,
|
||||
@@ -17,6 +22,7 @@ from govoplan_workflow.backend.schemas import (
|
||||
WorkflowDefinitionActivateRequest,
|
||||
WorkflowDefinitionCreateRequest,
|
||||
WorkflowDefinitionDeleteResponse,
|
||||
WorkflowDefinitionDeriveRequest,
|
||||
WorkflowDefinitionListResponse,
|
||||
WorkflowDefinitionResponse,
|
||||
WorkflowDefinitionRevisionListResponse,
|
||||
@@ -29,6 +35,7 @@ from govoplan_workflow.backend.schemas import (
|
||||
WorkflowNodeTypeResponse,
|
||||
WorkflowPortResponse,
|
||||
)
|
||||
from govoplan_workflow.backend.runtime import get_registry
|
||||
from govoplan_workflow.backend.service import (
|
||||
WorkflowConflictError,
|
||||
WorkflowError,
|
||||
@@ -39,6 +46,7 @@ from govoplan_workflow.backend.service import (
|
||||
create_definition,
|
||||
definition_response,
|
||||
delete_definition,
|
||||
derive_definition,
|
||||
get_definition,
|
||||
get_definition_revision,
|
||||
list_definition_revisions,
|
||||
@@ -89,6 +97,35 @@ def _http_error(exc: WorkflowError) -> HTTPException:
|
||||
)
|
||||
|
||||
|
||||
def _governance_http_error(
|
||||
exc: PermissionError | ValueError,
|
||||
) -> HTTPException:
|
||||
return HTTPException(
|
||||
status_code=(
|
||||
status.HTTP_403_FORBIDDEN
|
||||
if isinstance(exc, PermissionError)
|
||||
else status.HTTP_422_UNPROCESSABLE_CONTENT
|
||||
),
|
||||
detail=str(exc),
|
||||
)
|
||||
|
||||
|
||||
def _definition_response(
|
||||
session: Session,
|
||||
definition,
|
||||
principal: ApiPrincipal,
|
||||
*,
|
||||
revision: int | None = None,
|
||||
) -> WorkflowDefinitionResponse:
|
||||
return definition_response(
|
||||
session,
|
||||
definition,
|
||||
principal=principal,
|
||||
registry=get_registry(),
|
||||
revision=revision,
|
||||
)
|
||||
|
||||
|
||||
def _actor_id(principal: ApiPrincipal) -> str | None:
|
||||
return principal.account_id or principal.membership_id or principal.identity_id
|
||||
|
||||
@@ -209,13 +246,29 @@ def api_list_definitions(
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> WorkflowDefinitionListResponse:
|
||||
_require_any_scope(principal, DEFINITION_READ_SCOPE, ADMIN_SCOPE)
|
||||
registry = get_registry()
|
||||
definitions = [
|
||||
definition
|
||||
for definition in list_definitions(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
)
|
||||
if definition_decision(
|
||||
definition,
|
||||
principal=principal,
|
||||
registry=registry,
|
||||
action="view",
|
||||
).allowed
|
||||
]
|
||||
return WorkflowDefinitionListResponse(
|
||||
definitions=[
|
||||
definition_response(session, definition)
|
||||
for definition in list_definitions(
|
||||
definition_response(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
definition,
|
||||
principal=principal,
|
||||
registry=registry,
|
||||
)
|
||||
for definition in definitions
|
||||
]
|
||||
)
|
||||
|
||||
@@ -232,12 +285,25 @@ def api_create_definition(
|
||||
) -> WorkflowDefinitionResponse:
|
||||
_require_any_scope(principal, DEFINITION_WRITE_SCOPE, ADMIN_SCOPE)
|
||||
try:
|
||||
tenant_id, scope_type, scope_id, _scope_key = (
|
||||
normalize_definition_scope(
|
||||
principal,
|
||||
scope_type=payload.scope_type,
|
||||
scope_id=payload.scope_id,
|
||||
administrative=has_scope(principal, ADMIN_SCOPE),
|
||||
)
|
||||
)
|
||||
payload = payload.model_copy(
|
||||
update={"scope_type": scope_type, "scope_id": scope_id}
|
||||
)
|
||||
definition = create_definition(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
tenant_id=tenant_id or principal.tenant_id,
|
||||
actor_id=_actor_id(principal),
|
||||
payload=payload,
|
||||
)
|
||||
except (PermissionError, ValueError) as exc:
|
||||
raise _governance_http_error(exc) from exc
|
||||
except WorkflowError as exc:
|
||||
raise _http_error(exc) from exc
|
||||
_audit(
|
||||
@@ -248,9 +314,12 @@ def api_create_definition(
|
||||
details={
|
||||
"key": definition.definition_key,
|
||||
"revision": definition.current_revision,
|
||||
"scope_type": definition.scope_type,
|
||||
"scope_id": definition.scope_id,
|
||||
"definition_kind": definition.definition_kind,
|
||||
},
|
||||
)
|
||||
response = definition_response(session, definition)
|
||||
response = _definition_response(session, definition, principal)
|
||||
session.commit()
|
||||
return response
|
||||
|
||||
@@ -272,7 +341,20 @@ def api_get_definition(
|
||||
tenant_id=principal.tenant_id,
|
||||
definition_id=definition_id,
|
||||
)
|
||||
return definition_response(session, definition, revision=revision)
|
||||
require_definition_action(
|
||||
definition,
|
||||
principal=principal,
|
||||
registry=get_registry(),
|
||||
action="view",
|
||||
)
|
||||
return _definition_response(
|
||||
session,
|
||||
definition,
|
||||
principal,
|
||||
revision=revision,
|
||||
)
|
||||
except PermissionError as exc:
|
||||
raise _governance_http_error(exc) from exc
|
||||
except WorkflowError as exc:
|
||||
raise _http_error(exc) from exc
|
||||
|
||||
@@ -289,6 +371,17 @@ def api_update_definition(
|
||||
) -> WorkflowDefinitionResponse:
|
||||
_require_any_scope(principal, DEFINITION_WRITE_SCOPE, ADMIN_SCOPE)
|
||||
try:
|
||||
existing = get_definition(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
definition_id=definition_id,
|
||||
)
|
||||
require_definition_action(
|
||||
existing,
|
||||
principal=principal,
|
||||
registry=get_registry(),
|
||||
action="edit",
|
||||
)
|
||||
definition = update_definition(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
@@ -296,6 +389,8 @@ def api_update_definition(
|
||||
actor_id=_actor_id(principal),
|
||||
payload=payload,
|
||||
)
|
||||
except PermissionError as exc:
|
||||
raise _governance_http_error(exc) from exc
|
||||
except WorkflowError as exc:
|
||||
raise _http_error(exc) from exc
|
||||
_audit(
|
||||
@@ -308,7 +403,64 @@ def api_update_definition(
|
||||
"status": definition.status,
|
||||
},
|
||||
)
|
||||
response = definition_response(session, definition)
|
||||
response = _definition_response(session, definition, principal)
|
||||
session.commit()
|
||||
return response
|
||||
|
||||
|
||||
@router.post(
|
||||
"/definitions/{definition_id}/derive",
|
||||
response_model=WorkflowDefinitionResponse,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
)
|
||||
def api_derive_definition(
|
||||
definition_id: str,
|
||||
payload: WorkflowDefinitionDeriveRequest,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> WorkflowDefinitionResponse:
|
||||
_require_any_scope(principal, DEFINITION_WRITE_SCOPE, ADMIN_SCOPE)
|
||||
try:
|
||||
tenant_id, scope_type, scope_id, _scope_key = (
|
||||
normalize_definition_scope(
|
||||
principal,
|
||||
scope_type=payload.scope_type,
|
||||
scope_id=payload.scope_id,
|
||||
administrative=has_scope(principal, ADMIN_SCOPE),
|
||||
)
|
||||
)
|
||||
payload = payload.model_copy(
|
||||
update={"scope_type": scope_type, "scope_id": scope_id}
|
||||
)
|
||||
definition = derive_definition(
|
||||
session,
|
||||
tenant_id=tenant_id or principal.tenant_id,
|
||||
actor_id=_actor_id(principal),
|
||||
principal=principal,
|
||||
registry=get_registry(),
|
||||
source_definition_id=definition_id,
|
||||
payload=payload,
|
||||
)
|
||||
except (PermissionError, ValueError) as exc:
|
||||
raise _governance_http_error(exc) from exc
|
||||
except WorkflowError as exc:
|
||||
raise _http_error(exc) from exc
|
||||
_audit(
|
||||
session,
|
||||
principal,
|
||||
action="workflow.definition.derived",
|
||||
definition_id=definition.id,
|
||||
details={
|
||||
"source_definition_id": (
|
||||
definition.derived_from_definition_id
|
||||
),
|
||||
"source_revision": definition.derived_from_revision,
|
||||
"source_hash": definition.derived_from_hash,
|
||||
"scope_type": definition.scope_type,
|
||||
"scope_id": definition.scope_id,
|
||||
},
|
||||
)
|
||||
response = _definition_response(session, definition, principal)
|
||||
session.commit()
|
||||
return response
|
||||
|
||||
@@ -329,7 +481,15 @@ def api_list_definition_revisions(
|
||||
tenant_id=principal.tenant_id,
|
||||
definition_id=definition_id,
|
||||
)
|
||||
require_definition_action(
|
||||
definition,
|
||||
principal=principal,
|
||||
registry=get_registry(),
|
||||
action="view",
|
||||
)
|
||||
revisions = list_definition_revisions(session, definition=definition)
|
||||
except PermissionError as exc:
|
||||
raise _governance_http_error(exc) from exc
|
||||
except WorkflowError as exc:
|
||||
raise _http_error(exc) from exc
|
||||
return WorkflowDefinitionRevisionListResponse(
|
||||
@@ -354,11 +514,19 @@ def api_get_definition_revision(
|
||||
tenant_id=principal.tenant_id,
|
||||
definition_id=definition_id,
|
||||
)
|
||||
require_definition_action(
|
||||
definition,
|
||||
principal=principal,
|
||||
registry=get_registry(),
|
||||
action="view",
|
||||
)
|
||||
item = get_definition_revision(
|
||||
session,
|
||||
definition=definition,
|
||||
revision=revision,
|
||||
)
|
||||
except PermissionError as exc:
|
||||
raise _governance_http_error(exc) from exc
|
||||
except WorkflowError as exc:
|
||||
raise _http_error(exc) from exc
|
||||
return revision_response(item)
|
||||
@@ -376,6 +544,17 @@ def api_activate_definition(
|
||||
) -> WorkflowDefinitionResponse:
|
||||
_require_any_scope(principal, DEFINITION_WRITE_SCOPE, ADMIN_SCOPE)
|
||||
try:
|
||||
existing = get_definition(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
definition_id=definition_id,
|
||||
)
|
||||
require_definition_action(
|
||||
existing,
|
||||
principal=principal,
|
||||
registry=get_registry(),
|
||||
action="edit",
|
||||
)
|
||||
definition = activate_definition(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
@@ -383,6 +562,8 @@ def api_activate_definition(
|
||||
actor_id=_actor_id(principal),
|
||||
revision=payload.revision,
|
||||
)
|
||||
except PermissionError as exc:
|
||||
raise _governance_http_error(exc) from exc
|
||||
except WorkflowError as exc:
|
||||
raise _http_error(exc) from exc
|
||||
_audit(
|
||||
@@ -392,7 +573,7 @@ def api_activate_definition(
|
||||
definition_id=definition.id,
|
||||
details={"active_revision": definition.active_revision},
|
||||
)
|
||||
response = definition_response(session, definition)
|
||||
response = _definition_response(session, definition, principal)
|
||||
session.commit()
|
||||
return response
|
||||
|
||||
@@ -408,12 +589,25 @@ def api_archive_definition(
|
||||
) -> WorkflowDefinitionResponse:
|
||||
_require_any_scope(principal, DEFINITION_WRITE_SCOPE, ADMIN_SCOPE)
|
||||
try:
|
||||
existing = get_definition(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
definition_id=definition_id,
|
||||
)
|
||||
require_definition_action(
|
||||
existing,
|
||||
principal=principal,
|
||||
registry=get_registry(),
|
||||
action="edit",
|
||||
)
|
||||
definition = archive_definition(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
definition_id=definition_id,
|
||||
actor_id=_actor_id(principal),
|
||||
)
|
||||
except PermissionError as exc:
|
||||
raise _governance_http_error(exc) from exc
|
||||
except WorkflowError as exc:
|
||||
raise _http_error(exc) from exc
|
||||
_audit(
|
||||
@@ -423,7 +617,7 @@ def api_archive_definition(
|
||||
definition_id=definition.id,
|
||||
details={"active_revision": definition.active_revision},
|
||||
)
|
||||
response = definition_response(session, definition)
|
||||
response = _definition_response(session, definition, principal)
|
||||
session.commit()
|
||||
return response
|
||||
|
||||
@@ -439,12 +633,25 @@ def api_delete_definition(
|
||||
) -> WorkflowDefinitionDeleteResponse:
|
||||
_require_any_scope(principal, DEFINITION_WRITE_SCOPE, ADMIN_SCOPE)
|
||||
try:
|
||||
existing = get_definition(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
definition_id=definition_id,
|
||||
)
|
||||
require_definition_action(
|
||||
existing,
|
||||
principal=principal,
|
||||
registry=get_registry(),
|
||||
action="edit",
|
||||
)
|
||||
definition = delete_definition(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
definition_id=definition_id,
|
||||
actor_id=_actor_id(principal),
|
||||
)
|
||||
except PermissionError as exc:
|
||||
raise _governance_http_error(exc) from exc
|
||||
except WorkflowError as exc:
|
||||
raise _http_error(exc) from exc
|
||||
_audit(
|
||||
|
||||
Reference in New Issue
Block a user