some fixes and updates to help with running an acceptance test model (#323)
Co-authored-by: jasquat <jasquat@users.noreply.github.com>
This commit is contained in:
parent
ee18052be9
commit
121b3c7cc9
|
@ -10,6 +10,7 @@ from werkzeug.utils import ImportStringError
|
||||||
from spiffworkflow_backend.services.logging_service import setup_logger
|
from spiffworkflow_backend.services.logging_service import setup_logger
|
||||||
|
|
||||||
HTTP_REQUEST_TIMEOUT_SECONDS = 15
|
HTTP_REQUEST_TIMEOUT_SECONDS = 15
|
||||||
|
CONNECTOR_PROXY_COMMAND_TIMEOUT = 30
|
||||||
|
|
||||||
|
|
||||||
class ConfigurationError(Exception):
|
class ConfigurationError(Exception):
|
||||||
|
|
|
@ -350,7 +350,9 @@ class CustomBpmnScriptEngine(PythonScriptEngine): # type: ignore
|
||||||
methods = self.__get_augment_methods(task)
|
methods = self.__get_augment_methods(task)
|
||||||
if external_methods:
|
if external_methods:
|
||||||
methods.update(external_methods)
|
methods.update(external_methods)
|
||||||
super().execute(task, script, methods)
|
# do not run script if it is blank
|
||||||
|
if script:
|
||||||
|
super().execute(task, script, methods)
|
||||||
return True
|
return True
|
||||||
except WorkflowException as e:
|
except WorkflowException as e:
|
||||||
raise e
|
raise e
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
import json
|
import json
|
||||||
|
import re
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
import sentry_sdk
|
import sentry_sdk
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
from flask import g
|
from flask import g
|
||||||
|
from spiffworkflow_backend.config import CONNECTOR_PROXY_COMMAND_TIMEOUT
|
||||||
from spiffworkflow_backend.config import HTTP_REQUEST_TIMEOUT_SECONDS
|
from spiffworkflow_backend.config import HTTP_REQUEST_TIMEOUT_SECONDS
|
||||||
from spiffworkflow_backend.services.file_system_service import FileSystemService
|
from spiffworkflow_backend.services.file_system_service import FileSystemService
|
||||||
from spiffworkflow_backend.services.secret_service import SecretService
|
from spiffworkflow_backend.services.secret_service import SecretService
|
||||||
|
@ -21,8 +23,8 @@ def connector_proxy_url() -> Any:
|
||||||
|
|
||||||
|
|
||||||
class ServiceTaskDelegate:
|
class ServiceTaskDelegate:
|
||||||
@staticmethod
|
@classmethod
|
||||||
def check_prefixes(value: Any) -> Any:
|
def handle_template_substitutions(cls, value: Any) -> Any:
|
||||||
if isinstance(value, str):
|
if isinstance(value, str):
|
||||||
secret_prefix = "secret:" # noqa: S105
|
secret_prefix = "secret:" # noqa: S105
|
||||||
if value.startswith(secret_prefix):
|
if value.startswith(secret_prefix):
|
||||||
|
@ -38,6 +40,24 @@ class ServiceTaskDelegate:
|
||||||
with open(full_path) as f:
|
with open(full_path) as f:
|
||||||
return f.read()
|
return f.read()
|
||||||
|
|
||||||
|
if "SPIFF_SECRET:" in value:
|
||||||
|
spiff_secret_match = re.match(r".*SPIFF_SECRET:(?P<variable_name>\w+).*", value)
|
||||||
|
if spiff_secret_match is not None:
|
||||||
|
spiff_variable_name = spiff_secret_match.group("variable_name")
|
||||||
|
secret = SecretService.get_secret(spiff_variable_name)
|
||||||
|
with sentry_sdk.start_span(op="task", description="decrypt_secret"):
|
||||||
|
decrypted_value = SecretService._decrypt(secret.value)
|
||||||
|
return re.sub(r"\bSPIFF_SECRET:\w+", decrypted_value, value)
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def value_with_secrets_replaced(cls, value: Any) -> Any:
|
||||||
|
if isinstance(value, str):
|
||||||
|
return cls.handle_template_substitutions(value)
|
||||||
|
elif isinstance(value, dict):
|
||||||
|
for key, v in value.items():
|
||||||
|
value[key] = cls.value_with_secrets_replaced(v)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -65,17 +85,17 @@ class ServiceTaskDelegate:
|
||||||
)
|
)
|
||||||
return msg
|
return msg
|
||||||
|
|
||||||
@staticmethod
|
@classmethod
|
||||||
def call_connector(name: str, bpmn_params: Any, task_data: Any) -> str:
|
def call_connector(cls, name: str, bpmn_params: Any, task_data: Any) -> str:
|
||||||
"""Calls a connector via the configured proxy."""
|
"""Calls a connector via the configured proxy."""
|
||||||
call_url = f"{connector_proxy_url()}/v1/do/{name}"
|
call_url = f"{connector_proxy_url()}/v1/do/{name}"
|
||||||
current_app.logger.info(f"Calling connector proxy using connector: {name}")
|
current_app.logger.info(f"Calling connector proxy using connector: {name}")
|
||||||
with sentry_sdk.start_span(op="connector_by_name", description=name):
|
with sentry_sdk.start_span(op="connector_by_name", description=name):
|
||||||
with sentry_sdk.start_span(op="call-connector", description=call_url):
|
with sentry_sdk.start_span(op="call-connector", description=call_url):
|
||||||
params = {k: ServiceTaskDelegate.check_prefixes(v["value"]) for k, v in bpmn_params.items()}
|
params = {k: cls.value_with_secrets_replaced(v["value"]) for k, v in bpmn_params.items()}
|
||||||
params["spiff__task_data"] = task_data
|
params["spiff__task_data"] = task_data
|
||||||
|
|
||||||
proxied_response = requests.post(call_url, json=params, timeout=HTTP_REQUEST_TIMEOUT_SECONDS)
|
proxied_response = requests.post(call_url, json=params, timeout=CONNECTOR_PROXY_COMMAND_TIMEOUT)
|
||||||
response_text = proxied_response.text
|
response_text = proxied_response.text
|
||||||
json_parse_error = None
|
json_parse_error = None
|
||||||
|
|
||||||
|
@ -98,7 +118,10 @@ class ServiceTaskDelegate:
|
||||||
message = ServiceTaskDelegate.get_message_for_status(proxied_response.status_code)
|
message = ServiceTaskDelegate.get_message_for_status(proxied_response.status_code)
|
||||||
error = f"Received an unexpected response from service {name} : {message}"
|
error = f"Received an unexpected response from service {name} : {message}"
|
||||||
if "error" in parsed_response:
|
if "error" in parsed_response:
|
||||||
error += parsed_response["error"]
|
error_response = parsed_response["error"]
|
||||||
|
if isinstance(error_response, list | dict):
|
||||||
|
error_response = json.dumps(parsed_response["error"])
|
||||||
|
error += error_response
|
||||||
if json_parse_error:
|
if json_parse_error:
|
||||||
error += "A critical component (The connector proxy) is not responding correctly."
|
error += "A critical component (The connector proxy) is not responding correctly."
|
||||||
raise ConnectorProxyError(error)
|
raise ConnectorProxyError(error)
|
||||||
|
|
|
@ -11,19 +11,33 @@ from tests.spiffworkflow_backend.helpers.base_test import BaseTest
|
||||||
|
|
||||||
class TestServiceTaskDelegate(BaseTest):
|
class TestServiceTaskDelegate(BaseTest):
|
||||||
def test_check_prefixes_without_secret(self, app: Flask, with_db_and_bpmn_file_cleanup: None) -> None:
|
def test_check_prefixes_without_secret(self, app: Flask, with_db_and_bpmn_file_cleanup: None) -> None:
|
||||||
result = ServiceTaskDelegate.check_prefixes("hey")
|
result = ServiceTaskDelegate.value_with_secrets_replaced("hey")
|
||||||
assert result == "hey"
|
assert result == "hey"
|
||||||
|
|
||||||
def test_check_prefixes_with_int(self, app: Flask, with_db_and_bpmn_file_cleanup: None) -> None:
|
def test_check_prefixes_with_int(self, app: Flask, with_db_and_bpmn_file_cleanup: None) -> None:
|
||||||
result = ServiceTaskDelegate.check_prefixes(1)
|
result = ServiceTaskDelegate.value_with_secrets_replaced(1)
|
||||||
assert result == 1
|
assert result == 1
|
||||||
|
|
||||||
def test_check_prefixes_with_secret(self, app: Flask, with_db_and_bpmn_file_cleanup: None) -> None:
|
def test_check_prefixes_with_secret(self, app: Flask, with_db_and_bpmn_file_cleanup: None) -> None:
|
||||||
user = self.find_or_create_user("test_user")
|
user = self.find_or_create_user("test_user")
|
||||||
SecretService().add_secret("hot_secret", "my_secret_value", user.id)
|
SecretService().add_secret("hot_secret", "my_secret_value", user.id)
|
||||||
result = ServiceTaskDelegate.check_prefixes("secret:hot_secret")
|
result = ServiceTaskDelegate.value_with_secrets_replaced("secret:hot_secret")
|
||||||
assert result == "my_secret_value"
|
assert result == "my_secret_value"
|
||||||
|
|
||||||
|
def test_check_prefixes_with_spiff_secret(self, app: Flask, with_db_and_bpmn_file_cleanup: None) -> None:
|
||||||
|
user = self.find_or_create_user("test_user")
|
||||||
|
SecretService().add_secret("hot_secret", "my_secret_value", user.id)
|
||||||
|
result = ServiceTaskDelegate.value_with_secrets_replaced("TOKEN SPIFF_SECRET:hot_secret-haha")
|
||||||
|
assert result == "TOKEN my_secret_value-haha"
|
||||||
|
|
||||||
|
def test_check_prefixes_with_spiff_secret_in_dict(self, app: Flask, with_db_and_bpmn_file_cleanup: None) -> None:
|
||||||
|
user = self.find_or_create_user("test_user")
|
||||||
|
SecretService().add_secret("hot_secret", "my_secret_value", user.id)
|
||||||
|
result = ServiceTaskDelegate.value_with_secrets_replaced(
|
||||||
|
{"Authorization": "TOKEN SPIFF_SECRET:hot_secret-haha"}
|
||||||
|
)
|
||||||
|
assert result == {"Authorization": "TOKEN my_secret_value-haha"}
|
||||||
|
|
||||||
def test_invalid_call_returns_good_error_message(self, app: Flask, with_db_and_bpmn_file_cleanup: None) -> None:
|
def test_invalid_call_returns_good_error_message(self, app: Flask, with_db_and_bpmn_file_cleanup: None) -> None:
|
||||||
with patch("requests.post") as mock_post:
|
with patch("requests.post") as mock_post:
|
||||||
mock_post.return_value.status_code = 404
|
mock_post.return_value.status_code = 404
|
||||||
|
|
|
@ -4,6 +4,7 @@ import {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
} from '@carbon/react';
|
} from '@carbon/react';
|
||||||
import { Can } from '@casl/react';
|
import { Can } from '@casl/react';
|
||||||
|
import { useState } from 'react';
|
||||||
import {
|
import {
|
||||||
PermissionsToCheck,
|
PermissionsToCheck,
|
||||||
ProcessModel,
|
ProcessModel,
|
||||||
|
@ -78,6 +79,7 @@ export default function ProcessInstanceRun({
|
||||||
}: OwnProps) {
|
}: OwnProps) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { addError, removeError } = useAPIError();
|
const { addError, removeError } = useAPIError();
|
||||||
|
const [disableStartButton, setDisableStartButton] = useState<boolean>(false);
|
||||||
const modifiedProcessModelId = modifyProcessIdentifierForPathParam(
|
const modifiedProcessModelId = modifyProcessIdentifierForPathParam(
|
||||||
processModel.id
|
processModel.id
|
||||||
);
|
);
|
||||||
|
@ -109,20 +111,29 @@ export default function ProcessInstanceRun({
|
||||||
HttpService.makeCallToBackend({
|
HttpService.makeCallToBackend({
|
||||||
path: `/process-instances/${modifiedProcessModelId}/${processInstance.id}/run`,
|
path: `/process-instances/${modifiedProcessModelId}/${processInstance.id}/run`,
|
||||||
successCallback: onProcessInstanceRun,
|
successCallback: onProcessInstanceRun,
|
||||||
failureCallback: addError,
|
failureCallback: (result: any) => {
|
||||||
|
addError(result);
|
||||||
|
setDisableStartButton(false);
|
||||||
|
},
|
||||||
httpMethod: 'POST',
|
httpMethod: 'POST',
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const processInstanceCreateAndRun = () => {
|
const processInstanceCreateAndRun = () => {
|
||||||
removeError();
|
removeError();
|
||||||
|
setDisableStartButton(true);
|
||||||
HttpService.makeCallToBackend({
|
HttpService.makeCallToBackend({
|
||||||
path: processInstanceCreatePath,
|
path: processInstanceCreatePath,
|
||||||
successCallback: processModelRun,
|
successCallback: processModelRun,
|
||||||
failureCallback: addError,
|
failureCallback: (result: any) => {
|
||||||
|
addError(result);
|
||||||
|
setDisableStartButton(false);
|
||||||
|
},
|
||||||
httpMethod: 'POST',
|
httpMethod: 'POST',
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// if checkPermissions is false then assume the page using this component has already checked the permissions
|
||||||
if (checkPermissions) {
|
if (checkPermissions) {
|
||||||
return (
|
return (
|
||||||
<Can I="POST" a={processInstanceCreatePath} ability={ability}>
|
<Can I="POST" a={processInstanceCreatePath} ability={ability}>
|
||||||
|
@ -130,6 +141,7 @@ export default function ProcessInstanceRun({
|
||||||
data-qa="start-process-instance"
|
data-qa="start-process-instance"
|
||||||
onClick={processInstanceCreateAndRun}
|
onClick={processInstanceCreateAndRun}
|
||||||
className={className}
|
className={className}
|
||||||
|
disabled={disableStartButton}
|
||||||
>
|
>
|
||||||
Start
|
Start
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -137,7 +149,11 @@ export default function ProcessInstanceRun({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Button onClick={processInstanceCreateAndRun} className={className}>
|
<Button
|
||||||
|
onClick={processInstanceCreateAndRun}
|
||||||
|
className={className}
|
||||||
|
disabled={disableStartButton}
|
||||||
|
>
|
||||||
Start
|
Start
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in New Issue