diff --git a/.flake8 b/.flake8 index d404e7fda..a35f75e0e 100644 --- a/.flake8 +++ b/.flake8 @@ -8,8 +8,11 @@ rst-roles = class,const,func,meth,mod,ref rst-directives = deprecated per-file-ignores = - # prefer naming tests descriptively rather than forcing comments - spiffworkflow-backend/tests/*:S101,D103 + # asserts are ok in tests + spiffworkflow-backend/tests/*:S101 + + # prefer naming functions descriptively rather than forcing comments + spiffworkflow-backend/*:D103 spiffworkflow-backend/bin/keycloak_test_server.py:B950,D spiffworkflow-backend/conftest.py:S105 diff --git a/.github/workflows/frontend_tests.yml b/.github/workflows/frontend_tests.yml index 1bbfdbedc..969a3ae89 100644 --- a/.github/workflows/frontend_tests.yml +++ b/.github/workflows/frontend_tests.yml @@ -86,7 +86,7 @@ jobs: run: ./bin/wait_for_frontend_to_be_up 5 - name: wait_for_keycloak working-directory: ./spiffworkflow-backend - run: ./bin/wait_for_keycloak 5 + run: ./keycloak/bin/wait_for_keycloak 5 - name: Cypress run uses: cypress-io/github-action@v4 with: diff --git a/SpiffWorkflow/SpiffWorkflow/task.py b/SpiffWorkflow/SpiffWorkflow/task.py index 9673287c7..6d4a918aa 100644 --- a/SpiffWorkflow/SpiffWorkflow/task.py +++ b/SpiffWorkflow/SpiffWorkflow/task.py @@ -712,7 +712,6 @@ class Task(object, metaclass=DeprecatedMetaTask): Defines the given attribute/value pairs. """ self.data.update(kwargs) - data_log.info('Set data', extra=self.log_info()) def _inherit_data(self): """ diff --git a/bin/run_pyl b/bin/run_pyl index d97f68d85..974b80600 100755 --- a/bin/run_pyl +++ b/bin/run_pyl @@ -31,12 +31,6 @@ function get_python_dirs() { (git ls-tree -r HEAD --name-only | grep -E '\.py$' | awk -F '/' '{print $1}' | sort | uniq | grep -v '\.' | grep -Ev '^(bin|migrations)$') || echo '' } -function run_fix_docstrings() { - if command -v fix_python_docstrings >/dev/null ; then - fix_python_docstrings $(get_top_level_directories_containing_python_files) - fi -} - function run_autoflake() { # checking command -v autoflake8 is not good enough, since the asdf shim may be installed, which will make command -v succeed, # but autoflake8 may not have been pip installed inside the correct version of python. @@ -86,7 +80,6 @@ done for python_project in "${python_projects[@]}" ; do if [[ "$subcommand" != "pre" ]] || [[ -n "$(git status --porcelain "$python_project")" ]]; then pushd "$python_project" - run_fix_docstrings || run_fix_docstrings run_autoflake || run_autoflake popd fi diff --git a/poetry.lock b/poetry.lock index 118134c7f..3608303e0 100644 --- a/poetry.lock +++ b/poetry.lock @@ -614,7 +614,7 @@ werkzeug = "*" type = "git" url = "https://github.com/sartography/flask-bpmn" reference = "main" -resolved_reference = "c79c1e0b6d34ec05d82cce888b5e57b33d24403b" +resolved_reference = "c18306300f4312b8d36e0197fd6b62399180d0b1" [[package]] name = "flask-cors" @@ -1760,7 +1760,7 @@ lxml = "*" type = "git" url = "https://github.com/sartography/SpiffWorkflow" reference = "main" -resolved_reference = "80640024a8030481645f0c34f34c57e88f7b4f0c" +resolved_reference = "1f51db962ccaed5810f5d0f7d76a932f056430ab" [[package]] name = "sqlalchemy" diff --git a/spiffworkflow-backend/.flake8 b/spiffworkflow-backend/.flake8 index 1cc09c979..481ae8d32 100644 --- a/spiffworkflow-backend/.flake8 +++ b/spiffworkflow-backend/.flake8 @@ -8,8 +8,11 @@ rst-roles = class,const,func,meth,mod,ref rst-directives = deprecated per-file-ignores = - # prefer naming tests descriptively rather than forcing comments - tests/*:S101,D103 + # asserts are ok in tests + tests/*:S101 + + # prefer naming functions descriptively rather than forcing comments + *:D103 bin/keycloak_test_server.py:B950,D conftest.py:S105 diff --git a/spiffworkflow-backend/bin/get_token b/spiffworkflow-backend/bin/get_token index 1fcfbf0ae..4e6aca021 100755 --- a/spiffworkflow-backend/bin/get_token +++ b/spiffworkflow-backend/bin/get_token @@ -22,8 +22,8 @@ set -o errtrace -o errexit -o nounset -o pipefail # KEYCLOAK_BASE_URL=http://localhost:7002 KEYCLOAK_BASE_URL=https://keycloak.dev.spiffworkflow.org -# BACKEND_BASE_URL=http://localhost:7000 -BACKEND_BASE_URL=https://api.dev.spiffworkflow.org +BACKEND_BASE_URL=http://localhost:7000 +# BACKEND_BASE_URL=https://api.dev.spiffworkflow.org REALM_NAME=spiffworkflow USERNAME=${1-fin} PASSWORD=${2-fin} diff --git a/spiffworkflow-backend/bin/keycloak_test_secrets.json b/spiffworkflow-backend/bin/keycloak_test_secrets.json deleted file mode 100644 index 12dc3faa0..000000000 --- a/spiffworkflow-backend/bin/keycloak_test_secrets.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "web": { - "issuer": "http://localhost:8080/realms/finance", - "auth_uri": "http://localhost:8080/realms/finance/protocol/openid-connect/auth", - "client_id": "myclient", - "client_secret": "OAh6rkjXIiPJDtPOz4459i3VtdlxGcce", - "redirect_uris": ["http://localhost:5005/*"], - "userinfo_uri": "http://localhost:8080/realms/finance/protocol/openid-connect/userinfo", - "token_uri": "http://localhost:8080/realms/finance/protocol/openid-connect/token", - "token_introspection_uri": "http://localhost:8080/realms/finance/protocol/openid-connect/token/introspect" - } -} diff --git a/spiffworkflow-backend/bin/keycloak_test_server.py b/spiffworkflow-backend/bin/keycloak_test_server.py deleted file mode 100644 index 3e9334938..000000000 --- a/spiffworkflow-backend/bin/keycloak_test_server.py +++ /dev/null @@ -1,105 +0,0 @@ -# type: ignore -"""keycloak_test_server.""" -# ./bin/start_keycloak # starts keycloak on 8080 -# pip install flask_oidc -# pip install itsdangerous==2.0.1 -# python ./bin/keycloak_test_server.py # starts flask on 5005 -import json -import logging - -import requests -from flask import Flask -from flask import g -from flask_oidc import OpenIDConnect - -logging.basicConfig(level=logging.DEBUG) - -app = Flask(__name__) -app.config.update( - { - "SECRET_KEY": "SomethingNotEntirelySecret", - "TESTING": True, - "DEBUG": True, - "OIDC_CLIENT_SECRETS": "bin/keycloak_test_secrets.json", - "OIDC_ID_TOKEN_COOKIE_SECURE": False, - "OIDC_REQUIRE_VERIFIED_EMAIL": False, - "OIDC_USER_INFO_ENABLED": True, - "OIDC_OPENID_REALM": "flask-demo", - "OIDC_SCOPES": ["openid", "email", "profile"], - "OIDC_INTROSPECTION_AUTH_METHOD": "client_secret_post", - } -) - -oidc = OpenIDConnect(app) - - -@app.route("/") -def hello_world(): - """Hello_world.""" - if oidc.user_loggedin: - return ( - 'Hello, %s, See private ' - 'Log out' - % oidc.user_getfield("preferred_username") - ) - else: - return 'Welcome anonymous, Log in' - - -@app.route("/private") -@oidc.require_login -def hello_me(): - """Example for protected endpoint that extracts private information from the OpenID Connect id_token. - - Uses the accompanied access_token to access a backend service. - """ - info = oidc.user_getinfo(["preferred_username", "email", "sub"]) - - username = info.get("preferred_username") - email = info.get("email") - user_id = info.get("sub") - - if user_id in oidc.credentials_store: - try: - from oauth2client.client import OAuth2Credentials - - access_token = OAuth2Credentials.from_json( - oidc.credentials_store[user_id] - ).access_token - print("access_token=<%s>" % access_token) - headers = {"Authorization": "Bearer %s" % (access_token)} - # YOLO - greeting = requests.get( - "http://localhost:8080/greeting", headers=headers - ).text - except BaseException: - print("Could not access greeting-service") - greeting = "Hello %s" % username - - return """{} your email is {} and your user_id is {}! - """.format( - greeting, - email, - user_id, - ) - - -@app.route("/api", methods=["POST"]) -@oidc.accept_token(require_token=True, scopes_required=["openid"]) -def hello_api(): - """OAuth 2.0 protected API endpoint accessible via AccessToken.""" - return json.dumps({"hello": "Welcome %s" % g.oidc_token_info["sub"]}) - - -@app.route("/logout") -def logout(): - """Performs local logout by removing the session cookie.""" - oidc.logout() - return 'Hi, you have been logged out! Return' - - -if __name__ == "__main__": - app.run(port=5005) diff --git a/spiffworkflow-backend/bin/run_server_locally b/spiffworkflow-backend/bin/run_server_locally index 3e9f712b6..0fa63d218 100755 --- a/spiffworkflow-backend/bin/run_server_locally +++ b/spiffworkflow-backend/bin/run_server_locally @@ -29,7 +29,13 @@ else export FLASK_DEBUG=1 if [[ "${SPIFFWORKFLOW_BACKEND_RUN_DATA_SETUP:-}" != "false" ]]; then - SPIFFWORKFLOW_BACKEND_FAIL_ON_INVALID_PROCESS_MODELS=false poetry run python bin/save_all_bpmn.py + RUN_BACKGROUND_SCHEDULER=false SPIFFWORKFLOW_BACKEND_FAIL_ON_INVALID_PROCESS_MODELS=false poetry run python bin/save_all_bpmn.py fi - FLASK_APP=src/spiffworkflow_backend poetry run flask run -p 7000 + + if [[ -z "${RUN_BACKGROUND_SCHEDULER:-}" ]]; then + RUN_BACKGROUND_SCHEDULER=true + fi + + # this line blocks + RUN_BACKGROUND_SCHEDULER="${RUN_BACKGROUND_SCHEDULER}" FLASK_APP=src/spiffworkflow_backend poetry run flask run -p 7000 fi diff --git a/spiffworkflow-backend/bin/test_file_upload b/spiffworkflow-backend/bin/test_file_upload deleted file mode 100644 index b0e660bff..000000000 --- a/spiffworkflow-backend/bin/test_file_upload +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env bash - -function error_handler() { - >&2 echo "Exited with BAD EXIT CODE '${2}' in ${0} script at line: ${1}." - exit "$2" -} -trap 'error_handler ${LINENO} $?' ERR -set -o errtrace -o errexit -o nounset -o pipefail - -curl -v -F key1=value1 -F upload=@localfilename URL diff --git a/spiffworkflow-backend/bin/test_with_curl b/spiffworkflow-backend/bin/test_with_curl deleted file mode 100755 index c766d8867..000000000 --- a/spiffworkflow-backend/bin/test_with_curl +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env bash - -function error_handler() { - >&2 echo "Exited with BAD EXIT CODE '${2}' in ${0} script at line: ${1}." - exit "$2" -} -trap 'error_handler ${LINENO} $?' ERR -set -o errtrace -o errexit -o nounset -o pipefail - -if [[ "${1:-}" == "c" ]]; then - curl --fail localhost:5000/run_process -H "Content-type: application/json" -X POST -d '{}' -elif grep -qE '^[0-9]$' <<<"${1:-}" ; then - curl --fail localhost:5000/run_process -H "Content-type: application/json" -X POST -d "{ \"task_identifier\": \"${1}\"}" -else - ./bin/recreate_db clean - curl --silent --fail localhost:5000/run_process -H "Content-type: application/json" -X POST -d '{ "task_identifier": "1", "answer": {"Product Name": "G", "Quantity": "2"}}' | jq . - curl --silent --fail localhost:5000/run_process -H "Content-type: application/json" -X POST -d '{ "task_identifier": "1", "answer": {"Sleeve Type": "Short"}}' | jq . - curl --silent --fail localhost:5000/run_process -H "Content-type: application/json" -X POST -d '{ "task_identifier": "1", "answer": {"Continue shopping?": "N"}}' | jq . - curl --silent --fail localhost:5000/run_process -H "Content-type: application/json" -X POST -d '{ "task_identifier": "1", "answer": {"Shipping Method": "Overnight"}}' | jq . - curl --silent --fail localhost:5000/run_process -H "Content-type: application/json" -X POST -d '{ "task_identifier": "1", "answer": {"Shipping Address": "Somewhere"}}' | jq . - curl --silent --fail localhost:5000/run_process -H "Content-type: application/json" -X POST -d '{ "task_identifier": "1", "answer": {"Place Order": "Y"}}' | jq . - curl --silent --fail localhost:5000/run_process -H "Content-type: application/json" -X POST -d '{ "task_identifier": "1", "answer": {"Card Number": "MY_CARD"}}' | jq . - curl --silent --fail localhost:5000/run_process -H "Content-type: application/json" -X POST -d '{ "task_identifier": "2", "answer": {"Was the customer charged?": "Y"}}' | jq . - curl --silent --fail localhost:5000/run_process -H "Content-type: application/json" -X POST -d '{ "task_identifier": "1", "answer": {"Was the product available?": "Y"}}' | jq . - curl --silent --fail localhost:5000/run_process -H "Content-type: application/json" -X POST -d '{ "task_identifier": "1", "answer": {"Was the order shipped?": "Y"}}' | jq . -fi diff --git a/spiffworkflow-backend/keycloak/bin/add_test_users_to_keycloak b/spiffworkflow-backend/keycloak/bin/add_test_users_to_keycloak new file mode 100755 index 000000000..f7cdc6d8c --- /dev/null +++ b/spiffworkflow-backend/keycloak/bin/add_test_users_to_keycloak @@ -0,0 +1,50 @@ +#!/usr/bin/env bash + +function error_handler() { + >&2 echo "Exited with BAD EXIT CODE '${2}' in ${0} script at line: ${1}." + exit "$2" +} +trap 'error_handler ${LINENO} $?' ERR +set -o errtrace -o errexit -o nounset -o pipefail + +user_file_with_one_email_per_line="${1:-}" +if [[ -z "${1:-}" ]]; then + >&2 echo "usage: $(basename "$0") [user_file_with_one_email_per_line]" + exit 1 +fi + +KEYCLOAK_BASE_URL=http://localhost:7002 +REALM_NAME=master +ADMIN_USERNAME="admin" +ADMIN_PASSWORD="admin" +SECURE=false + +KEYCLOAK_URL=$KEYCLOAK_BASE_URL/realms/$REALM_NAME/protocol/openid-connect/token + +if [[ $SECURE = 'y' ]]; then + INSECURE= +else + INSECURE=--insecure +fi + +# https://www.appsdeveloperblog.com/keycloak-rest-api-create-a-new-user/ +result=$(curl --fail -s -X POST "$KEYCLOAK_URL" "$INSECURE" \ + --header 'Content-Type: application/x-www-form-urlencoded' \ + --data-urlencode "username=${ADMIN_USERNAME}" \ + --data-urlencode "password=${ADMIN_PASSWORD}" \ + --data-urlencode 'grant_type=password' \ + --data-urlencode 'client_id=admin-cli' +) +backend_token=$(jq -r '.access_token' <<< "$result") + +while read -r user_email; do + if [[ -n "$user_email" ]]; then + username=$(awk -F '@' '{print $1}' <<<"$user_email") + credentials='{"type":"password","value":"'"${username}"'","temporary":false}' + + curl --fail --location --request POST 'http://localhost:7002/admin/realms/spiffworkflow/users' \ + -H 'Content-Type: application/json' \ + -H "Authorization: Bearer $backend_token" \ + --data-raw '{"email":"'"${user_email}"'", "enabled":"true", "username":"'"${username}"'", "credentials":['"${credentials}"']}' + fi +done <"$user_file_with_one_email_per_line" diff --git a/spiffworkflow-backend/bin/export_keycloak_realms b/spiffworkflow-backend/keycloak/bin/export_keycloak_realms similarity index 82% rename from spiffworkflow-backend/bin/export_keycloak_realms rename to spiffworkflow-backend/keycloak/bin/export_keycloak_realms index 97eafc8e7..f205d0d7d 100755 --- a/spiffworkflow-backend/bin/export_keycloak_realms +++ b/spiffworkflow-backend/keycloak/bin/export_keycloak_realms @@ -7,6 +7,8 @@ function error_handler() { trap 'error_handler ${LINENO} $?' ERR set -o errtrace -o errexit -o nounset -o pipefail +script_dir="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" + realms="$*" if [[ -z "$realms" ]]; then realms="spiffworkflow-realm" @@ -19,7 +21,7 @@ docker exec keycloak /opt/keycloak/bin/kc.sh export --dir "${docker_container_pa docker cp "keycloak:${docker_container_path}" "$local_tmp_dir" for realm in $realms ; do - cp "${local_tmp_dir}/hey/${realm}.json" bin/ + cp "${local_tmp_dir}/hey/${realm}.json" "${script_dir}/../realm_exports/" done rm -rf "$local_tmp_dir" diff --git a/spiffworkflow-backend/bin/start_keycloak b/spiffworkflow-backend/keycloak/bin/start_keycloak similarity index 98% rename from spiffworkflow-backend/bin/start_keycloak rename to spiffworkflow-backend/keycloak/bin/start_keycloak index a44c0f511..de78efad1 100755 --- a/spiffworkflow-backend/bin/start_keycloak +++ b/spiffworkflow-backend/keycloak/bin/start_keycloak @@ -45,7 +45,7 @@ docker run \ -Dkeycloak.profile.feature.admin_fine_grained_authz=enabled script_dir="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" -cp "${script_dir}/spiffworkflow-realm.json" /tmp/spiffworkflow-realm.json +cp "${script_dir}/../realm_exports/spiffworkflow-realm.json" /tmp/spiffworkflow-realm.json spiff_subdomain="unused-for-local-dev" perl -pi -e "s/{{SPIFF_SUBDOMAIN}}/${spiff_subdomain}/g" /tmp/spiffworkflow-realm.json docker cp /tmp/spiffworkflow-realm.json keycloak:/tmp diff --git a/spiffworkflow-backend/bin/wait_for_keycloak b/spiffworkflow-backend/keycloak/bin/wait_for_keycloak similarity index 100% rename from spiffworkflow-backend/bin/wait_for_keycloak rename to spiffworkflow-backend/keycloak/bin/wait_for_keycloak diff --git a/spiffworkflow-backend/bin/finance-realm.json b/spiffworkflow-backend/keycloak/realm_exports/finance-realm.json similarity index 100% rename from spiffworkflow-backend/bin/finance-realm.json rename to spiffworkflow-backend/keycloak/realm_exports/finance-realm.json diff --git a/spiffworkflow-backend/bin/quarkus-realm.json b/spiffworkflow-backend/keycloak/realm_exports/quarkus-realm.json similarity index 100% rename from spiffworkflow-backend/bin/quarkus-realm.json rename to spiffworkflow-backend/keycloak/realm_exports/quarkus-realm.json diff --git a/spiffworkflow-backend/bin/spiffworkflow-realm.json b/spiffworkflow-backend/keycloak/realm_exports/spiffworkflow-realm.json similarity index 92% rename from spiffworkflow-backend/bin/spiffworkflow-realm.json rename to spiffworkflow-backend/keycloak/realm_exports/spiffworkflow-realm.json index e31942cf1..270e4876f 100644 --- a/spiffworkflow-backend/bin/spiffworkflow-realm.json +++ b/spiffworkflow-backend/keycloak/realm_exports/spiffworkflow-realm.json @@ -634,6 +634,46 @@ "realmRoles" : [ "default-roles-spiffworkflow" ], "notBefore" : 0, "groups" : [ ] + }, { + "id" : "29ba295e-9a70-41f1-bf0d-f02b468397c5", + "createdTimestamp" : 1674148694595, + "username" : "finance.lead", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "email" : "finance.lead@status.im", + "credentials" : [ { + "id" : "8f746fde-0a10-41b4-a973-0b967de73839", + "type" : "password", + "createdDate" : 1674148694661, + "secretData" : "{\"value\":\"vhe8ONTdkYaXLcSr73/4Ey//7U7rxh/0hiGc9S0wp8FV3EUsf+3bQSreDQCTp3DePJInpVCV34d4T0Ij+6Po0A==\",\"salt\":\"s6hEEdUPlULWfqGpxlG+TQ==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "default-roles-spiffworkflow" ], + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "f6d2488a-446c-493b-bbe8-210ede6f3e42", + "createdTimestamp" : 1674148694899, + "username" : "finance.sme", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "email" : "finance.sme@status.im", + "credentials" : [ { + "id" : "faee8eaa-0bf4-4050-8d17-8b6b52f0b7ee", + "type" : "password", + "createdDate" : 1674148694945, + "secretData" : "{\"value\":\"tk78HqSoRT0PAJ45zt2/q6gXRYxvDDIYtLzsVdYM3sHk+tRkgYeXoyKDSyRwHm9AjbM8jFI5yUXPsWck8vemOg==\",\"salt\":\"aR9qgYMx1VUfOrppTDzMmQ==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "default-roles-spiffworkflow" ], + "notBefore" : 0, + "groups" : [ ] }, { "id" : "9b46f3be-a81d-4b76-92e6-2ac8462f5ec8", "createdTimestamp" : 1665688255982, @@ -674,6 +714,26 @@ "realmRoles" : [ "default-roles-spiffworkflow" ], "notBefore" : 0, "groups" : [ ] + }, { + "id" : "f55135de-7341-459d-8a42-a59f52d05bed", + "createdTimestamp" : 1674148694958, + "username" : "infra.sme", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "email" : "infra.sme@status.im", + "credentials" : [ { + "id" : "e1f4368c-ed7c-481c-9426-fc0b8f2bf520", + "type" : "password", + "createdDate" : 1674148695008, + "secretData" : "{\"value\":\"7RHwvrhGAA3EddNNjPaVah+EOg5be0eugiwLLQLGlhFGSdGfg6kiUmPr5wBqBabivXHiSZgv/BiaL5KQ/VmR+A==\",\"salt\":\"HW3yCxErwpKASPvHX8o9Uw==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "default-roles-spiffworkflow" ], + "notBefore" : 0, + "groups" : [ ] }, { "id" : "1561518b-c327-491e-9db3-23c2b5394104", "createdTimestamp" : 1669303773974, @@ -843,6 +903,46 @@ "realmRoles" : [ "default-roles-spiffworkflow" ], "notBefore" : 0, "groups" : [ ] + }, { + "id" : "530e99cb-b400-4baf-8ca6-22e64a30ef84", + "createdTimestamp" : 1674148694688, + "username" : "legal.lead", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "email" : "legal.lead@status.im", + "credentials" : [ { + "id" : "81f3aeca-8316-4a1b-8eb9-2570c062d0df", + "type" : "password", + "createdDate" : 1674148694733, + "secretData" : "{\"value\":\"puCrVcCNrO6P0VF8w0ZSx97RHi/c6NCuSeTidk/tEfSpZyY9x0oz/bkdFJO359HuvhN5HMBQ+CKPNbW1VjOSoA==\",\"salt\":\"ZczpeV+0QJGZG96EfLWYRQ==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "default-roles-spiffworkflow" ], + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "2a3176a0-8dd5-4223-a3e1-3cac4134e474", + "createdTimestamp" : 1674148695030, + "username" : "legal.sme", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "email" : "legal.sme@status.im", + "credentials" : [ { + "id" : "52fd8bd4-8fc4-4b71-8325-424220ef83af", + "type" : "password", + "createdDate" : 1674148695076, + "secretData" : "{\"value\":\"Rce1M5ph1ITsCguiHlv7YMcDTyofRnSPnOraQskkmeojV+tlUeBBsHV1fTiqJ4f13vE1qtnwC/60vQV8BprsHw==\",\"salt\":\"zFyJq5G2F/pZeLmgKaGoxQ==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "default-roles-spiffworkflow" ], + "notBefore" : 0, + "groups" : [ ] }, { "id" : "6f5bfa09-7494-4a2f-b871-cf327048cac7", "createdTimestamp" : 1665517010600, @@ -905,6 +1005,26 @@ "realmRoles" : [ "default-roles-spiffworkflow" ], "notBefore" : 0, "groups" : [ ] + }, { + "id" : "c3ea06ee-c497-48e6-8816-43c8ef68bd8b", + "createdTimestamp" : 1674148694747, + "username" : "program.lead", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "email" : "program.lead@status.im", + "credentials" : [ { + "id" : "393e3cd9-c403-41dd-8562-7edba6acedd3", + "type" : "password", + "createdDate" : 1674148694793, + "secretData" : "{\"value\":\"AD/rFDJcnQNVSZLVnLl6FzdiMSkRFiKiF2L6jyPtnAOAuQ6IivNvDIqiZf98rPuSq1zs8wjeDzFzyXvTYp7Pjg==\",\"salt\":\"T4XlF58M6LNTX8ksxYq8jQ==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "default-roles-spiffworkflow" ], + "notBefore" : 0, + "groups" : [ ] }, { "id" : "f3852a7d-8adf-494f-b39d-96ad4c899ee5", "createdTimestamp" : 1665516926300, @@ -925,6 +1045,26 @@ "realmRoles" : [ "default-roles-spiffworkflow" ], "notBefore" : 0, "groups" : [ ] + }, { + "id" : "74374cda-1516-48e5-9ef2-1fd7bcee84d3", + "createdTimestamp" : 1674148695088, + "username" : "security.sme", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "email" : "security.sme@status.im", + "credentials" : [ { + "id" : "43427e80-292e-453f-9968-511a1064729e", + "type" : "password", + "createdDate" : 1674148695133, + "secretData" : "{\"value\":\"HB68S1rm/fef2nY2qpakAyZ0a+OFM0G/Xp+kHNdTQSWZA6fYq8EUzhfTFkUQ5xuTriOesXao0srtFmcCs2Pi8Q==\",\"salt\":\"e8J1O8M7mrDq/jTJXzwYyQ==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "default-roles-spiffworkflow" ], + "notBefore" : 0, + "groups" : [ ] }, { "id" : "487d3a85-89dd-4839-957a-c3f6d70551f6", "createdTimestamp" : 1657115173081, @@ -961,6 +1101,26 @@ }, "notBefore" : 0, "groups" : [ ] + }, { + "id" : "3d45bb85-0a2d-4b15-8a19-d26a5619d359", + "createdTimestamp" : 1674148694810, + "username" : "services.lead", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "email" : "services.lead@status.im", + "credentials" : [ { + "id" : "45607c53-3768-4f76-bda3-4d31b39ffccd", + "type" : "password", + "createdDate" : 1674148694884, + "secretData" : "{\"value\":\"E3GPcOLU56efhBQE7MMZa0OM0FAtgK5kDA9sy65uCwSyaoZGp4ZVUDsIfIkWe+TEEQA5QP5FVJbJhwvdkx3m9w==\",\"salt\":\"dySpiEZxeyb11oQZR2WYVQ==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "default-roles-spiffworkflow" ], + "notBefore" : 0, + "groups" : [ ] } ], "scopeMappings" : [ { "clientScope" : "offline_access", @@ -2174,7 +2334,7 @@ "subType" : "authenticated", "subComponents" : { }, "config" : { - "allowed-protocol-mapper-types" : [ "saml-user-property-mapper", "oidc-full-name-mapper", "saml-user-attribute-mapper", "oidc-usermodel-property-mapper", "oidc-address-mapper", "oidc-sha256-pairwise-sub-mapper", "oidc-usermodel-attribute-mapper", "saml-role-list-mapper" ] + "allowed-protocol-mapper-types" : [ "oidc-full-name-mapper", "oidc-usermodel-property-mapper", "saml-user-attribute-mapper", "oidc-usermodel-attribute-mapper", "saml-role-list-mapper", "saml-user-property-mapper", "oidc-address-mapper", "oidc-sha256-pairwise-sub-mapper" ] } }, { "id" : "d68e938d-dde6-47d9-bdc8-8e8523eb08cd", @@ -2192,7 +2352,7 @@ "subType" : "anonymous", "subComponents" : { }, "config" : { - "allowed-protocol-mapper-types" : [ "oidc-sha256-pairwise-sub-mapper", "oidc-full-name-mapper", "saml-user-property-mapper", "oidc-usermodel-property-mapper", "oidc-usermodel-attribute-mapper", "saml-role-list-mapper", "oidc-address-mapper", "saml-user-attribute-mapper" ] + "allowed-protocol-mapper-types" : [ "saml-user-property-mapper", "oidc-usermodel-property-mapper", "oidc-sha256-pairwise-sub-mapper", "oidc-usermodel-attribute-mapper", "oidc-full-name-mapper", "saml-role-list-mapper", "oidc-address-mapper", "saml-user-attribute-mapper" ] } }, { "id" : "3854361d-3fe5-47fb-9417-a99592e3dc5c", @@ -2282,7 +2442,7 @@ "internationalizationEnabled" : false, "supportedLocales" : [ ], "authenticationFlows" : [ { - "id" : "76ae522e-7ab3-48dc-af76-9cb8069368a2", + "id" : "fd44ea2b-052b-470a-9afd-216390c40d54", "alias" : "Account verification options", "description" : "Method with which to verity the existing account", "providerId" : "basic-flow", @@ -2304,7 +2464,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "ddf80243-ec40-4c21-ae94-2967d841f84c", + "id" : "88a96abb-a839-4405-97bf-fa53f5290482", "alias" : "Authentication Options", "description" : "Authentication options.", "providerId" : "basic-flow", @@ -2333,7 +2493,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "4f075680-46b7-49eb-b94c-d7425f105cb9", + "id" : "cbe05604-280f-4304-bda5-ed5245537f4d", "alias" : "Browser - Conditional OTP", "description" : "Flow to determine if the OTP is required for the authentication", "providerId" : "basic-flow", @@ -2355,7 +2515,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "a0467c77-c3dc-4df6-acd2-c05ca13601ed", + "id" : "5275913f-e597-4a89-b416-4f9412b9082b", "alias" : "Direct Grant - Conditional OTP", "description" : "Flow to determine if the OTP is required for the authentication", "providerId" : "basic-flow", @@ -2377,7 +2537,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "07536fec-8d41-4c73-845f-ca85002022e0", + "id" : "a0afd432-ed89-41c6-be8d-f31834e80ba1", "alias" : "First broker login - Conditional OTP", "description" : "Flow to determine if the OTP is required for the authentication", "providerId" : "basic-flow", @@ -2399,7 +2559,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "f123f912-71fb-4596-97f9-c0628a59413d", + "id" : "fab45b23-3353-4482-b690-07f3ab177776", "alias" : "Handle Existing Account", "description" : "Handle what to do if there is existing account with same email/username like authenticated identity provider", "providerId" : "basic-flow", @@ -2421,7 +2581,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "03c26cc5-366b-462d-9297-b4016f8d7c57", + "id" : "f5eb0757-f2cd-4d4b-9608-d1b9ae4fd941", "alias" : "Reset - Conditional OTP", "description" : "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", "providerId" : "basic-flow", @@ -2443,7 +2603,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "1b4f474e-aa64-45cc-90f1-63504585d89c", + "id" : "521586b9-ade0-4f8c-aff6-3d6c357aa6e4", "alias" : "User creation or linking", "description" : "Flow for the existing/non-existing user alternatives", "providerId" : "basic-flow", @@ -2466,7 +2626,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "38024dd6-daff-45de-8782-06b07b7bfa56", + "id" : "b21bb98a-9241-4484-966b-6f8294ba2186", "alias" : "Verify Existing Account by Re-authentication", "description" : "Reauthentication of existing account", "providerId" : "basic-flow", @@ -2488,7 +2648,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "b7e30fca-e4ac-4886-a2e7-642fe2a27ee7", + "id" : "7ec2a1f6-37e7-444e-9376-dee7d442ec2f", "alias" : "browser", "description" : "browser based authentication", "providerId" : "basic-flow", @@ -2524,7 +2684,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "92e3571d-ac3e-4e79-a391-5315954e866f", + "id" : "1bc2b251-bf69-40b1-ace2-e3be5037b910", "alias" : "clients", "description" : "Base authentication for clients", "providerId" : "client-flow", @@ -2560,7 +2720,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "5093dd2d-fe5d-4f41-a54d-03cd648d9b7f", + "id" : "12a854bd-4d8a-49eb-8be5-cfc9d25cba54", "alias" : "direct grant", "description" : "OpenID Connect Resource Owner Grant", "providerId" : "basic-flow", @@ -2589,7 +2749,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "95d2f1ff-6907-47ce-a93c-db462fe04844", + "id" : "99ebf3a7-674e-4603-a0cf-8fe4c6dd4cfc", "alias" : "docker auth", "description" : "Used by Docker clients to authenticate against the IDP", "providerId" : "basic-flow", @@ -2604,7 +2764,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "27405ee8-5730-419c-944c-a7c67edd91ce", + "id" : "a241b9b8-9c21-4a47-877a-5a6535678c90", "alias" : "first broker login", "description" : "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", "providerId" : "basic-flow", @@ -2627,7 +2787,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "fce6d926-3a99-40ee-b79e-cae84493dbd8", + "id" : "c9df7ad1-9b59-46ec-a85e-714fd682569c", "alias" : "forms", "description" : "Username, password, otp and other auth forms.", "providerId" : "basic-flow", @@ -2649,7 +2809,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "75d93596-b7fb-4a2c-a780-e6a038e66fe9", + "id" : "14f21f85-2bcb-4ed6-aaab-1ee237da153f", "alias" : "http challenge", "description" : "An authentication flow based on challenge-response HTTP Authentication Schemes", "providerId" : "basic-flow", @@ -2671,7 +2831,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "04cdc1ac-c58d-4f8c-bc10-7d5e2bb99485", + "id" : "bc7e40c0-9172-496b-8db1-3ebc20065887", "alias" : "registration", "description" : "registration flow", "providerId" : "basic-flow", @@ -2687,7 +2847,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "99593c1e-f2a5-4198-ad41-634694259110", + "id" : "ef97f42b-7f32-442c-ab4a-8cb6c873cf1f", "alias" : "registration form", "description" : "registration form", "providerId" : "form-flow", @@ -2723,7 +2883,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "7d53f026-b05e-4a9c-aba6-23b17826a4d4", + "id" : "1ee2b484-3836-466f-9f5b-bbf47abc5ad7", "alias" : "reset credentials", "description" : "Reset credentials for a user if they forgot their password or something", "providerId" : "basic-flow", @@ -2759,7 +2919,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "7ca17e64-f916-4d6c-91f0-815ec66f50e8", + "id" : "4918f32e-6780-4ddd-a1a2-c3ae9d8fa598", "alias" : "saml ecp", "description" : "SAML ECP Profile Authentication Flow", "providerId" : "basic-flow", @@ -2775,13 +2935,13 @@ } ] } ], "authenticatorConfig" : [ { - "id" : "9b71d817-b999-479d-97f8-07e39dd9e9fa", + "id" : "5479944f-6198-48df-8a18-4bc0caba5963", "alias" : "create unique user config", "config" : { "require.password.update.after.registration" : "false" } }, { - "id" : "f9f13ba1-6a17-436b-a80b-6ccc042f9fc2", + "id" : "fd9f571f-0d6e-4ece-a3e5-fffccc1e4fad", "alias" : "review profile config", "config" : { "update.profile.on.first.login" : "missing" @@ -2876,4 +3036,4 @@ "clientPolicies" : { "policies" : [ ] } -} +} \ No newline at end of file diff --git a/spiffworkflow-backend/bin/replicate_resource_set_denied_based_on_uri_with_keycloak/replicate_resource_set_denied_based_on_uri b/spiffworkflow-backend/keycloak/replicate_resource_set_denied_based_on_uri_with_keycloak/replicate_resource_set_denied_based_on_uri similarity index 100% rename from spiffworkflow-backend/bin/replicate_resource_set_denied_based_on_uri_with_keycloak/replicate_resource_set_denied_based_on_uri rename to spiffworkflow-backend/keycloak/replicate_resource_set_denied_based_on_uri_with_keycloak/replicate_resource_set_denied_based_on_uri diff --git a/spiffworkflow-backend/bin/replicate_resource_set_denied_based_on_uri_with_keycloak/testing-realm.json b/spiffworkflow-backend/keycloak/replicate_resource_set_denied_based_on_uri_with_keycloak/testing-realm.json similarity index 100% rename from spiffworkflow-backend/bin/replicate_resource_set_denied_based_on_uri_with_keycloak/testing-realm.json rename to spiffworkflow-backend/keycloak/replicate_resource_set_denied_based_on_uri_with_keycloak/testing-realm.json diff --git a/spiffworkflow-backend/keycloak/test_user_lists/status b/spiffworkflow-backend/keycloak/test_user_lists/status new file mode 100644 index 000000000..e9c518374 --- /dev/null +++ b/spiffworkflow-backend/keycloak/test_user_lists/status @@ -0,0 +1,9 @@ +finance.lead@status.im +legal.lead@status.im +program.lead@status.im +services.lead@status.im +finance.sme@status.im +infra.sme@status.im +legal.sme@status.im +security.sme@status.im + diff --git a/spiffworkflow-backend/poetry.lock b/spiffworkflow-backend/poetry.lock index 433abf204..b32b4247e 100644 --- a/spiffworkflow-backend/poetry.lock +++ b/spiffworkflow-backend/poetry.lock @@ -248,7 +248,7 @@ zstd = ["zstandard"] [[package]] name = "certifi" -version = "2022.9.24" +version = "2022.12.7" description = "Python package for providing Mozilla's CA Bundle." category = "main" optional = false @@ -610,6 +610,37 @@ python-versions = "*" bcrypt = ">=3.1.1" Flask = "*" +[[package]] +name = "flask-bpmn" +version = "0.0.0" +description = "Flask Bpmn" +category = "main" +optional = false +python-versions = "^3.7" +develop = false + +[package.dependencies] +click = "^8.0.1" +flask = "*" +flask-admin = "*" +flask-bcrypt = "*" +flask-cors = "*" +flask-mail = "*" +flask-marshmallow = "*" +flask-migrate = "*" +flask-restful = "*" +greenlet = "^2.0.1" +sentry-sdk = "*" +sphinx-autoapi = "^2.0.0" +spiffworkflow = "*" +werkzeug = "*" + +[package.source] +type = "git" +url = "https://github.com/sartography/flask-bpmn" +reference = "main" +resolved_reference = "c18306300f4312b8d36e0197fd6b62399180d0b1" + [[package]] name = "Flask-Cors" version = "3.0.10" @@ -1043,19 +1074,19 @@ python-versions = "*" [[package]] name = "mysql-connector-python" -version = "8.0.31" +version = "8.0.32" description = "MySQL driver written in Python" category = "main" optional = false python-versions = "*" [package.dependencies] -protobuf = ">=3.11.0,<=3.20.1" +protobuf = ">=3.11.0,<=3.20.3" [package.extras] -compression = ["lz4 (>=2.1.6,<=3.1.3)", "zstandard (>=0.12.0,<=0.15.2)"] +compression = ["lz4 (>=2.1.6,<=3.1.3)", "zstandard (>=0.12.0,<=0.19.0)"] dns-srv = ["dnspython (>=1.16.0,<=2.1.0)"] -gssapi = ["gssapi (>=1.6.9,<=1.8.1)"] +gssapi = ["gssapi (>=1.6.9,<=1.8.2)"] [[package]] name = "nodeenv" @@ -1179,7 +1210,7 @@ wcwidth = "*" [[package]] name = "protobuf" -version = "3.20.1" +version = "3.20.3" description = "Protocol Buffers" category = "main" optional = false @@ -1745,7 +1776,7 @@ description = "A workflow framework and BPMN/DMN Processor" category = "main" optional = false python-versions = "*" -develop = true +develop = false [package.dependencies] celery = "*" @@ -1753,8 +1784,10 @@ configparser = "*" lxml = "*" [package.source] -type = "directory" -url = "../../SpiffWorkflow" +type = "git" +url = "https://github.com/sartography/SpiffWorkflow" +reference = "be26100bcbef8026e26312c665dae42faf476485" +resolved_reference = "be26100bcbef8026e26312c665dae42faf476485" [[package]] name = "SQLAlchemy" @@ -2125,7 +2158,7 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools" [metadata] lock-version = "1.1" python-versions = ">=3.9,<3.12" -content-hash = "f646836512a18f2a0e9ba4979926e004abd17790da5178e06e87092fdf23c35f" +content-hash = "d804b8cbb34882f92cf19e5e59231aa7eac84764298fe7eae72bd03112e09496" [metadata.files] alabaster = [ @@ -2227,8 +2260,8 @@ celery = [ {file = "celery-5.2.7.tar.gz", hash = "sha256:fafbd82934d30f8a004f81e8f7a062e31413a23d444be8ee3326553915958c6d"}, ] certifi = [ - {file = "certifi-2022.9.24-py3-none-any.whl", hash = "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382"}, - {file = "certifi-2022.9.24.tar.gz", hash = "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14"}, + {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"}, + {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"}, ] cfgv = [ {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, @@ -2388,6 +2421,7 @@ Flask-Bcrypt = [ {file = "Flask-Bcrypt-1.0.1.tar.gz", hash = "sha256:f07b66b811417ea64eb188ae6455b0b708a793d966e1a80ceec4a23bc42a4369"}, {file = "Flask_Bcrypt-1.0.1-py3-none-any.whl", hash = "sha256:062fd991dc9118d05ac0583675507b9fe4670e44416c97e0e6819d03d01f808a"}, ] +flask-bpmn = [] Flask-Cors = [ {file = "Flask-Cors-3.0.10.tar.gz", hash = "sha256:b60839393f3b84a0f3746f6cdca56c1ad7426aa738b70d6c61375857823181de"}, {file = "Flask_Cors-3.0.10-py2.py3-none-any.whl", hash = "sha256:74efc975af1194fc7891ff5cd85b0f7478be4f7f59fe158102e91abb72bb4438"}, @@ -2465,7 +2499,6 @@ greenlet = [ {file = "greenlet-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5b0ff9878333823226d270417f24f4d06f235cb3e54d1103b71ea537a6a86ce"}, {file = "greenlet-2.0.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be9e0fb2ada7e5124f5282d6381903183ecc73ea019568d6d63d33f25b2a9000"}, {file = "greenlet-2.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b493db84d124805865adc587532ebad30efa68f79ad68f11b336e0a51ec86c2"}, - {file = "greenlet-2.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0459d94f73265744fee4c2d5ec44c6f34aa8a31017e6e9de770f7bcf29710be9"}, {file = "greenlet-2.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a20d33124935d27b80e6fdacbd34205732660e0a1d35d8b10b3328179a2b51a1"}, {file = "greenlet-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:ea688d11707d30e212e0110a1aac7f7f3f542a259235d396f88be68b649e47d1"}, {file = "greenlet-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:afe07421c969e259e9403c3bb658968702bc3b78ec0b6fde3ae1e73440529c23"}, @@ -2474,7 +2507,6 @@ greenlet = [ {file = "greenlet-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:659f167f419a4609bc0516fb18ea69ed39dbb25594934bd2dd4d0401660e8a1e"}, {file = "greenlet-2.0.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:356e4519d4dfa766d50ecc498544b44c0249b6de66426041d7f8b751de4d6b48"}, {file = "greenlet-2.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:811e1d37d60b47cb8126e0a929b58c046251f28117cb16fcd371eed61f66b764"}, - {file = "greenlet-2.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d38ffd0e81ba8ef347d2be0772e899c289b59ff150ebbbbe05dc61b1246eb4e0"}, {file = "greenlet-2.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0109af1138afbfb8ae647e31a2b1ab030f58b21dd8528c27beaeb0093b7938a9"}, {file = "greenlet-2.0.1-cp38-cp38-win32.whl", hash = "sha256:88c8d517e78acdf7df8a2134a3c4b964415b575d2840a2746ddb1cc6175f8608"}, {file = "greenlet-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:d6ee1aa7ab36475035eb48c01efae87d37936a8173fc4d7b10bb02c2d75dd8f6"}, @@ -2483,7 +2515,6 @@ greenlet = [ {file = "greenlet-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:505138d4fa69462447a562a7c2ef723c6025ba12ac04478bc1ce2fcc279a2db5"}, {file = "greenlet-2.0.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cce1e90dd302f45716a7715517c6aa0468af0bf38e814ad4eab58e88fc09f7f7"}, {file = "greenlet-2.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e9744c657d896c7b580455e739899e492a4a452e2dd4d2b3e459f6b244a638d"}, - {file = "greenlet-2.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:662e8f7cad915ba75d8017b3e601afc01ef20deeeabf281bd00369de196d7726"}, {file = "greenlet-2.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:41b825d65f31e394b523c84db84f9383a2f7eefc13d987f308f4663794d2687e"}, {file = "greenlet-2.0.1-cp39-cp39-win32.whl", hash = "sha256:db38f80540083ea33bdab614a9d28bcec4b54daa5aff1668d7827a9fc769ae0a"}, {file = "greenlet-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:b23d2a46d53210b498e5b701a1913697671988f4bf8e10f935433f6e7c332fb6"}, @@ -2740,32 +2771,31 @@ mypy-extensions = [ {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, ] mysql-connector-python = [ - {file = "mysql-connector-python-8.0.31.tar.gz", hash = "sha256:0fbe8f5441ad781b4f65c54a10ac77c6a329591456607e042786528599519636"}, - {file = "mysql_connector_python-8.0.31-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3e271d8de00d5e9f9bd4b212c8e23d2986dead0f20379010f3b274a3e24cbfcb"}, - {file = "mysql_connector_python-8.0.31-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:f3ee04a601f9cb90ace9618bbe2fa8e5bb59be3eb0c2bd8a5405fe69e05e446b"}, - {file = "mysql_connector_python-8.0.31-cp310-cp310-manylinux1_i686.whl", hash = "sha256:f89b7a731885b8a04248e4d8d124705ca836f0ddd3b7cf0c789e21f4b32810ed"}, - {file = "mysql_connector_python-8.0.31-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:48eb34f4e69a2fba56f310de6682862a15d46cd2bd51ee6eebc3a244e4ee0aa6"}, - {file = "mysql_connector_python-8.0.31-cp310-cp310-win_amd64.whl", hash = "sha256:a570a72e0015b36b9c0775ae27c1d4946225f02f62129d16a14e9d77a38c0717"}, - {file = "mysql_connector_python-8.0.31-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a7ac859a52486ac319e37f61469bbb9023faef38018223efa74e953f1fe23d36"}, - {file = "mysql_connector_python-8.0.31-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:79d6a6e8ce955df5ca0786cb8ed8fbd999745c9b50def89993a2a0f4732de721"}, - {file = "mysql_connector_python-8.0.31-cp311-cp311-manylinux1_i686.whl", hash = "sha256:e60426af313dcd526028d018d70757a82c5cc0673776b2a614e2180b5970feed"}, - {file = "mysql_connector_python-8.0.31-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:d0ca1ba3e5fb2f2cddcf271c320cd5c368f8d392c034ddab7a1c8dfd19510351"}, - {file = "mysql_connector_python-8.0.31-cp311-cp311-win_amd64.whl", hash = "sha256:a1d8c1509c740649f352400d50360185e5473371507bb6498ceda0c6e877920c"}, - {file = "mysql_connector_python-8.0.31-cp37-cp37m-macosx_11_0_x86_64.whl", hash = "sha256:447847396d1b51edd9cfe05a8c5ba82836d8ea4866f25f36a836cab322fdc4f0"}, - {file = "mysql_connector_python-8.0.31-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:5e01a2f50378c13407a32e40dd4d225cfee5996d9d11968f76720ec28aa45421"}, - {file = "mysql_connector_python-8.0.31-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ac85883ec3b3a9a0e36cacc89b8f5e666206842c432a5f69b09a7687ddf51d4a"}, - {file = "mysql_connector_python-8.0.31-cp37-cp37m-win_amd64.whl", hash = "sha256:28cb3667be64ebfbd3d477bbd2c71e50d48bd5ed7ba2072dd460ae886d27e88e"}, - {file = "mysql_connector_python-8.0.31-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:30f4542d4d20357c79604e6bf1a801e71dfc45c759c22b502ca5aa8122c3e859"}, - {file = "mysql_connector_python-8.0.31-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:e9e5ad544adfc82ffbda2c74685c8c953bce2e212c56f117020079f05e2c68b2"}, - {file = "mysql_connector_python-8.0.31-cp38-cp38-manylinux1_i686.whl", hash = "sha256:744c976569e81eecce5e8c7e8f80df2a1c3f64414829addc69c64aef8f56d091"}, - {file = "mysql_connector_python-8.0.31-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:17d6ea22dacca7fa78a73a81f2b186d4c5c6e70b7be314e352526654e9ba4713"}, - {file = "mysql_connector_python-8.0.31-cp38-cp38-win_amd64.whl", hash = "sha256:ae1b3d03802474a161cce8a97024484d18bef43b86d20114908cbc263817cade"}, - {file = "mysql_connector_python-8.0.31-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:746df133c677fbe4687da33aad5a711abdd9bd2277bbc350e20f903f07c81ef5"}, - {file = "mysql_connector_python-8.0.31-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:4d75e6c3a7f18004e8279cbd9f5edc70089d6aaf3cb64374e21098d9bf0b93c4"}, - {file = "mysql_connector_python-8.0.31-cp39-cp39-manylinux1_i686.whl", hash = "sha256:8ad0d08f3f7c9e48d6d102c7de718e5e44f630f916ff2f4b4ff8a3756b5d10ac"}, - {file = "mysql_connector_python-8.0.31-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:02526f16eacc3961ff681c5c8455d2306a9b45124f2f012ca75a1eac9ceb5165"}, - {file = "mysql_connector_python-8.0.31-cp39-cp39-win_amd64.whl", hash = "sha256:b2bbf443f6346e46c26a3e91dd96a428a1038f2d3c5e466541078479c64a1833"}, - {file = "mysql_connector_python-8.0.31-py2.py3-none-any.whl", hash = "sha256:9be9c4dcae987a2a3f07b2ad984984c24f90887dbfab3c8a971e631ad4ca5ccf"}, + {file = "mysql-connector-python-8.0.32.tar.gz", hash = "sha256:c2d20b29fd096a0633f9360c275bd2434d4bcf597281991c4b7f1c820cd07b84"}, + {file = "mysql_connector_python-8.0.32-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:4df11c683924ef34c177a54887dc4844ae735b01c8a29ce6ab92d6d3db7a2757"}, + {file = "mysql_connector_python-8.0.32-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:4b2d00c9e2cb9e3d11c57ec411226f43aa627607085fbed661cfea1c4dc57f61"}, + {file = "mysql_connector_python-8.0.32-cp310-cp310-manylinux1_i686.whl", hash = "sha256:992b7a464daa398e86df8c75f7d8cd6044f884ff9087e782120fc8beff96c638"}, + {file = "mysql_connector_python-8.0.32-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:232095f0c36266510009b0f1214d2823a649efb8bc511dbab9ce8847f66ab08a"}, + {file = "mysql_connector_python-8.0.32-cp310-cp310-win_amd64.whl", hash = "sha256:fd233c83daaf048c1f9827be984c2721576ae0adf50e139429a06ccd094987d9"}, + {file = "mysql_connector_python-8.0.32-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:ab13dd6ede0e0e99ba97c73946462c3420625ab6e63fe13b6fc350e30eb3298d"}, + {file = "mysql_connector_python-8.0.32-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:e722b6ffa5b0d7188eebac792b18bc871643db505bf60d0e6bd2859f31e5ed79"}, + {file = "mysql_connector_python-8.0.32-cp311-cp311-manylinux1_i686.whl", hash = "sha256:283fe6f647e9d684feb1b7c48fa6a46b1e72c59ecdd6ea2b62392cd80c1a6701"}, + {file = "mysql_connector_python-8.0.32-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:1c0a11f3ffbf850f2ca7b39e6c82021e8de910ddaeffd856e53dca028d21c923"}, + {file = "mysql_connector_python-8.0.32-cp311-cp311-win_amd64.whl", hash = "sha256:6cdba2779bcd16af0ceff0a6e50d33e6664a83f8d17d70524beb6f677a6d1fae"}, + {file = "mysql_connector_python-8.0.32-cp37-cp37m-macosx_12_0_x86_64.whl", hash = "sha256:93b1eb3e07d19a23ccf2605d818aacee0d842b1820bbeef8d0022d8d3d014ab9"}, + {file = "mysql_connector_python-8.0.32-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:d6b54656ca131a4f0f17b9d0adddc60f84fd982d64e06360026d5b06e5dbf865"}, + {file = "mysql_connector_python-8.0.32-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:8c5bfedc979d7858402f39c20d66a6cf03ca4c960732a98318126c278535ddb2"}, + {file = "mysql_connector_python-8.0.32-cp37-cp37m-win_amd64.whl", hash = "sha256:bdd716b1e162fe4b3887f6617e9ddcfa659ba96a9ddb22feeae208a72f43d22f"}, + {file = "mysql_connector_python-8.0.32-cp38-cp38-macosx_12_0_x86_64.whl", hash = "sha256:bd52a462759aa324a60054c4b44dc8b32007187a328f72be6b58f193d5e32a91"}, + {file = "mysql_connector_python-8.0.32-cp38-cp38-manylinux1_i686.whl", hash = "sha256:be82357cc7e7e1377e2f4f8c18aa89c8aab6c0117155cf9fcf18e3cd0eb6ac8e"}, + {file = "mysql_connector_python-8.0.32-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:1f399f3c2599d2591854cd0e0a24c7c399dff21ac5accb6e52e06924de29f3f4"}, + {file = "mysql_connector_python-8.0.32-cp38-cp38-win_amd64.whl", hash = "sha256:c8bba02501525e1fbbba094a6d8d391d1534e8be41be6396c3e1b9f7d9d13b1c"}, + {file = "mysql_connector_python-8.0.32-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:145aeb75eefb7425e0a7fb36a4f95ebfe79e06be7c69a4045d34cde95c666dc4"}, + {file = "mysql_connector_python-8.0.32-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:c990f4c0702d1739076261c4dece1042e1eb18bf34e0d8516d19ec5166a205ce"}, + {file = "mysql_connector_python-8.0.32-cp39-cp39-manylinux1_i686.whl", hash = "sha256:7f7a69db9e0c36764a6c65377f6174aee46e484520e48659e7aa674415b8e192"}, + {file = "mysql_connector_python-8.0.32-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:677b5c6dcaec7e2a4bf95b991a869f4d371114f69a0d9a5bb236e988c8f4c376"}, + {file = "mysql_connector_python-8.0.32-cp39-cp39-win_amd64.whl", hash = "sha256:8c334c41cd1c5bcfa3550340253ef7d9d3b962211f33327c20f69706a0bcce06"}, + {file = "mysql_connector_python-8.0.32-py2.py3-none-any.whl", hash = "sha256:e0299236297b63bf6cbb61d81a9d400bc01cad4743d1abe5296ef349de15ee53"}, ] nodeenv = [ {file = "nodeenv-1.7.0-py2.py3-none-any.whl", hash = "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e"}, @@ -2782,7 +2812,10 @@ orjson = [ {file = "orjson-3.8.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b68a42a31f8429728183c21fb440c21de1b62e5378d0d73f280e2d894ef8942e"}, {file = "orjson-3.8.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ff13410ddbdda5d4197a4a4c09969cb78c722a67550f0a63c02c07aadc624833"}, {file = "orjson-3.8.0-cp310-none-win_amd64.whl", hash = "sha256:2d81e6e56bbea44be0222fb53f7b255b4e7426290516771592738ca01dbd053b"}, + {file = "orjson-3.8.0-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:200eae21c33f1f8b02a11f5d88d76950cd6fd986d88f1afe497a8ae2627c49aa"}, + {file = "orjson-3.8.0-cp311-cp311-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:9529990f3eab54b976d327360aa1ff244a4b12cb5e4c5b3712fcdd96e8fe56d4"}, {file = "orjson-3.8.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:e2defd9527651ad39ec20ae03c812adf47ef7662bdd6bc07dabb10888d70dc62"}, + {file = "orjson-3.8.0-cp311-none-win_amd64.whl", hash = "sha256:b21c7af0ff6228ca7105f54f0800636eb49201133e15ddb80ac20c1ce973ef07"}, {file = "orjson-3.8.0-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:9e6ac22cec72d5b39035b566e4b86c74b84866f12b5b0b6541506a080fb67d6d"}, {file = "orjson-3.8.0-cp37-cp37m-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:e2f4a5542f50e3d336a18cb224fc757245ca66b1fd0b70b5dd4471b8ff5f2b0e"}, {file = "orjson-3.8.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1418feeb8b698b9224b1f024555895169d481604d5d884498c1838d7412794c"}, @@ -2852,30 +2885,28 @@ prompt-toolkit = [ {file = "prompt_toolkit-3.0.31.tar.gz", hash = "sha256:9ada952c9d1787f52ff6d5f3484d0b4df8952787c087edf6a1f7c2cb1ea88148"}, ] protobuf = [ - {file = "protobuf-3.20.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3cc797c9d15d7689ed507b165cd05913acb992d78b379f6014e013f9ecb20996"}, - {file = "protobuf-3.20.1-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:ff8d8fa42675249bb456f5db06c00de6c2f4c27a065955917b28c4f15978b9c3"}, - {file = "protobuf-3.20.1-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cd68be2559e2a3b84f517fb029ee611546f7812b1fdd0aa2ecc9bc6ec0e4fdde"}, - {file = "protobuf-3.20.1-cp310-cp310-win32.whl", hash = "sha256:9016d01c91e8e625141d24ec1b20fed584703e527d28512aa8c8707f105a683c"}, - {file = "protobuf-3.20.1-cp310-cp310-win_amd64.whl", hash = "sha256:32ca378605b41fd180dfe4e14d3226386d8d1b002ab31c969c366549e66a2bb7"}, - {file = "protobuf-3.20.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9be73ad47579abc26c12024239d3540e6b765182a91dbc88e23658ab71767153"}, - {file = "protobuf-3.20.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:097c5d8a9808302fb0da7e20edf0b8d4703274d140fd25c5edabddcde43e081f"}, - {file = "protobuf-3.20.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e250a42f15bf9d5b09fe1b293bdba2801cd520a9f5ea2d7fb7536d4441811d20"}, - {file = "protobuf-3.20.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:cdee09140e1cd184ba9324ec1df410e7147242b94b5f8b0c64fc89e38a8ba531"}, - {file = "protobuf-3.20.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:af0ebadc74e281a517141daad9d0f2c5d93ab78e9d455113719a45a49da9db4e"}, - {file = "protobuf-3.20.1-cp37-cp37m-win32.whl", hash = "sha256:755f3aee41354ae395e104d62119cb223339a8f3276a0cd009ffabfcdd46bb0c"}, - {file = "protobuf-3.20.1-cp37-cp37m-win_amd64.whl", hash = "sha256:62f1b5c4cd6c5402b4e2d63804ba49a327e0c386c99b1675c8a0fefda23b2067"}, - {file = "protobuf-3.20.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:06059eb6953ff01e56a25cd02cca1a9649a75a7e65397b5b9b4e929ed71d10cf"}, - {file = "protobuf-3.20.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:cb29edb9eab15742d791e1025dd7b6a8f6fcb53802ad2f6e3adcb102051063ab"}, - {file = "protobuf-3.20.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:69ccfdf3657ba59569c64295b7d51325f91af586f8d5793b734260dfe2e94e2c"}, - {file = "protobuf-3.20.1-cp38-cp38-win32.whl", hash = "sha256:dd5789b2948ca702c17027c84c2accb552fc30f4622a98ab5c51fcfe8c50d3e7"}, - {file = "protobuf-3.20.1-cp38-cp38-win_amd64.whl", hash = "sha256:77053d28427a29987ca9caf7b72ccafee011257561259faba8dd308fda9a8739"}, - {file = "protobuf-3.20.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6f50601512a3d23625d8a85b1638d914a0970f17920ff39cec63aaef80a93fb7"}, - {file = "protobuf-3.20.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:284f86a6207c897542d7e956eb243a36bb8f9564c1742b253462386e96c6b78f"}, - {file = "protobuf-3.20.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:7403941f6d0992d40161aa8bb23e12575637008a5a02283a930addc0508982f9"}, - {file = "protobuf-3.20.1-cp39-cp39-win32.whl", hash = "sha256:db977c4ca738dd9ce508557d4fce0f5aebd105e158c725beec86feb1f6bc20d8"}, - {file = "protobuf-3.20.1-cp39-cp39-win_amd64.whl", hash = "sha256:7e371f10abe57cee5021797126c93479f59fccc9693dafd6bd5633ab67808a91"}, - {file = "protobuf-3.20.1-py2.py3-none-any.whl", hash = "sha256:adfc6cf69c7f8c50fd24c793964eef18f0ac321315439d94945820612849c388"}, - {file = "protobuf-3.20.1.tar.gz", hash = "sha256:adc31566d027f45efe3f44eeb5b1f329da43891634d61c75a5944e9be6dd42c9"}, + {file = "protobuf-3.20.3-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:f4bd856d702e5b0d96a00ec6b307b0f51c1982c2bf9c0052cf9019e9a544ba99"}, + {file = "protobuf-3.20.3-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9aae4406ea63d825636cc11ffb34ad3379335803216ee3a856787bcf5ccc751e"}, + {file = "protobuf-3.20.3-cp310-cp310-win32.whl", hash = "sha256:28545383d61f55b57cf4df63eebd9827754fd2dc25f80c5253f9184235db242c"}, + {file = "protobuf-3.20.3-cp310-cp310-win_amd64.whl", hash = "sha256:67a3598f0a2dcbc58d02dd1928544e7d88f764b47d4a286202913f0b2801c2e7"}, + {file = "protobuf-3.20.3-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:899dc660cd599d7352d6f10d83c95df430a38b410c1b66b407a6b29265d66469"}, + {file = "protobuf-3.20.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e64857f395505ebf3d2569935506ae0dfc4a15cb80dc25261176c784662cdcc4"}, + {file = "protobuf-3.20.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:d9e4432ff660d67d775c66ac42a67cf2453c27cb4d738fc22cb53b5d84c135d4"}, + {file = "protobuf-3.20.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:74480f79a023f90dc6e18febbf7b8bac7508420f2006fabd512013c0c238f454"}, + {file = "protobuf-3.20.3-cp37-cp37m-win32.whl", hash = "sha256:b6cc7ba72a8850621bfec987cb72623e703b7fe2b9127a161ce61e61558ad905"}, + {file = "protobuf-3.20.3-cp37-cp37m-win_amd64.whl", hash = "sha256:8c0c984a1b8fef4086329ff8dd19ac77576b384079247c770f29cc8ce3afa06c"}, + {file = "protobuf-3.20.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:de78575669dddf6099a8a0f46a27e82a1783c557ccc38ee620ed8cc96d3be7d7"}, + {file = "protobuf-3.20.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:f4c42102bc82a51108e449cbb32b19b180022941c727bac0cfd50170341f16ee"}, + {file = "protobuf-3.20.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:44246bab5dd4b7fbd3c0c80b6f16686808fab0e4aca819ade6e8d294a29c7050"}, + {file = "protobuf-3.20.3-cp38-cp38-win32.whl", hash = "sha256:c02ce36ec760252242a33967d51c289fd0e1c0e6e5cc9397e2279177716add86"}, + {file = "protobuf-3.20.3-cp38-cp38-win_amd64.whl", hash = "sha256:447d43819997825d4e71bf5769d869b968ce96848b6479397e29fc24c4a5dfe9"}, + {file = "protobuf-3.20.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:398a9e0c3eaceb34ec1aee71894ca3299605fa8e761544934378bbc6c97de23b"}, + {file = "protobuf-3.20.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:bf01b5720be110540be4286e791db73f84a2b721072a3711efff6c324cdf074b"}, + {file = "protobuf-3.20.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:daa564862dd0d39c00f8086f88700fdbe8bc717e993a21e90711acfed02f2402"}, + {file = "protobuf-3.20.3-cp39-cp39-win32.whl", hash = "sha256:819559cafa1a373b7096a482b504ae8a857c89593cf3a25af743ac9ecbd23480"}, + {file = "protobuf-3.20.3-cp39-cp39-win_amd64.whl", hash = "sha256:03038ac1cfbc41aa21f6afcbcd357281d7521b4157926f30ebecc8d4ea59dcb7"}, + {file = "protobuf-3.20.3-py2.py3-none-any.whl", hash = "sha256:a7ca6d488aa8ff7f329d4c545b2dbad8ac31464f1d8b1c87ad1346717731e4db"}, + {file = "protobuf-3.20.3.tar.gz", hash = "sha256:2e3427429c9cffebf259491be0af70189607f365c2f41c7c3764af6f337105f2"}, ] psycopg2 = [ {file = "psycopg2-2.9.4-cp310-cp310-win32.whl", hash = "sha256:8de6a9fc5f42fa52f559e65120dcd7502394692490c98fed1221acf0819d7797"}, diff --git a/spiffworkflow-backend/pyproject.toml b/spiffworkflow-backend/pyproject.toml index c72f9e792..fff605b09 100644 --- a/spiffworkflow-backend/pyproject.toml +++ b/spiffworkflow-backend/pyproject.toml @@ -27,10 +27,13 @@ flask-marshmallow = "*" flask-migrate = "*" flask-restful = "*" werkzeug = "*" -#SpiffWorkflow = {git = "https://github.com/sartography/SpiffWorkflow", rev = "main"} -SpiffWorkflow = {develop = true, path = "/home/dan/code/workflow/SpiffWorkflow" } +# temporarily switch off main to fix CI because poetry export doesn't capture the revision if it's not here (it ignores the lock) +SpiffWorkflow = {git = "https://github.com/sartography/SpiffWorkflow", rev = "main"} +# SpiffWorkflow = {develop = true, path = "../SpiffWorkflow" } sentry-sdk = "^1.10" sphinx-autoapi = "^2.0" +flask-bpmn = {git = "https://github.com/sartography/flask-bpmn", rev = "main"} +# flask-bpmn = {develop = true, path = "../flask-bpmn"} mysql-connector-python = "*" pytest-flask = "^1.2.0" pytest-flask-sqlalchemy = "^1.1.0" diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/__init__.py b/spiffworkflow-backend/src/spiffworkflow_backend/__init__.py index 64649048e..0c5f12320 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/__init__.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/__init__.py @@ -125,7 +125,11 @@ def create_app() -> flask.app.Flask: app.json = MyJSONEncoder(app) - if app.config["RUN_BACKGROUND_SCHEDULER"]: + # do not start the scheduler twice in flask debug mode + if ( + app.config["RUN_BACKGROUND_SCHEDULER"] + and os.environ.get("WERKZEUG_RUN_MAIN") != "true" + ): start_scheduler(app) configure_sentry(app) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/config/development.py b/spiffworkflow-backend/src/spiffworkflow_backend/config/development.py index 39e10cb58..b1ea980c2 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/config/development.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/config/development.py @@ -10,7 +10,7 @@ SPIFFWORKFLOW_BACKEND_LOG_LEVEL = environ.get( ) RUN_BACKGROUND_SCHEDULER = ( - environ.get("RUN_BACKGROUND_SCHEDULER", default="true") == "true" + environ.get("RUN_BACKGROUND_SCHEDULER", default="false") == "true" ) GIT_CLONE_URL_FOR_PUBLISHING = environ.get( "GIT_CLONE_URL", default="https://github.com/sartography/sample-process-models.git" diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_groups_controller.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_groups_controller.py index 65dca25f7..472e0358e 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_groups_controller.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_groups_controller.py @@ -20,11 +20,29 @@ from spiffworkflow_backend.routes.process_api_blueprint import ( _un_modify_modified_process_model_id, ) from spiffworkflow_backend.services.process_model_service import ProcessModelService +from spiffworkflow_backend.services.process_model_service import ( + ProcessModelWithInstancesNotDeletableError, +) def process_group_create(body: dict) -> flask.wrappers.Response: """Add_process_group.""" process_group = ProcessGroup(**body) + + if ProcessModelService.is_process_model_identifier(process_group.id): + raise ApiError( + error_code="process_model_with_id_already_exists", + message=f"Process Model with given id already exists: {process_group.id}", + status_code=400, + ) + + if ProcessModelService.is_process_group_identifier(process_group.id): + raise ApiError( + error_code="process_group_with_id_already_exists", + message=f"Process Group with given id already exists: {process_group.id}", + status_code=400, + ) + ProcessModelService.add_process_group(process_group) _commit_and_push_to_git( f"User: {g.user.username} added process group {process_group.id}" @@ -35,7 +53,16 @@ def process_group_create(body: dict) -> flask.wrappers.Response: def process_group_delete(modified_process_group_id: str) -> flask.wrappers.Response: """Process_group_delete.""" process_group_id = _un_modify_modified_process_model_id(modified_process_group_id) - ProcessModelService().process_group_delete(process_group_id) + + try: + ProcessModelService().process_group_delete(process_group_id) + except ProcessModelWithInstancesNotDeletableError as exception: + raise ApiError( + error_code="existing_instances", + message=str(exception), + status_code=400, + ) from exception + _commit_and_push_to_git( f"User: {g.user.username} deleted process group {process_group_id}" ) @@ -54,6 +81,13 @@ def process_group_update( } process_group_id = _un_modify_modified_process_model_id(modified_process_group_id) + if not ProcessModelService.is_process_group_identifier(process_group_id): + raise ApiError( + error_code="process_group_does_not_exist", + message=f"Process Group with given id does not exist: {process_group_id}", + status_code=400, + ) + process_group = ProcessGroup(id=process_group_id, **body_filtered) ProcessModelService.update_process_group(process_group) _commit_and_push_to_git( 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 08e665ca9..065150aa9 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_instances_controller.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_instances_controller.py @@ -182,9 +182,17 @@ def process_instance_log_list( ) if not detailed: log_query = log_query.filter( - or_( + # this was the previous implementation, where we only show completed tasks and skipped tasks. + # maybe we want to iterate on this in the future (in a third tab under process instance logs?) + # or_( + # SpiffLoggingModel.message.in_(["State change to COMPLETED"]), # type: ignore + # SpiffLoggingModel.message.like("Skipped task %"), # type: ignore + # ) + and_( SpiffLoggingModel.message.in_(["State change to COMPLETED"]), # type: ignore - SpiffLoggingModel.message.like("Skipped task %"), # type: ignore + SpiffLoggingModel.bpmn_task_type.in_( # type: ignore + ["Default Throwing Event", "End Event", "Default Start Event"] + ), ) ) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_models_controller.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_models_controller.py index bad5af7b6..694c01efd 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_models_controller.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_models_controller.py @@ -39,6 +39,9 @@ from spiffworkflow_backend.services.process_instance_report_service import ( ProcessInstanceReportService, ) from spiffworkflow_backend.services.process_model_service import ProcessModelService +from spiffworkflow_backend.services.process_model_service import ( + ProcessModelWithInstancesNotDeletableError, +) from spiffworkflow_backend.services.spec_file_service import ( ProcessModelFileInvalidError, ) @@ -75,6 +78,24 @@ def process_model_create( status_code=400, ) + if ProcessModelService.is_process_model_identifier(process_model_info.id): + raise ApiError( + error_code="process_model_with_id_already_exists", + message=( + f"Process Model with given id already exists: {process_model_info.id}" + ), + status_code=400, + ) + + if ProcessModelService.is_process_group_identifier(process_model_info.id): + raise ApiError( + error_code="process_group_with_id_already_exists", + message=( + f"Process Group with given id already exists: {process_model_info.id}" + ), + status_code=400, + ) + ProcessModelService.add_process_model(process_model_info) _commit_and_push_to_git( f"User: {g.user.username} created process model {process_model_info.id}" @@ -91,7 +112,15 @@ def process_model_delete( ) -> flask.wrappers.Response: """Process_model_delete.""" process_model_identifier = modified_process_model_identifier.replace(":", "/") - ProcessModelService().process_model_delete(process_model_identifier) + try: + ProcessModelService().process_model_delete(process_model_identifier) + except ProcessModelWithInstancesNotDeletableError as exception: + raise ApiError( + error_code="existing_instances", + message=str(exception), + status_code=400, + ) from exception + _commit_and_push_to_git( f"User: {g.user.username} deleted process model {process_model_identifier}" ) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/script_unit_tests_controller.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/script_unit_tests_controller.py index 3a6d11435..1af8febbb 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/script_unit_tests_controller.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/script_unit_tests_controller.py @@ -49,7 +49,7 @@ def script_unit_test_create( # TODO: move this to an xml service or something file_contents = SpecFileService.get_data(process_model, file.name) - bpmn_etree_element = etree.fromstring(file_contents) + bpmn_etree_element = SpecFileService.get_etree_from_xml_bytes(file_contents) nsmap = bpmn_etree_element.nsmap spiff_element_maker = ElementMaker( diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/user.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/user.py index 072acfaa6..976f7883c 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/user.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/user.py @@ -303,7 +303,6 @@ def login_api() -> Response: def login_api_return(code: str, state: str, session_state: str) -> str: - """Login_api_return.""" state_dict = ast.literal_eval(base64.b64decode(state).decode("utf-8")) state_dict["redirect_url"] diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py index 51b011cd4..b99d52de8 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py @@ -692,9 +692,8 @@ class ProcessInstanceProcessor: ): continue - subprocesses_by_child_task_ids[ - task_id - ] = subprocesses_by_child_task_ids[subprocess_id] + subprocesses_by_child_task_ids[task_id] =\ + subprocesses_by_child_task_ids[subprocess_id] self.get_highest_level_calling_subprocesses_by_child_task_ids( subprocesses_by_child_task_ids, task_typename_by_task_id ) @@ -1022,10 +1021,10 @@ class ProcessInstanceProcessor: data = SpecFileService.get_data(process_model_info, file.name) try: if file.type == FileType.bpmn.value: - bpmn: etree.Element = etree.fromstring(data) + bpmn: etree.Element = SpecFileService.get_etree_from_xml_bytes(data) parser.add_bpmn_xml(bpmn, filename=file.name) elif file.type == FileType.dmn.value: - dmn: etree.Element = etree.fromstring(data) + dmn: etree.Element = SpecFileService.get_etree_from_xml_bytes(data) parser.add_dmn_xml(dmn, filename=file.name) except XMLSyntaxError as xse: raise ApiError( @@ -1075,9 +1074,13 @@ class ProcessInstanceProcessor: if bpmn_process_instance.is_completed(): return ProcessInstanceStatus.complete user_tasks = bpmn_process_instance.get_ready_user_tasks() - waiting_tasks = bpmn_process_instance.get_tasks(TaskState.WAITING) - if len(waiting_tasks) > 0: - return ProcessInstanceStatus.waiting + + # if the process instance has status "waiting" it will get picked up + # by background processing. when that happens it can potentially overwrite + # human tasks which is bad because we cache them with the previous id's. + # waiting_tasks = bpmn_process_instance.get_tasks(TaskState.WAITING) + # if len(waiting_tasks) > 0: + # return ProcessInstanceStatus.waiting if len(user_tasks) > 0: return ProcessInstanceStatus.user_input_required else: diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_service.py index 9ac175945..d0c43fb24 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_service.py @@ -26,6 +26,10 @@ from spiffworkflow_backend.services.user_service import UserService T = TypeVar("T") +class ProcessModelWithInstancesNotDeletableError(Exception): + """ProcessModelWithInstancesNotDeletableError.""" + + class ProcessModelService(FileSystemService): """ProcessModelService.""" @@ -44,7 +48,7 @@ class ProcessModelService(FileSystemService): return path.replace(os.sep, "/") @classmethod - def is_group(cls, path: str) -> bool: + def is_process_group(cls, path: str) -> bool: """Is_group.""" group_json_path = os.path.join(path, cls.PROCESS_GROUP_JSON_FILE) if os.path.exists(group_json_path): @@ -52,8 +56,8 @@ class ProcessModelService(FileSystemService): return False @classmethod - def is_group_identifier(cls, process_group_identifier: str) -> bool: - """Is_group_identifier.""" + def is_process_group_identifier(cls, process_group_identifier: str) -> bool: + """Is_process_group_identifier.""" if os.path.exists(FileSystemService.root_path()): process_group_path = os.path.abspath( os.path.join( @@ -63,21 +67,21 @@ class ProcessModelService(FileSystemService): ), ) ) - return cls.is_group(process_group_path) + return cls.is_process_group(process_group_path) return False @classmethod - def is_model(cls, path: str) -> bool: - """Is_model.""" + def is_process_model(cls, path: str) -> bool: + """Is_process_model.""" model_json_path = os.path.join(path, cls.PROCESS_MODEL_JSON_FILE) if os.path.exists(model_json_path): return True return False @classmethod - def is_model_identifier(cls, process_model_identifier: str) -> bool: - """Is_model_identifier.""" + def is_process_model_identifier(cls, process_model_identifier: str) -> bool: + """Is_process_model_identifier.""" if os.path.exists(FileSystemService.root_path()): process_model_path = os.path.abspath( os.path.join( @@ -87,7 +91,7 @@ class ProcessModelService(FileSystemService): ), ) ) - return cls.is_model(process_model_path) + return cls.is_process_model(process_model_path) return False @@ -129,7 +133,9 @@ class ProcessModelService(FileSystemService): def save_process_model(cls, process_model: ProcessModelInfo) -> None: """Save_process_model.""" process_model_path = os.path.abspath( - os.path.join(FileSystemService.root_path(), process_model.id) + os.path.join( + FileSystemService.root_path(), process_model.id_for_file_path() + ) ) os.makedirs(process_model_path, exist_ok=True) json_path = os.path.abspath( @@ -150,12 +156,9 @@ class ProcessModelService(FileSystemService): ProcessInstanceModel.process_model_identifier == process_model_id ).all() if len(instances) > 0: - raise ApiError( - error_code="existing_instances", - message=( - f"We cannot delete the model `{process_model_id}`, there are" - " existing instances that depend on it." - ), + raise ProcessModelWithInstancesNotDeletableError( + f"We cannot delete the model `{process_model_id}`, there are" + " existing instances that depend on it." ) process_model = self.get_process_model(process_model_id) path = self.workflow_path(process_model) @@ -196,7 +199,7 @@ class ProcessModelService(FileSystemService): model_path = os.path.abspath( os.path.join(FileSystemService.root_path(), process_model_id) ) - if cls.is_model(model_path): + if cls.is_process_model(model_path): return cls.get_process_model_from_relative_path(process_model_id) raise ProcessEntityNotFoundError("process_model_not_found") @@ -300,7 +303,7 @@ class ProcessModelService(FileSystemService): FileSystemService.id_string_to_relative_path(process_group_id), ) ) - if cls.is_group(process_group_path): + if cls.is_process_group(process_group_path): return cls.find_or_create_process_group( process_group_path, find_direct_nested_items=find_direct_nested_items, @@ -348,7 +351,7 @@ class ProcessModelService(FileSystemService): for _root, dirs, _files in os.walk(group_path): for dir in dirs: model_dir = os.path.join(group_path, dir) - if ProcessModelService.is_model(model_dir): + if ProcessModelService.is_process_model(model_dir): process_model = self.get_process_model(model_dir) all_nested_models.append(process_model) return all_nested_models @@ -366,13 +369,10 @@ class ProcessModelService(FileSystemService): if len(instances) > 0: problem_models.append(process_model) if len(problem_models) > 0: - raise ApiError( - error_code="existing_instances", - message=( - f"We cannot delete the group `{process_group_id}`, there are" - " models with existing instances inside the group." - f" {problem_models}" - ), + raise ProcessModelWithInstancesNotDeletableError( + f"We cannot delete the group `{process_group_id}`, there are" + " models with existing instances inside the group." + f" {problem_models}" ) shutil.rmtree(path) self.cleanup_process_group_display_order() @@ -403,7 +403,7 @@ class ProcessModelService(FileSystemService): process_groups = [] for item in directory_items: # if item.is_dir() and not item.name[0] == ".": - if item.is_dir() and cls.is_group(item): # type: ignore + if item.is_dir() and cls.is_process_group(item): # type: ignore scanned_process_group = cls.find_or_create_process_group(item.path) process_groups.append(scanned_process_group) return process_groups @@ -450,12 +450,12 @@ class ProcessModelService(FileSystemService): for nested_item in nested_items: if nested_item.is_dir(): # TODO: check whether this is a group or model - if cls.is_group(nested_item.path): + if cls.is_process_group(nested_item.path): # This is a nested group process_group.process_groups.append( cls.find_or_create_process_group(nested_item.path) ) - elif ProcessModelService.is_model(nested_item.path): + elif ProcessModelService.is_process_model(nested_item.path): process_group.process_models.append( cls.__scan_process_model( nested_item.path, diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/spec_file_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/spec_file_service.py index 7273fd6c3..1e8233ec2 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/spec_file_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/spec_file_service.py @@ -5,6 +5,7 @@ from datetime import datetime from typing import List from typing import Optional +from flask_bpmn.models.db import db from lxml import etree # type: ignore from SpiffWorkflow.bpmn.parser.BpmnParser import BpmnValidator # type: ignore @@ -93,6 +94,12 @@ class SpecFileService(FileSystemService): process_model_info, file.name, file_contents ) + @classmethod + def get_etree_from_xml_bytes(cls, binary_data: bytes) -> etree.Element: + """Get_etree_from_xml_bytes.""" + etree_xml_parser = etree.XMLParser(resolve_entities=False) + return etree.fromstring(binary_data, parser=etree_xml_parser) + @classmethod def get_references_for_file_contents( cls, process_model_info: ProcessModelInfo, file_name: str, binary_data: bytes @@ -118,13 +125,13 @@ class SpecFileService(FileSystemService): correlations = {} start_messages = [] if file_type.value == FileType.bpmn.value: - parser.add_bpmn_xml(etree.fromstring(binary_data)) + parser.add_bpmn_xml(cls.get_etree_from_xml_bytes(binary_data)) parser_type = "process" sub_parsers = list(parser.process_parsers.values()) messages = parser.messages correlations = parser.correlations elif file_type.value == FileType.dmn.value: - parser.add_dmn_xml(etree.fromstring(binary_data)) + parser.add_dmn_xml(cls.get_etree_from_xml_bytes(binary_data)) sub_parsers = list(parser.dmn_parsers.values()) parser_type = "decision" else: @@ -172,9 +179,13 @@ class SpecFileService(FileSystemService): validator = BpmnValidator() parser = MyCustomParser(validator=validator) try: - parser.add_bpmn_xml(etree.fromstring(binary_data), filename=file_name) + parser.add_bpmn_xml( + cls.get_etree_from_xml_bytes(binary_data), filename=file_name + ) except Exception as exception: - raise ProcessModelFileInvalidError(str(exception)) + raise ProcessModelFileInvalidError( + f"Received error trying to parse bpmn xml: {str(exception)}" + ) from exception @classmethod def update_file( diff --git a/spiffworkflow-backend/tests/data/call-activity-to-human-task/manual_task.bpmn b/spiffworkflow-backend/tests/data/call-activity-to-human-task/manual_task.bpmn new file mode 100644 index 000000000..ff9c40003 --- /dev/null +++ b/spiffworkflow-backend/tests/data/call-activity-to-human-task/manual_task.bpmn @@ -0,0 +1,42 @@ + + + + + Flow_1nxz6rd + + + + Flow_1jtitb1 + + + + + NOOOOOOOOOOOOOOOOOO!!!!!!!!!! + + Flow_1nxz6rd + Flow_1jtitb1 + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spiffworkflow-backend/tests/data/call-activity-to-human-task/primary_process.bpmn b/spiffworkflow-backend/tests/data/call-activity-to-human-task/primary_process.bpmn new file mode 100644 index 000000000..8278559b8 --- /dev/null +++ b/spiffworkflow-backend/tests/data/call-activity-to-human-task/primary_process.bpmn @@ -0,0 +1,39 @@ + + + + + Flow_0fdzi5f + + + + Flow_0ii0wgu + + + + Flow_0fdzi5f + Flow_0ii0wgu + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spiffworkflow-backend/tests/data/xml_with_entity/file_to_inject b/spiffworkflow-backend/tests/data/xml_with_entity/file_to_inject new file mode 100644 index 000000000..81ddf4e64 --- /dev/null +++ b/spiffworkflow-backend/tests/data/xml_with_entity/file_to_inject @@ -0,0 +1 @@ +THIS_STRING_SHOULD_NOT_EXIST_ITS_SECRET diff --git a/spiffworkflow-backend/tests/data/xml_with_entity/invoice.bpmn b/spiffworkflow-backend/tests/data/xml_with_entity/invoice.bpmn new file mode 100644 index 000000000..44216f1e3 --- /dev/null +++ b/spiffworkflow-backend/tests/data/xml_with_entity/invoice.bpmn @@ -0,0 +1,6 @@ + + ]> + + John + &ent; + diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/helpers/base_test.py b/spiffworkflow-backend/tests/spiffworkflow_backend/helpers/base_test.py index 4d0d7c1a5..3b1c33443 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/helpers/base_test.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/helpers/base_test.py @@ -139,7 +139,7 @@ class BaseTest: process_group_path = os.path.abspath( os.path.join(FileSystemService.root_path(), process_group_id) ) - if ProcessModelService.is_group(process_group_path): + if ProcessModelService.is_process_group(process_group_path): if exception_notification_addresses is None: exception_notification_addresses = [] @@ -173,11 +173,11 @@ class BaseTest: " model" ) - def get_test_data_file_contents( + def get_test_data_file_full_path( self, file_name: str, process_model_test_data_dir: str - ) -> bytes: + ) -> str: """Get_test_data_file_contents.""" - file_full_path = os.path.join( + return os.path.join( current_app.instance_path, "..", "..", @@ -186,6 +186,14 @@ class BaseTest: process_model_test_data_dir, file_name, ) + + def get_test_data_file_contents( + self, file_name: str, process_model_test_data_dir: str + ) -> bytes: + """Get_test_data_file_contents.""" + file_full_path = self.get_test_data_file_full_path( + file_name, process_model_test_data_dir + ) with open(file_full_path, "rb") as file: return file.read() @@ -251,9 +259,9 @@ class BaseTest: There must be an existing process model to instantiate. """ - if not ProcessModelService.is_model_identifier(test_process_model_id): + if not ProcessModelService.is_process_model_identifier(test_process_model_id): dirname = os.path.dirname(test_process_model_id) - if not ProcessModelService.is_group_identifier(dirname): + if not ProcessModelService.is_process_group_identifier(dirname): process_group = ProcessGroup(id=dirname, display_name=dirname) ProcessModelService.add_process_group(process_group) basename = os.path.basename(test_process_model_id) diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_logging_service.py b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_logging_service.py index d27bbdc7c..f4aeddebc 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_logging_service.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_logging_service.py @@ -57,7 +57,7 @@ class TestLoggingService(BaseTest): assert response.status_code == 200 log_response = client.get( - f"/v1.0/logs/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}", + f"/v1.0/logs/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}?detailed=true", headers=headers, ) assert log_response.status_code == 200 diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_acceptance_test_fixtures.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_acceptance_test_fixtures.py index c738c7f69..e4eeecf1b 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_acceptance_test_fixtures.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_acceptance_test_fixtures.py @@ -18,15 +18,15 @@ def test_start_dates_are_one_hour_apart(app: Flask) -> None: ) group_identifier = os.path.dirname(process_model_identifier) parent_group_identifier = os.path.dirname(group_identifier) - if not ProcessModelService.is_group(parent_group_identifier): + if not ProcessModelService.is_process_group(parent_group_identifier): process_group = ProcessGroup( id=parent_group_identifier, display_name=parent_group_identifier ) ProcessModelService.add_process_group(process_group) - if not ProcessModelService.is_group(group_identifier): + if not ProcessModelService.is_process_group(group_identifier): process_group = ProcessGroup(id=group_identifier, display_name=group_identifier) ProcessModelService.add_process_group(process_group) - if not ProcessModelService.is_model(process_model_identifier): + if not ProcessModelService.is_process_model(process_model_identifier): process_model = ProcessModelInfo( id=process_model_identifier, display_name=process_model_identifier, diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_service.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_service.py new file mode 100644 index 000000000..bacde125b --- /dev/null +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_service.py @@ -0,0 +1,46 @@ +"""Test_process_instance_processor.""" +import os +from flask.app import Flask +from spiffworkflow_backend.models.spiff_logging import SpiffLoggingModel +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 +from spiffworkflow_backend.services.process_instance_processor import ( + ProcessInstanceProcessor, +) + + +class TestProcessInstanceService(BaseTest): + """TestProcessInstanceService.""" + + def test_does_not_log_set_data_when_calling_engine_steps_on_waiting_call_activity( + self, + app: Flask, + with_db_and_bpmn_file_cleanup: None, + with_super_admin_user: UserModel, + ) -> None: + """Test_does_not_log_set_data_when_calling_engine_steps_on_waiting_call_activity.""" + tmp_file = '/tmp/testfile.txt' + if os.path.isfile(tmp_file): + os.remove(tmp_file) + process_model = load_test_spec( + process_model_id="test_group/call-activity-to-human-task", + process_model_source_directory="call-activity-to-human-task", + ) + process_instance = self.create_process_instance_from_process_model( + process_model=process_model, user=with_super_admin_user + ) + processor = ProcessInstanceProcessor(process_instance) + processor.do_engine_steps(save=True) + + process_instance_logs = SpiffLoggingModel.query.filter_by(process_instance_id=process_instance.id).all() + initial_length = len(process_instance_logs) + + # logs should NOT increase after running this a second time since it's just waiting on a human task + print("HEY NOW") + with open(tmp_file, 'w') as f: + f.write("HEY") + processor.do_engine_steps(save=True) + process_instance_logs = SpiffLoggingModel.query.filter_by(process_instance_id=process_instance.id).all() + assert len(process_instance_logs) == initial_length diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_spec_file_service.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_spec_file_service.py index b3e19067f..eb7726c6e 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_spec_file_service.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_spec_file_service.py @@ -1,9 +1,12 @@ """Test_message_service.""" import os +import sys import pytest from flask import Flask from flask.testing import FlaskClient +from flask_bpmn.models.db import db +from lxml import etree # type: ignore from tests.spiffworkflow_backend.helpers.base_test import BaseTest from tests.spiffworkflow_backend.helpers.test_data import load_test_spec @@ -237,3 +240,29 @@ class TestSpecFileService(BaseTest): full_file_path = SpecFileService.full_file_path(process_model, "bad_xml.bpmn") assert not os.path.isfile(full_file_path) + + @pytest.mark.skipif( + sys.platform == "win32", + reason="tmp file path is not valid xml for windows and it doesn't matter", + ) + def test_does_not_evaluate_entities( + self, + app: Flask, + client: FlaskClient, + with_db_and_bpmn_file_cleanup: None, + ) -> None: + """Test_does_not_evaluate_entities.""" + string_replacement = b"THIS_STRING_SHOULD_NOT_EXIST_ITS_SECRET" + tmp_file = os.path.normpath( + self.get_test_data_file_full_path("file_to_inject", "xml_with_entity") + ) + file_contents = self.get_test_data_file_contents( + "invoice.bpmn", "xml_with_entity" + ) + file_contents = ( + file_contents.decode("utf-8") + .replace("{{FULL_PATH_TO_FILE}}", tmp_file) + .encode() + ) + etree_element = SpecFileService.get_etree_from_xml_bytes(file_contents) + assert string_replacement not in etree.tostring(etree_element) diff --git a/spiffworkflow-frontend/.github/workflows/tests.yml b/spiffworkflow-frontend/.github/workflows/tests.yml index 37c6c2fa9..464ed5ebd 100644 --- a/spiffworkflow-frontend/.github/workflows/tests.yml +++ b/spiffworkflow-frontend/.github/workflows/tests.yml @@ -87,7 +87,7 @@ jobs: run: ./bin/wait_for_frontend_to_be_up 5 - name: wait_for_keycloak working-directory: ./spiffworkflow-backend - run: ./bin/wait_for_keycloak 5 + run: ./keycloak/bin/wait_for_keycloak 5 - name: Cypress run uses: cypress-io/github-action@v4 with: diff --git a/spiffworkflow-frontend/cypress/e2e/process_models.cy.js b/spiffworkflow-frontend/cypress/e2e/process_models.cy.js index a709b624f..0e9250463 100644 --- a/spiffworkflow-frontend/cypress/e2e/process_models.cy.js +++ b/spiffworkflow-frontend/cypress/e2e/process_models.cy.js @@ -1,6 +1,9 @@ +import { slowCypressDown } from 'cypress-slow-down'; import { modifyProcessIdentifierForPathParam } from '../../src/helpers'; import { miscDisplayName } from '../support/helpers'; +// slowCypressDown(500); + describe('process-models', () => { beforeEach(() => { cy.login(); @@ -132,7 +135,7 @@ describe('process-models', () => { cy.get('.tile-process-group-content-container').should('exist'); }); - it('can upload and run a bpmn file', () => { + it.only('can upload and run a bpmn file', () => { const uuid = () => Cypress._.random(0, 1e6); const id = uuid(); const directParentGroupId = 'acceptance-tests-group-one'; diff --git a/spiffworkflow-frontend/package-lock.json b/spiffworkflow-frontend/package-lock.json index 8466f7a8d..c52d1a86b 100644 --- a/spiffworkflow-frontend/package-lock.json +++ b/spiffworkflow-frontend/package-lock.json @@ -41,6 +41,7 @@ "bpmn-js-spiffworkflow": "sartography/bpmn-js-spiffworkflow#main", "cookie": "^0.5.0", "craco": "^0.0.3", + "cypress-slow-down": "^1.2.1", "date-fns": "^2.28.0", "diagram-js": "^8.5.0", "dmn-js": "^12.2.0", @@ -57,6 +58,7 @@ "react-icons": "^4.4.0", "react-jsonschema-form": "^1.8.1", "react-markdown": "^8.0.3", + "react-router": "^6.3.0", "react-router-dom": "^6.3.0", "react-scripts": "^5.0.1", "remark-gfm": "^3.0.1", @@ -9936,6 +9938,19 @@ "node": "^14.0.0 || ^16.0.0 || >=18.0.0" } }, + "node_modules/cypress-plugin-config": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/cypress-plugin-config/-/cypress-plugin-config-1.2.0.tgz", + "integrity": "sha512-vgMMwjeI/L+2xptqkyhJ20LRuZrrsdbPaGMNNLVq+Cwox5+9dm0E312gpMXgXRs05uyUAzL/nCm/tdTckSAgoQ==" + }, + "node_modules/cypress-slow-down": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/cypress-slow-down/-/cypress-slow-down-1.2.1.tgz", + "integrity": "sha512-Pd+nESR+Ca8I+mLGbBrPVMEFvJBWxkJcEdcIUDxSBnMoWI00hiIKxzEgVqCv5c6Oap2OPpnrPLbJBwveCNKLig==", + "dependencies": { + "cypress-plugin-config": "^1.0.0" + } + }, "node_modules/cypress/node_modules/@types/node": { "version": "14.18.26", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.26.tgz", @@ -38782,6 +38797,19 @@ } } }, + "cypress-plugin-config": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/cypress-plugin-config/-/cypress-plugin-config-1.2.0.tgz", + "integrity": "sha512-vgMMwjeI/L+2xptqkyhJ20LRuZrrsdbPaGMNNLVq+Cwox5+9dm0E312gpMXgXRs05uyUAzL/nCm/tdTckSAgoQ==" + }, + "cypress-slow-down": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/cypress-slow-down/-/cypress-slow-down-1.2.1.tgz", + "integrity": "sha512-Pd+nESR+Ca8I+mLGbBrPVMEFvJBWxkJcEdcIUDxSBnMoWI00hiIKxzEgVqCv5c6Oap2OPpnrPLbJBwveCNKLig==", + "requires": { + "cypress-plugin-config": "^1.0.0" + } + }, "damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", diff --git a/spiffworkflow-frontend/package.json b/spiffworkflow-frontend/package.json index 515d191a7..da1665264 100644 --- a/spiffworkflow-frontend/package.json +++ b/spiffworkflow-frontend/package.json @@ -36,6 +36,7 @@ "bpmn-js-spiffworkflow": "sartography/bpmn-js-spiffworkflow#main", "cookie": "^0.5.0", "craco": "^0.0.3", + "cypress-slow-down": "^1.2.1", "date-fns": "^2.28.0", "diagram-js": "^8.5.0", "dmn-js": "^12.2.0", @@ -52,6 +53,7 @@ "react-icons": "^4.4.0", "react-jsonschema-form": "^1.8.1", "react-markdown": "^8.0.3", + "react-router": "^6.3.0", "react-router-dom": "^6.3.0", "react-scripts": "^5.0.1", "remark-gfm": "^3.0.1", diff --git a/spiffworkflow-frontend/src/components/ReactDiagramEditor.tsx b/spiffworkflow-frontend/src/components/ReactDiagramEditor.tsx index 54d63eb96..8e947fdc1 100644 --- a/spiffworkflow-frontend/src/components/ReactDiagramEditor.tsx +++ b/spiffworkflow-frontend/src/components/ReactDiagramEditor.tsx @@ -84,6 +84,7 @@ type OwnProps = { onJsonFilesRequested?: (..._args: any[]) => any; onDmnFilesRequested?: (..._args: any[]) => any; onSearchProcessModels?: (..._args: any[]) => any; + onElementsChanged?: (..._args: any[]) => any; url?: string; }; @@ -109,6 +110,7 @@ export default function ReactDiagramEditor({ onJsonFilesRequested, onDmnFilesRequested, onSearchProcessModels, + onElementsChanged, url, }: OwnProps) { const [diagramXMLString, setDiagramXMLString] = useState(''); @@ -291,6 +293,11 @@ export default function ReactDiagramEditor({ diagramModeler.on('element.click', (element: any) => { handleElementClick(element); }); + diagramModeler.on('elements.changed', (event: any) => { + if (onElementsChanged) { + onElementsChanged(event); + } + }); diagramModeler.on('spiff.service_tasks.requested', (event: any) => { handleServiceTasksRequested(event); @@ -330,6 +337,7 @@ export default function ReactDiagramEditor({ onJsonFilesRequested, onDmnFilesRequested, onSearchProcessModels, + onElementsChanged, ]); useEffect(() => { diff --git a/spiffworkflow-frontend/src/hooks/UsePrompt.tsx b/spiffworkflow-frontend/src/hooks/UsePrompt.tsx new file mode 100644 index 000000000..63b873616 --- /dev/null +++ b/spiffworkflow-frontend/src/hooks/UsePrompt.tsx @@ -0,0 +1,58 @@ +/** + * These hooks re-implement the now removed useBlocker and usePrompt hooks in 'react-router-dom'. + * Thanks for the idea @piecyk https://github.com/remix-run/react-router/issues/8139#issuecomment-953816315 + * Source: https://github.com/remix-run/react-router/commit/256cad70d3fd4500b1abcfea66f3ee622fb90874#diff-b60f1a2d4276b2a605c05e19816634111de2e8a4186fe9dd7de8e344b65ed4d3L344-L381 + */ + +import { useCallback, useContext, useEffect } from 'react'; +import { UNSAFE_NavigationContext as NavigationContext } from 'react-router-dom'; + +/** + * Blocks all navigation attempts. This is useful for preventing the page from + * changing until some condition is met, like saving form data. + * + * @param blocker + * @param when + * @see https://reactrouter.com/api/useBlocker + */ +export function useBlocker(blocker: any, when: any = true) { + const { navigator } = useContext(NavigationContext); + + useEffect(() => { + if (!when) return null; + + const unblock = (navigator as any).block((tx: any) => { + const autoUnblockingTx = { + ...tx, + retry() { + // Automatically unblock the transition so it can play all the way + // through before retrying it. TODO: Figure out how to re-enable + // this block if the transition is cancelled for some reason. + unblock(); + tx.retry(); + }, + }; + + blocker(autoUnblockingTx); + }); + + return unblock; + }, [navigator, blocker, when]); +} +/** + * Prompts the user with an Alert before they leave the current screen. + * + * @param message + * @param when + */ +export function usePrompt(message: any, when: any = true) { + const blocker = useCallback( + (tx: any) => { + // eslint-disable-next-line no-alert + if (window.confirm(message)) tx.retry(); + }, + [message] + ); + + useBlocker(blocker, when); +} diff --git a/spiffworkflow-frontend/src/routes/ProcessInstanceLogList.tsx b/spiffworkflow-frontend/src/routes/ProcessInstanceLogList.tsx index b4a4f683a..acfb4020c 100644 --- a/spiffworkflow-frontend/src/routes/ProcessInstanceLogList.tsx +++ b/spiffworkflow-frontend/src/routes/ProcessInstanceLogList.tsx @@ -46,15 +46,21 @@ export default function ProcessInstanceLogList() { return ( {rowToUse.id} - {rowToUse.message} - {rowToUse.bpmn_task_name} + + {rowToUse.bpmn_task_name || + (rowToUse.bpmn_task_type === 'Default Start Event' + ? 'Process Started' + : '') || + (rowToUse.bpmn_task_type === 'End Event' ? 'Process Ended' : '')} + {isDetailedView && ( <> + {rowToUse.message} {rowToUse.bpmn_task_identifier} {rowToUse.bpmn_task_type} - {rowToUse.bpmn_process_identifier} )} + {rowToUse.bpmn_process_identifier} {rowToUse.username} Id - Message Task Name {isDetailedView && ( <> + Message Task Identifier Task Type - Bpmn Process Identifier )} + Bpmn Process Identifier User Timestamp diff --git a/spiffworkflow-frontend/src/routes/ProcessModelEditDiagram.tsx b/spiffworkflow-frontend/src/routes/ProcessModelEditDiagram.tsx index f6af667ec..af38d8bc4 100644 --- a/spiffworkflow-frontend/src/routes/ProcessModelEditDiagram.tsx +++ b/spiffworkflow-frontend/src/routes/ProcessModelEditDiagram.tsx @@ -35,11 +35,13 @@ import { } from '../interfaces'; import ProcessSearch from '../components/ProcessSearch'; import { Notification } from '../components/Notification'; +import { usePrompt } from '../hooks/UsePrompt'; export default function ProcessModelEditDiagram() { const [showFileNameEditor, setShowFileNameEditor] = useState(false); const handleShowFileNameEditor = () => setShowFileNameEditor(true); const [processModel, setProcessModel] = useState(null); + const [diagramHasChanges, setDiagramHasChanges] = useState(false); const [scriptText, setScriptText] = useState(''); const [scriptType, setScriptType] = useState(''); @@ -112,6 +114,8 @@ export default function ProcessModelEditDiagram() { const processModelPath = `process-models/${modifiedProcessModelId}`; + usePrompt('Changes you made may not be saved.', diagramHasChanges); + useEffect(() => { // Grab all available process models in case we need to search for them. // Taken from the Process Group List @@ -206,6 +210,11 @@ export default function ProcessModelEditDiagram() { // after saving the file, make sure we null out newFileName // so it does not get used over the params setNewFileName(''); + setDiagramHasChanges(false); + }; + + const onElementsChanged = () => { + setDiagramHasChanges(true); }; const onDeleteFile = (fileName = params.file_name) => { @@ -922,6 +931,7 @@ export default function ProcessModelEditDiagram() { onLaunchDmnEditor={onLaunchDmnEditor} onDmnFilesRequested={onDmnFilesRequested} onSearchProcessModels={onSearchProcessModels} + onElementsChanged={onElementsChanged} /> ); };