diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml
index d96de3db..6c720265 100755
--- a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml
+++ b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml
@@ -946,6 +946,27 @@ paths:
schema:
$ref: "#/components/schemas/Workflow"
+ /process-instances/find-by-id/{process_instance_id}:
+ parameters:
+ - name: process_instance_id
+ in: path
+ required: true
+ description: The unique id of an existing process instance.
+ schema:
+ type: integer
+ get:
+ operationId: spiffworkflow_backend.routes.process_instances_controller.process_instance_find_by_id
+ summary: Find a process instance based on its id only
+ tags:
+ - Process Instances
+ responses:
+ "200":
+ description: One Process Instance
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/Workflow"
+
/process-instances/{modified_process_model_identifier}/{process_instance_id}:
parameters:
- name: modified_process_model_identifier
diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/testing.yml b/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/testing.yml
index 79a13710..d3edf0a8 100644
--- a/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/testing.yml
+++ b/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/testing.yml
@@ -30,6 +30,12 @@ permissions:
allowed_permissions: [read]
uri: /*
+ process-instances-find-by-id:
+ groups: [everybody]
+ users: []
+ allowed_permissions: [read]
+ uri: /process-instances/find-by-id/*
+
tasks-crud:
groups: [everybody]
users: []
diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/process_model.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/process_model.py
index c737b274..5e0ba6ca 100644
--- a/spiffworkflow-backend/src/spiffworkflow_backend/models/process_model.py
+++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/process_model.py
@@ -58,6 +58,14 @@ class ProcessModelInfo:
"""Id_for_file_path."""
return self.id.replace("/", os.sep)
+ @classmethod
+ def modify_process_identifier_for_path_param(cls, identifier: str) -> str:
+ """Identifier."""
+ if "\\" in identifier:
+ raise Exception(f"Found backslash in identifier: {identifier}")
+
+ return identifier.replace("/", ":")
+
class ProcessModelInfoSchema(Schema):
"""ProcessModelInfoSchema."""
diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_instances_controller.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_instances_controller.py
index ed27f2b2..5fbaecdf 100644
--- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_instances_controller.py
+++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_instances_controller.py
@@ -31,6 +31,7 @@ from spiffworkflow_backend.models.process_instance_metadata import (
from spiffworkflow_backend.models.process_instance_report import (
ProcessInstanceReportModel,
)
+from spiffworkflow_backend.models.process_model import ProcessModelInfo
from spiffworkflow_backend.models.spec_reference import SpecReferenceCache
from spiffworkflow_backend.models.spec_reference import SpecReferenceNotFoundError
from spiffworkflow_backend.models.spiff_logging import SpiffLoggingModel
@@ -43,6 +44,7 @@ from spiffworkflow_backend.routes.process_api_blueprint import _get_process_mode
from spiffworkflow_backend.routes.process_api_blueprint import (
_un_modify_modified_process_model_id,
)
+from spiffworkflow_backend.services.authorization_service import AuthorizationService
from spiffworkflow_backend.services.error_handling_service import ErrorHandlingService
from spiffworkflow_backend.services.git_service import GitCommandError
from spiffworkflow_backend.services.git_service import GitService
@@ -88,9 +90,7 @@ def process_instance_run(
do_engine_steps: bool = True,
) -> flask.wrappers.Response:
"""Process_instance_run."""
- process_instance = ProcessInstanceService().get_process_instance(
- process_instance_id
- )
+ process_instance = _find_process_instance_by_id_or_raise(process_instance_id)
if process_instance.status != "not_started":
raise ApiError(
error_code="process_instance_not_runnable",
@@ -138,9 +138,7 @@ def process_instance_terminate(
modified_process_model_identifier: str,
) -> flask.wrappers.Response:
"""Process_instance_run."""
- process_instance = ProcessInstanceService().get_process_instance(
- process_instance_id
- )
+ process_instance = _find_process_instance_by_id_or_raise(process_instance_id)
processor = ProcessInstanceProcessor(process_instance)
processor.terminate()
return Response(json.dumps({"ok": True}), status=200, mimetype="application/json")
@@ -151,9 +149,7 @@ def process_instance_suspend(
modified_process_model_identifier: str,
) -> flask.wrappers.Response:
"""Process_instance_suspend."""
- process_instance = ProcessInstanceService().get_process_instance(
- process_instance_id
- )
+ process_instance = _find_process_instance_by_id_or_raise(process_instance_id)
processor = ProcessInstanceProcessor(process_instance)
processor.suspend()
return Response(json.dumps({"ok": True}), status=200, mimetype="application/json")
@@ -164,9 +160,7 @@ def process_instance_resume(
modified_process_model_identifier: str,
) -> flask.wrappers.Response:
"""Process_instance_resume."""
- process_instance = ProcessInstanceService().get_process_instance(
- process_instance_id
- )
+ process_instance = _find_process_instance_by_id_or_raise(process_instance_id)
processor = ProcessInstanceProcessor(process_instance)
processor.resume()
return Response(json.dumps({"ok": True}), status=200, mimetype="application/json")
@@ -575,14 +569,43 @@ def process_instance_reset(
spiff_step: int = 0,
) -> flask.wrappers.Response:
"""Reset a process instance to a particular step."""
- process_instance = ProcessInstanceService().get_process_instance(
- process_instance_id
- )
+ process_instance = _find_process_instance_by_id_or_raise(process_instance_id)
processor = ProcessInstanceProcessor(process_instance)
processor.reset_process(spiff_step)
return Response(json.dumps({"ok": True}), status=200, mimetype="application/json")
+def process_instance_find_by_id(
+ process_instance_id: int,
+) -> flask.wrappers.Response:
+ """Process_instance_find_by_id."""
+ process_instance = _find_process_instance_by_id_or_raise(process_instance_id)
+ modified_process_model_identifier = (
+ ProcessModelInfo.modify_process_identifier_for_path_param(
+ process_instance.process_model_identifier
+ )
+ )
+ process_instance_uri = (
+ f"/process-instances/{modified_process_model_identifier}/{process_instance.id}"
+ )
+ has_permission = AuthorizationService.user_has_permission(
+ user=g.user,
+ permission="read",
+ target_uri=process_instance_uri,
+ )
+
+ uri_type = None
+ if not has_permission:
+ process_instance = _find_process_instance_for_me_or_raise(process_instance_id)
+ uri_type = "for-me"
+
+ response_json = {
+ "process_instance": process_instance,
+ "uri_type": uri_type,
+ }
+ return make_response(jsonify(response_json), 200)
+
+
def _get_process_instance(
modified_process_model_identifier: str,
process_instance: ProcessInstanceModel,
diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py
index 69d19cb7..9abe2597 100644
--- a/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py
+++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py
@@ -624,6 +624,84 @@ class AuthorizationService:
return permissions_to_assign
+ @classmethod
+ def set_basic_permissions(cls) -> list[PermissionToAssign]:
+ """Set_basic_permissions."""
+ permissions_to_assign: list[PermissionToAssign] = []
+ permissions_to_assign.append(
+ PermissionToAssign(
+ permission="read", target_uri="/process-instances/for-me"
+ )
+ )
+ permissions_to_assign.append(
+ PermissionToAssign(permission="read", target_uri="/processes")
+ )
+ permissions_to_assign.append(
+ PermissionToAssign(permission="read", target_uri="/service-tasks")
+ )
+ permissions_to_assign.append(
+ PermissionToAssign(
+ permission="read", target_uri="/user-groups/for-current-user"
+ )
+ )
+ permissions_to_assign.append(
+ PermissionToAssign(
+ permission="read", target_uri="/process-instances/find-by-id/*"
+ )
+ )
+
+ for permission in ["create", "read", "update", "delete"]:
+ permissions_to_assign.append(
+ PermissionToAssign(
+ permission=permission, target_uri="/process-instances/reports/*"
+ )
+ )
+ permissions_to_assign.append(
+ PermissionToAssign(permission=permission, target_uri="/tasks/*")
+ )
+ return permissions_to_assign
+
+ @classmethod
+ def set_process_group_permissions(
+ cls, target: str, permission_set: str
+ ) -> list[PermissionToAssign]:
+ """Set_process_group_permissions."""
+ permissions_to_assign: list[PermissionToAssign] = []
+ process_group_identifier = (
+ target.removeprefix("PG:").replace("/", ":").removeprefix(":")
+ )
+ process_related_path_segment = f"{process_group_identifier}:*"
+ if process_group_identifier == "ALL":
+ process_related_path_segment = "*"
+ target_uris = [
+ f"/process-groups/{process_related_path_segment}",
+ f"/process-models/{process_related_path_segment}",
+ ]
+ permissions_to_assign = permissions_to_assign + cls.get_permissions_to_assign(
+ permission_set, process_related_path_segment, target_uris
+ )
+ return permissions_to_assign
+
+ @classmethod
+ def set_process_model_permissions(
+ cls, target: str, permission_set: str
+ ) -> list[PermissionToAssign]:
+ """Set_process_model_permissions."""
+ permissions_to_assign: list[PermissionToAssign] = []
+ process_model_identifier = (
+ target.removeprefix("PM:").replace("/", ":").removeprefix(":")
+ )
+ process_related_path_segment = f"{process_model_identifier}/*"
+
+ if process_model_identifier == "ALL":
+ process_related_path_segment = "*"
+
+ target_uris = [f"/process-models/{process_related_path_segment}"]
+ permissions_to_assign = permissions_to_assign + cls.get_permissions_to_assign(
+ permission_set, process_related_path_segment, target_uris
+ )
+ return permissions_to_assign
+
@classmethod
def explode_permissions(
cls, permission_set: str, target: str
@@ -654,72 +732,20 @@ class AuthorizationService:
permissions = ["create", "read", "update", "delete"]
if target.startswith("PG:"):
- process_group_identifier = (
- target.removeprefix("PG:").replace("/", ":").removeprefix(":")
+ permissions_to_assign += cls.set_process_group_permissions(
+ target, permission_set
)
- process_related_path_segment = f"{process_group_identifier}:*"
- if process_group_identifier == "ALL":
- process_related_path_segment = "*"
- target_uris = [
- f"/process-groups/{process_related_path_segment}",
- f"/process-models/{process_related_path_segment}",
- ]
- permissions_to_assign = (
- permissions_to_assign
- + cls.get_permissions_to_assign(
- permission_set, process_related_path_segment, target_uris
- )
- )
-
elif target.startswith("PM:"):
- process_model_identifier = (
- target.removeprefix("PM:").replace("/", ":").removeprefix(":")
+ permissions_to_assign += cls.set_process_model_permissions(
+ target, permission_set
)
- process_related_path_segment = f"{process_model_identifier}/*"
-
- if process_model_identifier == "ALL":
- process_related_path_segment = "*"
-
- target_uris = [f"/process-models/{process_related_path_segment}"]
- permissions_to_assign = (
- permissions_to_assign
- + cls.get_permissions_to_assign(
- permission_set, process_related_path_segment, target_uris
- )
- )
-
elif permission_set == "start":
raise InvalidPermissionError(
"Permission 'start' is only available for macros PM and PG."
)
elif target.startswith("BASIC"):
- permissions_to_assign.append(
- PermissionToAssign(
- permission="read", target_uri="/process-instances/for-me"
- )
- )
- permissions_to_assign.append(
- PermissionToAssign(permission="read", target_uri="/processes")
- )
- permissions_to_assign.append(
- PermissionToAssign(permission="read", target_uri="/service-tasks")
- )
- permissions_to_assign.append(
- PermissionToAssign(
- permission="read", target_uri="/user-groups/for-current-user"
- )
- )
-
- for permission in ["create", "read", "update", "delete"]:
- permissions_to_assign.append(
- PermissionToAssign(
- permission=permission, target_uri="/process-instances/reports/*"
- )
- )
- permissions_to_assign.append(
- PermissionToAssign(permission=permission, target_uri="/tasks/*")
- )
+ permissions_to_assign += cls.set_basic_permissions()
elif target == "ALL":
for permission in permissions:
permissions_to_assign.append(
diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/helpers/base_test.py b/spiffworkflow-backend/tests/spiffworkflow_backend/helpers/base_test.py
index 47cf2d87..df62f5be 100644
--- a/spiffworkflow-backend/tests/spiffworkflow_backend/helpers/base_test.py
+++ b/spiffworkflow-backend/tests/spiffworkflow_backend/helpers/base_test.py
@@ -354,11 +354,8 @@ class BaseTest:
assert has_permission is expected_result
def modify_process_identifier_for_path_param(self, identifier: str) -> str:
- """Identifier."""
- if "\\" in identifier:
- raise Exception(f"Found backslash in identifier: {identifier}")
-
- return identifier.replace("/", ":")
+ """Modify_process_identifier_for_path_param."""
+ return ProcessModelInfo.modify_process_identifier_for_path_param(identifier)
def un_modify_modified_process_identifier_for_path_param(
self, modified_identifier: str
diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_instances_controller.py b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_instances_controller.py
new file mode 100644
index 00000000..e9c403a4
--- /dev/null
+++ b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_instances_controller.py
@@ -0,0 +1,61 @@
+"""Test_users_controller."""
+from flask.app import Flask
+from flask.testing import FlaskClient
+from tests.spiffworkflow_backend.helpers.base_test import BaseTest
+from tests.spiffworkflow_backend.helpers.test_data import load_test_spec
+
+from spiffworkflow_backend.models.user import UserModel
+
+
+class TestProcessInstancesController(BaseTest):
+ """TestProcessInstancesController."""
+
+ def test_find_by_id(
+ self,
+ app: Flask,
+ client: FlaskClient,
+ with_db_and_bpmn_file_cleanup: None,
+ with_super_admin_user: UserModel,
+ ) -> None:
+ """Test_user_search_returns_a_user."""
+ user_one = self.create_user_with_permission(
+ username="user_one", target_uri="/process-instances/find-by-id/*"
+ )
+ user_two = self.create_user_with_permission(
+ username="user_two", target_uri="/process-instances/find-by-id/*"
+ )
+
+ process_model = load_test_spec(
+ process_model_id="group/sample",
+ bpmn_file_name="sample.bpmn",
+ process_model_source_directory="sample",
+ )
+ process_instance = self.create_process_instance_from_process_model(
+ process_model=process_model, user=user_one
+ )
+
+ response = client.get(
+ f"/v1.0/process-instances/find-by-id/{process_instance.id}",
+ headers=self.logged_in_headers(user_one),
+ )
+ assert response.status_code == 200
+ assert response.json
+ assert 'process_instance' in response.json
+ assert response.json['process_instance']["id"] == process_instance.id
+ assert response.json['uri_type'] == 'for-me'
+
+ response = client.get(
+ f"/v1.0/process-instances/find-by-id/{process_instance.id}",
+ headers=self.logged_in_headers(user_two),
+ )
+ assert response.status_code == 400
+
+ response = client.get(
+ f"/v1.0/process-instances/find-by-id/{process_instance.id}",
+ headers=self.logged_in_headers(with_super_admin_user),
+ )
+ assert response.status_code == 200
+ assert response.json
+ assert 'process_instance' in response.json
+ assert response.json['process_instance']["id"] == process_instance.id
+ assert response.json['uri_type'] is None
diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_authorization_service.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_authorization_service.py
index 83ed7fd8..9e7af5d0 100644
--- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_authorization_service.py
+++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_authorization_service.py
@@ -277,6 +277,7 @@ class TestAuthorizationService(BaseTest):
) -> None:
"""Test_explode_permissions_basic."""
expected_permissions = [
+ ("/process-instances/find-by-id/*", "read"),
("/process-instances/for-me", "read"),
("/process-instances/reports/*", "create"),
("/process-instances/reports/*", "delete"),
diff --git a/spiffworkflow-frontend/src/helpers.test.tsx b/spiffworkflow-frontend/src/helpers.test.tsx
index 660f65f6..5a7889a6 100644
--- a/spiffworkflow-frontend/src/helpers.test.tsx
+++ b/spiffworkflow-frontend/src/helpers.test.tsx
@@ -1,5 +1,6 @@
import {
convertSecondsToFormattedDateString,
+ isInteger,
slugifyString,
underscorizeString,
} from './helpers';
@@ -20,3 +21,11 @@ test('it can keep the correct date when converting seconds to date', () => {
const dateString = convertSecondsToFormattedDateString(1666325400);
expect(dateString).toEqual('2022-10-21');
});
+
+test('it can validate numeric values', () => {
+ expect(isInteger('11')).toEqual(true);
+ expect(isInteger('hey')).toEqual(false);
+ expect(isInteger(' ')).toEqual(false);
+ expect(isInteger('1 2')).toEqual(false);
+ expect(isInteger(2)).toEqual(true);
+});
diff --git a/spiffworkflow-frontend/src/helpers.tsx b/spiffworkflow-frontend/src/helpers.tsx
index d91f0543..b3edd776 100644
--- a/spiffworkflow-frontend/src/helpers.tsx
+++ b/spiffworkflow-frontend/src/helpers.tsx
@@ -253,3 +253,7 @@ export const setErrorMessageSafely = (
errorMessageSetter({ message: newErrorMessageString });
return null;
};
+
+export const isInteger = (str: string | number) => {
+ return /^\d+$/.test(str.toString());
+};
diff --git a/spiffworkflow-frontend/src/routes/AdminRoutes.tsx b/spiffworkflow-frontend/src/routes/AdminRoutes.tsx
index 2d61439b..8d21a5b9 100644
--- a/spiffworkflow-frontend/src/routes/AdminRoutes.tsx
+++ b/spiffworkflow-frontend/src/routes/AdminRoutes.tsx
@@ -23,6 +23,7 @@ import MessageInstanceList from './MessageInstanceList';
import Configuration from './Configuration';
import JsonSchemaFormBuilder from './JsonSchemaFormBuilder';
import ProcessModelNewExperimental from './ProcessModelNewExperimental';
+import ProcessInstanceFindById from './ProcessInstanceFindById';
export default function AdminRoutes() {
const location = useLocation();
@@ -133,6 +134,10 @@ export default function AdminRoutes() {
path="process-models/:process_model_id/form-builder"
element={}
/>
+ }
+ />
);
}
diff --git a/spiffworkflow-frontend/src/routes/ProcessInstanceFindById.tsx b/spiffworkflow-frontend/src/routes/ProcessInstanceFindById.tsx
new file mode 100644
index 00000000..e55520ef
--- /dev/null
+++ b/spiffworkflow-frontend/src/routes/ProcessInstanceFindById.tsx
@@ -0,0 +1,79 @@
+import { useEffect, useState } from 'react';
+import { useNavigate } from 'react-router-dom';
+// @ts-ignore
+import { Button, ButtonSet, Form, Stack, TextInput } from '@carbon/react';
+import { isInteger, modifyProcessIdentifierForPathParam } from '../helpers';
+import HttpService from '../services/HttpService';
+import { ProcessInstance } from '../interfaces';
+
+export default function ProcessInstanceFindById() {
+ const navigate = useNavigate();
+ const [processInstanceId, setProcessInstanceId] = useState('');
+ const [processInstanceIdValid, setProcessInstanceIdValid] =
+ useState(true);
+
+ useEffect(() => {}, []);
+
+ const handleProcessInstanceNavigation = (result: any) => {
+ const processInstance: ProcessInstance = result.process_instance;
+ let path = '/admin/process-instances/';
+ if (result.uri_type === 'for-me') {
+ path += 'for-me/';
+ }
+ path += `${modifyProcessIdentifierForPathParam(
+ processInstance.process_model_identifier
+ )}/${processInstance.id}`;
+ navigate(path);
+ };
+
+ const handleFormSubmission = (event: any) => {
+ event.preventDefault();
+
+ if (!processInstanceId) {
+ setProcessInstanceIdValid(false);
+ }
+
+ if (processInstanceId && processInstanceIdValid) {
+ HttpService.makeCallToBackend({
+ path: `/process-instances/find-by-id/${processInstanceId}`,
+ successCallback: handleProcessInstanceNavigation,
+ });
+ }
+ };
+
+ const handleProcessInstanceIdChange = (event: any) => {
+ if (isInteger(event.target.value)) {
+ setProcessInstanceIdValid(true);
+ } else {
+ setProcessInstanceIdValid(false);
+ }
+ setProcessInstanceId(event.target.value);
+ };
+
+ const formElements = () => {
+ return (
+
+ );
+ };
+
+ const formButtons = () => {
+ const buttons = [];
+ return {buttons};
+ };
+
+ return (
+
+ );
+}
diff --git a/spiffworkflow-frontend/src/routes/ProcessInstanceList.tsx b/spiffworkflow-frontend/src/routes/ProcessInstanceList.tsx
index a18f48c8..33af0c1f 100644
--- a/spiffworkflow-frontend/src/routes/ProcessInstanceList.tsx
+++ b/spiffworkflow-frontend/src/routes/ProcessInstanceList.tsx
@@ -85,6 +85,15 @@ export default function ProcessInstanceList({ variant }: OwnProps) {
All
+ {
+ navigate('/admin/process-instances/find-by-id');
+ }}
+ >
+ Find By Id
+
diff --git a/spiffworkflow-frontend/src/routes/ProcessModelShow.tsx b/spiffworkflow-frontend/src/routes/ProcessModelShow.tsx
index 9c1e4bef..a46d02a2 100644
--- a/spiffworkflow-frontend/src/routes/ProcessModelShow.tsx
+++ b/spiffworkflow-frontend/src/routes/ProcessModelShow.tsx
@@ -100,7 +100,7 @@ export default function ProcessModelShow() {
onClose={() => setProcessInstance(null)}
>
view