Merge commit 'd7e8533061a995595f520f31f7d4b56440078f5b'

This commit is contained in:
burnettk 2022-10-25 17:38:59 -04:00
commit 6fa370091f
5 changed files with 379 additions and 28 deletions

View File

@ -0,0 +1,26 @@
"""Get_env."""
from typing import Any
from flask import g
from typing import Optional
from SpiffWorkflow.task import Task as SpiffTask # type: ignore
from spiffworkflow_backend.scripts.script import Script
class GetUser(Script):
"""GetUser."""
def get_description(self) -> str:
"""Get_description."""
return """Return the current user."""
def run(
self,
task: Optional[SpiffTask],
environment_identifier: str,
*_args: Any,
**kwargs: Any
) -> Any:
"""Run."""
return g.user.username

View File

@ -14,6 +14,7 @@ from typing import List
from typing import NewType from typing import NewType
from typing import Optional from typing import Optional
from typing import Tuple from typing import Tuple
from typing import TypedDict
from typing import Union from typing import Union
from flask import current_app from flask import current_app
@ -75,6 +76,7 @@ from spiffworkflow_backend.models.process_instance import ProcessInstanceStatus
from spiffworkflow_backend.models.process_model import ProcessModelInfo from spiffworkflow_backend.models.process_model import ProcessModelInfo
from spiffworkflow_backend.models.task_event import TaskAction from spiffworkflow_backend.models.task_event import TaskAction
from spiffworkflow_backend.models.task_event import TaskEventModel from spiffworkflow_backend.models.task_event import TaskEventModel
from spiffworkflow_backend.models.user import UserModel
from spiffworkflow_backend.models.user import UserModelSchema from spiffworkflow_backend.models.user import UserModelSchema
from spiffworkflow_backend.scripts.script import Script from spiffworkflow_backend.scripts.script import Script
from spiffworkflow_backend.services.file_system_service import FileSystemService from spiffworkflow_backend.services.file_system_service import FileSystemService
@ -106,6 +108,13 @@ DEFAULT_GLOBALS.update(safe_globals)
DEFAULT_GLOBALS["__builtins__"]["__import__"] = _import DEFAULT_GLOBALS["__builtins__"]["__import__"] = _import
class PotentialOwnerIdList(TypedDict):
"""PotentialOwnerIdList."""
potential_owner_ids: list[int]
lane_assignment_id: Optional[int]
class ProcessInstanceProcessorError(Exception): class ProcessInstanceProcessorError(Exception):
"""ProcessInstanceProcessorError.""" """ProcessInstanceProcessorError."""
@ -114,6 +123,10 @@ class NoPotentialOwnersForTaskError(Exception):
"""NoPotentialOwnersForTaskError.""" """NoPotentialOwnersForTaskError."""
class PotentialOwnerUserNotFoundError(Exception):
"""PotentialOwnerUserNotFoundError."""
class CustomBpmnScriptEngine(PythonScriptEngine): # type: ignore class CustomBpmnScriptEngine(PythonScriptEngine): # type: ignore
"""This is a custom script processor that can be easily injected into Spiff Workflow. """This is a custom script processor that can be easily injected into Spiff Workflow.
@ -499,6 +512,58 @@ class ProcessInstanceProcessor:
self.save() self.save()
def raise_if_no_potential_owners(
self, potential_owner_ids: list[int], message: str
) -> None:
"""Raise_if_no_potential_owners."""
if not potential_owner_ids:
raise (NoPotentialOwnersForTaskError(message))
def get_potential_owner_ids_from_task(
self, task: SpiffTask
) -> PotentialOwnerIdList:
"""Get_potential_owner_ids_from_task."""
task_spec = task.task_spec
task_lane = "process_initiator"
if task_spec.lane is not None and task_spec.lane != "":
task_lane = task_spec.lane
potential_owner_ids = []
lane_assignment_id = None
if re.match(r"(process.?)initiator", task_lane, re.IGNORECASE):
potential_owner_ids = [self.process_instance_model.process_initiator_id]
elif "lane_owners" in task.data and task_lane in task.data["lane_owners"]:
for username in task.data["lane_owners"][task_lane]:
lane_owner_user = UserModel.query.filter_by(username=username).first()
if lane_owner_user is not None:
potential_owner_ids.append(lane_owner_user.id)
self.raise_if_no_potential_owners(
potential_owner_ids,
f"No users found in task data lane owner list for lane: {task_lane}. "
f"The user list used: {task.data['lane_owners'][task_lane]}",
)
else:
group_model = GroupModel.query.filter_by(identifier=task_lane).first()
if group_model is None:
raise (
NoPotentialOwnersForTaskError(
f"Could not find a group with name matching lane: {task_lane}"
)
)
potential_owner_ids = [
i.user_id for i in group_model.user_group_assignments
]
lane_assignment_id = group_model.id
self.raise_if_no_potential_owners(
potential_owner_ids,
f"Could not find any users in group to assign to lane: {task_lane}",
)
return {
"potential_owner_ids": potential_owner_ids,
"lane_assignment_id": lane_assignment_id,
}
def save(self) -> None: def save(self) -> None:
"""Saves the current state of this processor to the database.""" """Saves the current state of this processor to the database."""
self.process_instance_model.bpmn_json = self.serialize() self.process_instance_model.bpmn_json = self.serialize()
@ -532,32 +597,9 @@ class ProcessInstanceProcessor:
# filter out non-usertasks # filter out non-usertasks
task_spec = ready_or_waiting_task.task_spec task_spec = ready_or_waiting_task.task_spec
if not self.bpmn_process_instance._is_engine_task(task_spec): if not self.bpmn_process_instance._is_engine_task(task_spec):
ready_or_waiting_task.data["current_user"]["id"] potential_owner_hash = self.get_potential_owner_ids_from_task(
task_lane = "process_initiator" ready_or_waiting_task
if task_spec.lane is not None and task_spec.lane != "": )
task_lane = task_spec.lane
potential_owner_ids = []
lane_assignment_id = None
if re.match(r"(process.?)initiator", task_lane, re.IGNORECASE):
potential_owner_ids = [
self.process_instance_model.process_initiator_id
]
else:
group_model = GroupModel.query.filter_by(
identifier=task_lane
).first()
if group_model is None:
raise (
NoPotentialOwnersForTaskError(
f"Could not find a group with name matching lane: {task_lane}"
)
)
potential_owner_ids = [
i.user_id for i in group_model.user_group_assignments
]
lane_assignment_id = group_model.id
extensions = ready_or_waiting_task.task_spec.extensions extensions = ready_or_waiting_task.task_spec.extensions
form_file_name = None form_file_name = None
@ -586,12 +628,12 @@ class ProcessInstanceProcessor:
task_title=ready_or_waiting_task.task_spec.description, task_title=ready_or_waiting_task.task_spec.description,
task_type=ready_or_waiting_task.task_spec.__class__.__name__, task_type=ready_or_waiting_task.task_spec.__class__.__name__,
task_status=ready_or_waiting_task.get_state_name(), task_status=ready_or_waiting_task.get_state_name(),
lane_assignment_id=lane_assignment_id, lane_assignment_id=potential_owner_hash["lane_assignment_id"],
) )
db.session.add(active_task) db.session.add(active_task)
db.session.commit() db.session.commit()
for potential_owner_id in potential_owner_ids: for potential_owner_id in potential_owner_hash["potential_owner_ids"]:
active_task_user = ActiveTaskUserModel( active_task_user = ActiveTaskUserModel(
user_id=potential_owner_id, active_task_id=active_task.id user_id=potential_owner_id, active_task_id=active_task.id
) )

View File

@ -0,0 +1,175 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:spiffworkflow="http://spiffworkflow.org/bpmn/schema/1.0/core" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_96f6665" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.0.0-dev">
<bpmn:collaboration id="Collaboration_0iyw0q7">
<bpmn:participant id="Participant_17eqap4" processRef="Proccess_yhito9d" />
</bpmn:collaboration>
<bpmn:process id="Proccess_yhito9d" isExecutable="true">
<bpmn:laneSet id="LaneSet_17rankp">
<bpmn:lane id="process_initiator" name="Process Initiator">
<bpmn:flowNodeRef>StartEvent_1</bpmn:flowNodeRef>
<bpmn:flowNodeRef>initator_one</bpmn:flowNodeRef>
<bpmn:flowNodeRef>initiator_two</bpmn:flowNodeRef>
<bpmn:flowNodeRef>Activity_0wq6mdd</bpmn:flowNodeRef>
</bpmn:lane>
<bpmn:lane id="finance_team" name="Finance Team">
<bpmn:flowNodeRef>finance_approval_one</bpmn:flowNodeRef>
<bpmn:flowNodeRef>finance_approval_two</bpmn:flowNodeRef>
<bpmn:flowNodeRef>Activity_1s1855p</bpmn:flowNodeRef>
</bpmn:lane>
<bpmn:lane id="bigwig" name="Bigwig">
<bpmn:flowNodeRef>Event_0nsh6vv</bpmn:flowNodeRef>
<bpmn:flowNodeRef>bigwig_approval</bpmn:flowNodeRef>
</bpmn:lane>
</bpmn:laneSet>
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>Flow_1tbyols</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="Flow_1tbyols" sourceRef="StartEvent_1" targetRef="initator_one" />
<bpmn:manualTask id="initator_one" name="Initiator One">
<bpmn:extensionElements>
<spiffworkflow:instructionsForEndUser>This is initiator user?</spiffworkflow:instructionsForEndUser>
</bpmn:extensionElements>
<bpmn:incoming>Flow_1tbyols</bpmn:incoming>
<bpmn:outgoing>Flow_0xyca1b</bpmn:outgoing>
</bpmn:manualTask>
<bpmn:sequenceFlow id="Flow_0jh05kw" sourceRef="initiator_two" targetRef="bigwig_approval" />
<bpmn:sequenceFlow id="Flow_04sc2wb" sourceRef="bigwig_approval" targetRef="Event_0nsh6vv" />
<bpmn:sequenceFlow id="Flow_0xyca1b" sourceRef="initator_one" targetRef="Activity_0wq6mdd" />
<bpmn:manualTask id="initiator_two" name="Initiator Two">
<bpmn:extensionElements>
<spiffworkflow:instructionsForEndUser>This is initiator again?</spiffworkflow:instructionsForEndUser>
</bpmn:extensionElements>
<bpmn:incoming>Flow_1aluose</bpmn:incoming>
<bpmn:outgoing>Flow_0jh05kw</bpmn:outgoing>
</bpmn:manualTask>
<bpmn:endEvent id="Event_0nsh6vv">
<bpmn:incoming>Flow_04sc2wb</bpmn:incoming>
</bpmn:endEvent>
<bpmn:manualTask id="bigwig_approval" name="Bigwig Approval">
<bpmn:incoming>Flow_0jh05kw</bpmn:incoming>
<bpmn:outgoing>Flow_04sc2wb</bpmn:outgoing>
</bpmn:manualTask>
<bpmn:sequenceFlow id="Flow_13ejjwk" sourceRef="Activity_0wq6mdd" targetRef="finance_approval_one" />
<bpmn:scriptTask id="Activity_0wq6mdd" name="Set Potential Owners For Lanes" scriptFormat="python">
<bpmn:incoming>Flow_0xyca1b</bpmn:incoming>
<bpmn:outgoing>Flow_13ejjwk</bpmn:outgoing>
<bpmn:script>lane_owners = {
"Finance Team": ['testuser3', 'testuser4'],
"Bigwig": ['testadmin1'],
"Process Initiator": ['testadmin1']
}</bpmn:script>
</bpmn:scriptTask>
<bpmn:sequenceFlow id="Flow_1aluose" sourceRef="finance_approval_two" targetRef="initiator_two" />
<bpmn:manualTask id="finance_approval_one" name="Finance Approval One">
<bpmn:extensionElements>
<spiffworkflow:instructionsForEndUser>This is finance user one?</spiffworkflow:instructionsForEndUser>
<spiffworkflow:postScript />
<spiffworkflow:preScript />
</bpmn:extensionElements>
<bpmn:incoming>Flow_13ejjwk</bpmn:incoming>
<bpmn:outgoing>Flow_0bgkfue</bpmn:outgoing>
</bpmn:manualTask>
<bpmn:manualTask id="finance_approval_two" name="Finance Approval Two">
<bpmn:extensionElements>
<spiffworkflow:instructionsForEndUser>This is finance user two? {{approver}}</spiffworkflow:instructionsForEndUser>
</bpmn:extensionElements>
<bpmn:incoming>Flow_1ivhu7x</bpmn:incoming>
<bpmn:outgoing>Flow_1aluose</bpmn:outgoing>
</bpmn:manualTask>
<bpmn:sequenceFlow id="Flow_0bgkfue" sourceRef="finance_approval_one" targetRef="Activity_1s1855p" />
<bpmn:sequenceFlow id="Flow_1ivhu7x" sourceRef="Activity_1s1855p" targetRef="finance_approval_two" />
<bpmn:scriptTask id="Activity_1s1855p" scriptFormat="python">
<bpmn:incoming>Flow_0bgkfue</bpmn:incoming>
<bpmn:outgoing>Flow_1ivhu7x</bpmn:outgoing>
<bpmn:script>approver = get_user()
lane_owners["Finance Team"].remove(approver)</bpmn:script>
</bpmn:scriptTask>
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Collaboration_0iyw0q7">
<bpmndi:BPMNShape id="Participant_17eqap4_di" bpmnElement="Participant_17eqap4" isHorizontal="true">
<dc:Bounds x="129" y="-68" width="600" height="490" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Lane_0ih9iki_di" bpmnElement="bigwig" isHorizontal="true">
<dc:Bounds x="159" y="-68" width="570" height="120" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Lane_0irvyol_di" bpmnElement="finance_team" isHorizontal="true">
<dc:Bounds x="159" y="302" width="570" height="120" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Lane_1ewsife_di" bpmnElement="process_initiator" isHorizontal="true">
<dc:Bounds x="159" y="52" width="570" height="250" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="179" y="159" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1lm1ald_di" bpmnElement="initator_one">
<dc:Bounds x="270" y="137" width="100" height="80" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1c1xxe3_di" bpmnElement="initiator_two">
<dc:Bounds x="550" y="137" width="100" height="80" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_0nsh6vv_di" bpmnElement="Event_0nsh6vv">
<dc:Bounds x="672" y="-28" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1i20328_di" bpmnElement="bigwig_approval">
<dc:Bounds x="550" y="-50" width="100" height="80" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1xdc8g9_di" bpmnElement="Activity_0wq6mdd">
<dc:Bounds x="420" y="140" width="100" height="80" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1y566d5_di" bpmnElement="finance_approval_one">
<dc:Bounds x="340" y="320" width="100" height="80" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1rq1fsj_di" bpmnElement="finance_approval_two">
<dc:Bounds x="600" y="320" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0g8uv6m_di" bpmnElement="Activity_1s1855p">
<dc:Bounds x="470" y="320" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_1tbyols_di" bpmnElement="Flow_1tbyols">
<di:waypoint x="215" y="177" />
<di:waypoint x="270" y="177" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0jh05kw_di" bpmnElement="Flow_0jh05kw">
<di:waypoint x="600" y="137" />
<di:waypoint x="600" y="30" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_04sc2wb_di" bpmnElement="Flow_04sc2wb">
<di:waypoint x="650" y="-10" />
<di:waypoint x="672" y="-10" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0xyca1b_di" bpmnElement="Flow_0xyca1b">
<di:waypoint x="370" y="179" />
<di:waypoint x="420" y="180" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_13ejjwk_di" bpmnElement="Flow_13ejjwk">
<di:waypoint x="460" y="220" />
<di:waypoint x="460" y="270" />
<di:waypoint x="380" y="270" />
<di:waypoint x="380" y="320" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1aluose_di" bpmnElement="Flow_1aluose">
<di:waypoint x="650" y="320" />
<di:waypoint x="650" y="269" />
<di:waypoint x="600" y="269" />
<di:waypoint x="600" y="217" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0bgkfue_di" bpmnElement="Flow_0bgkfue">
<di:waypoint x="440" y="360" />
<di:waypoint x="470" y="360" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1ivhu7x_di" bpmnElement="Flow_1ivhu7x">
<di:waypoint x="570" y="360" />
<di:waypoint x="600" y="360" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -94,6 +94,8 @@ class TestAuthorizationService(BaseTest):
"""Test_user_can_be_added_to_active_task_on_first_login.""" """Test_user_can_be_added_to_active_task_on_first_login."""
initiator_user = self.find_or_create_user("initiator_user") initiator_user = self.find_or_create_user("initiator_user")
assert initiator_user.principal is not None assert initiator_user.principal is not None
# to ensure there is a user that can be assigned to the task
self.find_or_create_user("testuser1")
AuthorizationService.import_permissions_from_yaml_file() AuthorizationService.import_permissions_from_yaml_file()
process_model = load_test_spec( process_model = load_test_spec(

View File

@ -1,5 +1,6 @@
"""Test_process_instance_processor.""" """Test_process_instance_processor."""
import pytest import pytest
from flask import g
from flask.app import Flask from flask.app import Flask
from tests.spiffworkflow_backend.helpers.base_test import BaseTest from tests.spiffworkflow_backend.helpers.base_test import BaseTest
from tests.spiffworkflow_backend.helpers.test_data import load_test_spec from tests.spiffworkflow_backend.helpers.test_data import load_test_spec
@ -118,3 +119,108 @@ class TestProcessInstanceProcessor(BaseTest):
) )
assert process_instance.status == ProcessInstanceStatus.complete.value assert process_instance.status == ProcessInstanceStatus.complete.value
def test_sets_permission_correctly_on_active_task_when_using_dict(
self,
app: Flask,
with_db_and_bpmn_file_cleanup: None,
) -> None:
"""Test_sets_permission_correctly_on_active_task_when_using_dict."""
initiator_user = self.find_or_create_user("initiator_user")
finance_user_three = self.find_or_create_user("testuser3")
finance_user_four = self.find_or_create_user("testuser4")
testadmin1 = self.find_or_create_user("testadmin1")
assert initiator_user.principal is not None
assert finance_user_three.principal is not None
AuthorizationService.import_permissions_from_yaml_file()
finance_group = GroupModel.query.filter_by(identifier="Finance Team").first()
assert finance_group is not None
process_model = load_test_spec(
process_model_id="model_with_lanes",
bpmn_file_name="lanes_with_owner_dict.bpmn",
)
process_instance = self.create_process_instance_from_process_model(
process_model=process_model, user=initiator_user
)
processor = ProcessInstanceProcessor(process_instance)
processor.do_engine_steps(save=True)
assert len(process_instance.active_tasks) == 1
active_task = process_instance.active_tasks[0]
assert active_task.lane_assignment_id is None
assert len(active_task.potential_owners) == 1
assert active_task.potential_owners[0] == initiator_user
spiff_task = processor.__class__.get_task_by_bpmn_identifier(
active_task.task_name, processor.bpmn_process_instance
)
with pytest.raises(UserDoesNotHaveAccessToTaskError):
ProcessInstanceService.complete_form_task(
processor, spiff_task, {}, finance_user_three
)
ProcessInstanceService.complete_form_task(
processor, spiff_task, {}, initiator_user
)
assert len(process_instance.active_tasks) == 1
active_task = process_instance.active_tasks[0]
assert active_task.lane_assignment_id is None
assert len(active_task.potential_owners) == 2
assert active_task.potential_owners == [finance_user_three, finance_user_four]
spiff_task = processor.__class__.get_task_by_bpmn_identifier(
active_task.task_name, processor.bpmn_process_instance
)
with pytest.raises(UserDoesNotHaveAccessToTaskError):
ProcessInstanceService.complete_form_task(
processor, spiff_task, {}, initiator_user
)
g.user = finance_user_three
ProcessInstanceService.complete_form_task(
processor, spiff_task, {}, finance_user_three
)
assert len(process_instance.active_tasks) == 1
active_task = process_instance.active_tasks[0]
assert active_task.lane_assignment_id is None
assert len(active_task.potential_owners) == 1
assert active_task.potential_owners[0] == finance_user_four
spiff_task = processor.__class__.get_task_by_bpmn_identifier(
active_task.task_name, processor.bpmn_process_instance
)
with pytest.raises(UserDoesNotHaveAccessToTaskError):
ProcessInstanceService.complete_form_task(
processor, spiff_task, {}, initiator_user
)
ProcessInstanceService.complete_form_task(
processor, spiff_task, {}, finance_user_four
)
assert len(process_instance.active_tasks) == 1
active_task = process_instance.active_tasks[0]
assert active_task.lane_assignment_id is None
assert len(active_task.potential_owners) == 1
assert active_task.potential_owners[0] == initiator_user
spiff_task = processor.__class__.get_task_by_bpmn_identifier(
active_task.task_name, processor.bpmn_process_instance
)
ProcessInstanceService.complete_form_task(
processor, spiff_task, {}, initiator_user
)
assert len(process_instance.active_tasks) == 1
active_task = process_instance.active_tasks[0]
spiff_task = processor.__class__.get_task_by_bpmn_identifier(
active_task.task_name, processor.bpmn_process_instance
)
with pytest.raises(UserDoesNotHaveAccessToTaskError):
ProcessInstanceService.complete_form_task(
processor, spiff_task, {}, initiator_user
)
ProcessInstanceService.complete_form_task(processor, spiff_task, {}, testadmin1)
assert process_instance.status == ProcessInstanceStatus.complete.value