Merge remote-tracking branch 'origin/main' into bug/cypress-tests

This commit is contained in:
burnettk 2023-01-04 20:58:03 -05:00
commit 479969db16
155 changed files with 6689 additions and 3333 deletions

View File

@ -11,6 +11,12 @@ repos:
require_serial: true require_serial: true
# exclude: ^migrations/ # exclude: ^migrations/
exclude: "/migrations/" exclude: "/migrations/"
# otherwise it will not fix long lines if the long lines contain long strings
# https://github.com/psf/black/pull/1132
# https://github.com/psf/black/pull/1609
args: [--preview]
- id: check-added-large-files - id: check-added-large-files
files: ^spiffworkflow-backend/ files: ^spiffworkflow-backend/
name: Check for added large files name: Check for added large files

View File

@ -10,9 +10,9 @@ services:
environment: environment:
- MYSQL_DATABASE=spiffworkflow_backend_development - MYSQL_DATABASE=spiffworkflow_backend_development
- MYSQL_ROOT_PASSWORD=my-secret-pw - MYSQL_ROOT_PASSWORD=my-secret-pw
- MYSQL_TCP_PORT=7003 - MYSQL_TCP_PORT=8003
ports: ports:
- "7003" - "8003"
healthcheck: healthcheck:
test: mysql --user=root --password=my-secret-pw -e 'select 1' spiffworkflow_backend_development test: mysql --user=root --password=my-secret-pw -e 'select 1' spiffworkflow_backend_development
interval: 10s interval: 10s
@ -30,12 +30,12 @@ services:
- SPIFFWORKFLOW_BACKEND_ENV=development - SPIFFWORKFLOW_BACKEND_ENV=development
- FLASK_DEBUG=0 - FLASK_DEBUG=0
- FLASK_SESSION_SECRET_KEY=super_secret_key - FLASK_SESSION_SECRET_KEY=super_secret_key
- OPEN_ID_SERVER_URL=http://localhost:7000/openid - OPEN_ID_SERVER_URL=http://localhost:8000/openid
- SPIFFWORKFLOW_FRONTEND_URL=http://localhost:7001 - SPIFFWORKFLOW_FRONTEND_URL=http://localhost:8001
- SPIFFWORKFLOW_BACKEND_URL=http://localhost:7000 - SPIFFWORKFLOW_BACKEND_URL=http://localhost:8000
- SPIFFWORKFLOW_BACKEND_PORT=7000 - SPIFFWORKFLOW_BACKEND_PORT=8000
- SPIFFWORKFLOW_BACKEND_UPGRADE_DB=true - SPIFFWORKFLOW_BACKEND_UPGRADE_DB=true
- SPIFFWORKFLOW_BACKEND_DATABASE_URI=mysql+mysqlconnector://root:my-secret-pw@spiffworkflow-db:7003/spiffworkflow_backend_development - SPIFFWORKFLOW_BACKEND_DATABASE_URI=mysql+mysqlconnector://root:my-secret-pw@spiffworkflow-db:8003/spiffworkflow_backend_development
- BPMN_SPEC_ABSOLUTE_DIR=/app/process_models - BPMN_SPEC_ABSOLUTE_DIR=/app/process_models
- SPIFFWORKFLOW_BACKEND_LOAD_FIXTURE_DATA=false - SPIFFWORKFLOW_BACKEND_LOAD_FIXTURE_DATA=false
- SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME=example.yml - SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME=example.yml
@ -43,12 +43,12 @@ services:
- OPEN_ID_CLIENT_ID=spiffworkflow-backend - OPEN_ID_CLIENT_ID=spiffworkflow-backend
- OPEN_ID_CLIENT_SECRET_KEY=my_open_id_secret_key - OPEN_ID_CLIENT_SECRET_KEY=my_open_id_secret_key
ports: ports:
- "7000:7000" - "8000:8000"
volumes: volumes:
- ./process_models:/app/process_models - ./process_models:/app/process_models
- ./log:/app/log - ./log:/app/log
healthcheck: healthcheck:
test: curl localhost:7000/v1.0/status --fail test: curl localhost:8000/v1.0/status --fail
interval: 10s interval: 10s
timeout: 5s timeout: 5s
retries: 20 retries: 20
@ -58,9 +58,9 @@ services:
image: ghcr.io/sartography/spiffworkflow-frontend image: ghcr.io/sartography/spiffworkflow-frontend
environment: environment:
- APPLICATION_ROOT=/ - APPLICATION_ROOT=/
- PORT0=7001 - PORT0=8001
ports: ports:
- "7001:7001" - "8001:8001"
spiffworkflow-connector: spiffworkflow-connector:
container_name: spiffworkflow-connector container_name: spiffworkflow-connector
@ -69,10 +69,11 @@ services:
- FLASK_ENV=${FLASK_ENV:-development} - FLASK_ENV=${FLASK_ENV:-development}
- FLASK_DEBUG=0 - FLASK_DEBUG=0
- FLASK_SESSION_SECRET_KEY=${FLASK_SESSION_SECRET_KEY:-super_secret_key} - FLASK_SESSION_SECRET_KEY=${FLASK_SESSION_SECRET_KEY:-super_secret_key}
- CONNECTOR_PROXY_PORT=8004
ports: ports:
- "7004:7004" - "8004:8004"
healthcheck: healthcheck:
test: curl localhost:7004/liveness --fail test: curl localhost:8004/liveness --fail
interval: 10s interval: 10s
timeout: 5s timeout: 5s
retries: 20 retries: 20

81
flask-bpmn/poetry.lock generated
View File

@ -813,22 +813,6 @@ category = "main"
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=3.6"
[[package]]
name = "libcst"
version = "0.4.3"
description = "A concrete syntax tree with AST-like properties for Python 3.5, 3.6, 3.7, 3.8, 3.9, and 3.10 programs."
category = "dev"
optional = false
python-versions = ">=3.7"
[package.dependencies]
pyyaml = ">=5.2"
typing-extensions = ">=3.7.4.2"
typing-inspect = ">=0.4.0"
[package.extras]
dev = ["black (==22.3.0)", "coverage (>=4.5.4)", "fixit (==0.1.1)", "flake8 (>=3.7.8)", "hypothesis (>=4.36.0)", "hypothesmith (>=0.0.4)", "jinja2 (==3.0.3)", "jupyter (>=1.0.0)", "maturin (>=0.8.3,<0.9)", "nbsphinx (>=0.4.2)", "prompt-toolkit (>=2.0.9)", "pyre-check (==0.9.9)", "setuptools-rust (>=0.12.1)", "setuptools-scm (>=6.0.1)", "slotscheck (>=0.7.1)", "sphinx-rtd-theme (>=0.4.3)", "ufmt (==1.3)", "usort (==1.0.0rc1)"]
[[package]] [[package]]
name = "livereload" name = "livereload"
version = "2.6.3" version = "2.6.3"
@ -905,18 +889,6 @@ category = "dev"
optional = false optional = false
python-versions = "*" python-versions = "*"
[[package]]
name = "monkeytype"
version = "22.2.0"
description = "Generating type annotations from sampled production types"
category = "dev"
optional = false
python-versions = ">=3.6"
[package.dependencies]
libcst = ">=0.3.7"
mypy-extensions = "*"
[[package]] [[package]]
name = "mypy" name = "mypy"
version = "0.991" version = "0.991"
@ -1504,7 +1476,7 @@ test = ["pytest"]
[[package]] [[package]]
name = "SpiffWorkflow" name = "SpiffWorkflow"
version = "1.2.1" version = "1.2.1"
description = "" description = "A workflow framework and BPMN/DMN Processor"
category = "main" category = "main"
optional = false optional = false
python-versions = "*" python-versions = "*"
@ -1520,7 +1492,7 @@ lxml = "*"
type = "git" type = "git"
url = "https://github.com/sartography/SpiffWorkflow" url = "https://github.com/sartography/SpiffWorkflow"
reference = "main" reference = "main"
resolved_reference = "025bc30f27366e06dd1286b7563e4b1cb04c1c46" resolved_reference = "841bd63017bb1d92858456393f144b4e5b23c994"
[[package]] [[package]]
name = "sqlalchemy" name = "sqlalchemy"
@ -1627,18 +1599,6 @@ category = "main"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
[[package]]
name = "typing-inspect"
version = "0.7.1"
description = "Runtime inspection utilities for typing module."
category = "dev"
optional = false
python-versions = "*"
[package.dependencies]
mypy-extensions = ">=0.3.0"
typing-extensions = ">=3.7.4"
[[package]] [[package]]
name = "unidecode" name = "unidecode"
version = "1.3.4" version = "1.3.4"
@ -1770,7 +1730,7 @@ testing = ["func-timeout", "jaraco.itertools", "pytest (>=6)", "pytest-black (>=
[metadata] [metadata]
lock-version = "1.1" lock-version = "1.1"
python-versions = "^3.7" python-versions = "^3.7"
content-hash = "6dfda037ebb3024834a45670108756a3057fff1b6fb5b916d222d3a162509b7d" content-hash = "45cac5741fa47e44710f5aae6dfdb4636fc4d60df2d6aba467052fdd5199e791"
[metadata.files] [metadata.files]
alabaster = [ alabaster = [
@ -2234,32 +2194,6 @@ lazy-object-proxy = [
{file = "lazy_object_proxy-1.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:677ea950bef409b47e51e733283544ac3d660b709cfce7b187f5ace137960d61"}, {file = "lazy_object_proxy-1.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:677ea950bef409b47e51e733283544ac3d660b709cfce7b187f5ace137960d61"},
{file = "lazy_object_proxy-1.7.1-pp37.pp38-none-any.whl", hash = "sha256:d66906d5785da8e0be7360912e99c9188b70f52c422f9fc18223347235691a84"}, {file = "lazy_object_proxy-1.7.1-pp37.pp38-none-any.whl", hash = "sha256:d66906d5785da8e0be7360912e99c9188b70f52c422f9fc18223347235691a84"},
] ]
libcst = [
{file = "libcst-0.4.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bea98a8be2b1725784ae01e89519121eba7d81280dcbee40ae03ececd7277cf3"},
{file = "libcst-0.4.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3d9191c764645dddf94d49885e590433fa0ee6d347b07eec86566786e6d2ada5"},
{file = "libcst-0.4.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c0f22e9787e44304e7cd9744e543602ab2c1bca8b922cb6237ea08d9a0be3fdd"},
{file = "libcst-0.4.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff147dd77b6ea72e4f2f0abfcd1be11a3108c28cb65e6da666c0b77142033f7c"},
{file = "libcst-0.4.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d744d4a6301c75322f1d88365dccfe402a51e724583a2edc4cba474462cc9419"},
{file = "libcst-0.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:ed0f15545eddfdd6270069ce0b2d4c253298817bd676a1a6adddaa1d66c7e28b"},
{file = "libcst-0.4.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a6f57056a743853c01bbd21bfd96c2a1b4c317bbc66920f5f2c9999b3dca7233"},
{file = "libcst-0.4.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c3d33da8f9b088e118bfc6ecacdd627ac237baeb490f4d7a383af4df4ea4f82"},
{file = "libcst-0.4.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df5f51a837fc10cdbf5c61acb467f6c15d5f9ca1d94a84a6a29c4f20ce7b437e"},
{file = "libcst-0.4.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f744f60057c8998b856d9baf28765c65574992f4a49830ca350010fc31f4eac4"},
{file = "libcst-0.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:88ab371aab82f7241448e263ec42abced649a77cdd21df960268e6df70b3f3f7"},
{file = "libcst-0.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:826ea5f10a84625db861ccf35946317f4f29e575261e44c0cd6c24c4dde5c2bb"},
{file = "libcst-0.4.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cab5b23796ce66303398bb7b2d27bcb17d2416dacd3d00229c961aed87d79a3b"},
{file = "libcst-0.4.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afc793c95af79e5adc5905713ccddff034d0de3e3da748424b722edf890227de"},
{file = "libcst-0.4.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c982387b8e23ad18efbd0287004924931a0b05c91ed5630453faf224bb0b185"},
{file = "libcst-0.4.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bc4c25aca45df5f86a6a1c8c219e8c7a90acdaef02b53eb01eafa563381cb0ce"},
{file = "libcst-0.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:1a395129ecf6c6ce429427f34100ccd99f35898a98187764a4559d9f92166cd0"},
{file = "libcst-0.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ca00819affafccb02b2582ec47706712b995c9887cad02bb8efe94a066830f37"},
{file = "libcst-0.4.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:231a9ca446570f9b63d8c2c6dbf6c796fb939a5e4ef9dc0dd9304a21a6c0da16"},
{file = "libcst-0.4.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b08e7a56950479c856183ad6fdf0a21df028d6732e1d19822ec1593e32f700ca"},
{file = "libcst-0.4.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cb70e7e5118234e75d309fcf04931e20f282f16c80dda464fc1b88ef02e52e4"},
{file = "libcst-0.4.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c8c00b24ab39facff463b18b9abc8df7dd063ae0ce9fe2e78e199c9a8572e37"},
{file = "libcst-0.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:28f35b9a21b2f8982a8ed3f53b1fdbc5435252409d34d061a3229dc4b413b8c7"},
{file = "libcst-0.4.3.tar.gz", hash = "sha256:f79ab61287505d97ed57ead14b78777f48cd6ec5339ca4978987e4c35957a465"},
]
livereload = [ livereload = [
{file = "livereload-2.6.3.tar.gz", hash = "sha256:776f2f865e59fde56490a56bcc6773b6917366bce0c267c60ee8aaf1a0959869"}, {file = "livereload-2.6.3.tar.gz", hash = "sha256:776f2f865e59fde56490a56bcc6773b6917366bce0c267c60ee8aaf1a0959869"},
] ]
@ -2389,10 +2323,6 @@ mccabe = [
{file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"},
{file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"},
] ]
monkeytype = [
{file = "MonkeyType-22.2.0-py3-none-any.whl", hash = "sha256:3d0815c7e98a18e9267990a452548247f6775fd636e65df5a7d77100ea7ad282"},
{file = "MonkeyType-22.2.0.tar.gz", hash = "sha256:6b0c00b49dcc5095a2c08d28246cf005e05673fc51f64d203f9a6bca2036dfab"},
]
mypy = [ mypy = [
{file = "mypy-0.991-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7d17e0a9707d0772f4a7b878f04b4fd11f6f5bcb9b3813975a9b13c9332153ab"}, {file = "mypy-0.991-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7d17e0a9707d0772f4a7b878f04b4fd11f6f5bcb9b3813975a9b13c9332153ab"},
{file = "mypy-0.991-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0714258640194d75677e86c786e80ccf294972cc76885d3ebbb560f11db0003d"}, {file = "mypy-0.991-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0714258640194d75677e86c786e80ccf294972cc76885d3ebbb560f11db0003d"},
@ -2808,11 +2738,6 @@ typing-extensions = [
{file = "typing_extensions-4.2.0-py3-none-any.whl", hash = "sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708"}, {file = "typing_extensions-4.2.0-py3-none-any.whl", hash = "sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708"},
{file = "typing_extensions-4.2.0.tar.gz", hash = "sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376"}, {file = "typing_extensions-4.2.0.tar.gz", hash = "sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376"},
] ]
typing-inspect = [
{file = "typing_inspect-0.7.1-py2-none-any.whl", hash = "sha256:b1f56c0783ef0f25fb064a01be6e5407e54cf4a4bf4f3ba3fe51e0bd6dcea9e5"},
{file = "typing_inspect-0.7.1-py3-none-any.whl", hash = "sha256:3cd7d4563e997719a710a3bfe7ffb544c6b72069b6812a02e9b414a8fa3aaa6b"},
{file = "typing_inspect-0.7.1.tar.gz", hash = "sha256:047d4097d9b17f46531bf6f014356111a1b6fb821a24fe7ac909853ca2a782aa"},
]
unidecode = [ unidecode = [
{file = "Unidecode-1.3.4-py3-none-any.whl", hash = "sha256:afa04efcdd818a93237574791be9b2817d7077c25a068b00f8cff7baa4e59257"}, {file = "Unidecode-1.3.4-py3-none-any.whl", hash = "sha256:afa04efcdd818a93237574791be9b2817d7077c25a068b00f8cff7baa4e59257"},
{file = "Unidecode-1.3.4.tar.gz", hash = "sha256:8e4352fb93d5a735c788110d2e7ac8e8031eb06ccbfe8d324ab71735015f9342"}, {file = "Unidecode-1.3.4.tar.gz", hash = "sha256:8e4352fb93d5a735c788110d2e7ac8e8031eb06ccbfe8d324ab71735015f9342"},

View File

@ -175,6 +175,10 @@ def handle_exception(exception: Exception) -> flask.wrappers.Response:
if not isinstance(exception, ApiError) or exception.error_code != "invalid_token": if not isinstance(exception, ApiError) or exception.error_code != "invalid_token":
id = capture_exception(exception) id = capture_exception(exception)
if isinstance(exception, ApiError):
current_app.logger.info(
f"Sending ApiError exception to sentry: {exception} with error code {exception.error_code}")
organization_slug = current_app.config.get("SENTRY_ORGANIZATION_SLUG") organization_slug = current_app.config.get("SENTRY_ORGANIZATION_SLUG")
project_slug = current_app.config.get("SENTRY_PROJECT_SLUG") project_slug = current_app.config.get("SENTRY_PROJECT_SLUG")
if organization_slug and project_slug: if organization_slug and project_slug:

50
poetry.lock generated
View File

@ -163,7 +163,7 @@ python-versions = "*"
[[package]] [[package]]
name = "black" name = "black"
version = "22.10.0" version = "23.1a1"
description = "The uncompromising code formatter." description = "The uncompromising code formatter."
category = "dev" category = "dev"
optional = false optional = false
@ -614,7 +614,7 @@ werkzeug = "*"
type = "git" type = "git"
url = "https://github.com/sartography/flask-bpmn" url = "https://github.com/sartography/flask-bpmn"
reference = "main" reference = "main"
resolved_reference = "860f2387bebdaa9220e9fbf6f8fa7f74e805d0d4" resolved_reference = "c79c1e0b6d34ec05d82cce888b5e57b33d24403b"
[[package]] [[package]]
name = "flask-cors" name = "flask-cors"
@ -1760,7 +1760,7 @@ lxml = "*"
type = "git" type = "git"
url = "https://github.com/sartography/SpiffWorkflow" url = "https://github.com/sartography/SpiffWorkflow"
reference = "main" reference = "main"
resolved_reference = "bba7ddf5478af579b891ca63c50babbfccf6b7a4" resolved_reference = "80640024a8030481645f0c34f34c57e88f7b4f0c"
[[package]] [[package]]
name = "sqlalchemy" name = "sqlalchemy"
@ -2182,27 +2182,18 @@ billiard = [
{file = "billiard-3.6.4.0.tar.gz", hash = "sha256:299de5a8da28a783d51b197d496bef4f1595dd023a93a4f59dde1886ae905547"}, {file = "billiard-3.6.4.0.tar.gz", hash = "sha256:299de5a8da28a783d51b197d496bef4f1595dd023a93a4f59dde1886ae905547"},
] ]
black = [ black = [
{file = "black-22.10.0-1fixedarch-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:5cc42ca67989e9c3cf859e84c2bf014f6633db63d1cbdf8fdb666dcd9e77e3fa"}, {file = "black-23.1a1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fb7641d442ede92538bc70fa0201f884753a7d0f62f26c722b7b00301b95902"},
{file = "black-22.10.0-1fixedarch-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:5d8f74030e67087b219b032aa33a919fae8806d49c867846bfacde57f43972ef"}, {file = "black-23.1a1-cp310-cp310-win_amd64.whl", hash = "sha256:88288a645402106b8eb9f50d7340ae741e16240bb01c2eed8466549153daa96e"},
{file = "black-22.10.0-1fixedarch-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:197df8509263b0b8614e1df1756b1dd41be6738eed2ba9e9769f3880c2b9d7b6"}, {file = "black-23.1a1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4db1d8027ce7ae53f0ccf02b0be0b8808fefb291d6cb1543420f4165d96d364c"},
{file = "black-22.10.0-1fixedarch-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:2644b5d63633702bc2c5f3754b1b475378fbbfb481f62319388235d0cd104c2d"}, {file = "black-23.1a1-cp311-cp311-win_amd64.whl", hash = "sha256:88ec25a64063945b4591b6378bead544c5d3260de1c93ad96f3ad2d76ddd76fd"},
{file = "black-22.10.0-1fixedarch-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:e41a86c6c650bcecc6633ee3180d80a025db041a8e2398dcc059b3afa8382cd4"}, {file = "black-23.1a1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dff6f0157e47fbbeada046fca144b6557d3be2fb2602d668881cd179f04a352"},
{file = "black-22.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2039230db3c6c639bd84efe3292ec7b06e9214a2992cd9beb293d639c6402edb"}, {file = "black-23.1a1-cp37-cp37m-win_amd64.whl", hash = "sha256:ca658b69260a18bf7aa0b0a6562dbbd304a737487d1318998aaca5a75901fd2c"},
{file = "black-22.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14ff67aec0a47c424bc99b71005202045dc09270da44a27848d534600ac64fc7"}, {file = "black-23.1a1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85dede655442f5e246e7abd667fe07e14916897ba52f3640b5489bf11f7dbf67"},
{file = "black-22.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:819dc789f4498ecc91438a7de64427c73b45035e2e3680c92e18795a839ebb66"}, {file = "black-23.1a1-cp38-cp38-win_amd64.whl", hash = "sha256:ddbf9da228726d46f45c29024263e160d41030a415097254817d65127012d1a2"},
{file = "black-22.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5b9b29da4f564ba8787c119f37d174f2b69cdfdf9015b7d8c5c16121ddc054ae"}, {file = "black-23.1a1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63330069d8ec909cf4e2c4d43a7f00aeb03335430ef9fec6cd2328e6ebde8a77"},
{file = "black-22.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8b49776299fece66bffaafe357d929ca9451450f5466e997a7285ab0fe28e3b"}, {file = "black-23.1a1-cp39-cp39-win_amd64.whl", hash = "sha256:793c9176beb2adf295f6b863d9a4dc953fe2ac359ca3da108d71d14cb2c09e52"},
{file = "black-22.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:21199526696b8f09c3997e2b4db8d0b108d801a348414264d2eb8eb2532e540d"}, {file = "black-23.1a1-py3-none-any.whl", hash = "sha256:e88e4b633d64b9e7adc4a6b922f52bb204af9f90d7b1e3317e6490f2b598b1ea"},
{file = "black-22.10.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e464456d24e23d11fced2bc8c47ef66d471f845c7b7a42f3bd77bf3d1789650"}, {file = "black-23.1a1.tar.gz", hash = "sha256:0b945a5a1e5a5321f884de0061d5a8585d947c9b608e37b6d26ceee4dfdf4b62"},
{file = "black-22.10.0-cp37-cp37m-win_amd64.whl", hash = "sha256:9311e99228ae10023300ecac05be5a296f60d2fd10fff31cf5c1fa4ca4b1988d"},
{file = "black-22.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fba8a281e570adafb79f7755ac8721b6cf1bbf691186a287e990c7929c7692ff"},
{file = "black-22.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:915ace4ff03fdfff953962fa672d44be269deb2eaf88499a0f8805221bc68c87"},
{file = "black-22.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:444ebfb4e441254e87bad00c661fe32df9969b2bf224373a448d8aca2132b395"},
{file = "black-22.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:974308c58d057a651d182208a484ce80a26dac0caef2895836a92dd6ebd725e0"},
{file = "black-22.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72ef3925f30e12a184889aac03d77d031056860ccae8a1e519f6cbb742736383"},
{file = "black-22.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:432247333090c8c5366e69627ccb363bc58514ae3e63f7fc75c54b1ea80fa7de"},
{file = "black-22.10.0-py3-none-any.whl", hash = "sha256:c957b2b4ea88587b46cf49d1dc17681c1e672864fd7af32fc1e9664d572b3458"},
{file = "black-22.10.0.tar.gz", hash = "sha256:f513588da599943e0cde4e32cc9879e825d58720d6557062d1098c5ad80080e1"},
] ]
blinker = [ blinker = [
{file = "blinker-1.5-py2.py3-none-any.whl", hash = "sha256:1eb563df6fdbc39eeddc177d953203f99f097e9bf0e2b8f9f3cf18b6ca425e36"}, {file = "blinker-1.5-py2.py3-none-any.whl", hash = "sha256:1eb563df6fdbc39eeddc177d953203f99f097e9bf0e2b8f9f3cf18b6ca425e36"},
@ -2857,7 +2848,18 @@ psycopg2 = [
{file = "psycopg2-2.9.5.tar.gz", hash = "sha256:a5246d2e683a972e2187a8714b5c2cf8156c064629f9a9b1a873c1730d9e245a"}, {file = "psycopg2-2.9.5.tar.gz", hash = "sha256:a5246d2e683a972e2187a8714b5c2cf8156c064629f9a9b1a873c1730d9e245a"},
] ]
pyasn1 = [ pyasn1 = [
{file = "pyasn1-0.4.8-py2.4.egg", hash = "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"},
{file = "pyasn1-0.4.8-py2.5.egg", hash = "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf"},
{file = "pyasn1-0.4.8-py2.6.egg", hash = "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00"},
{file = "pyasn1-0.4.8-py2.7.egg", hash = "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8"},
{file = "pyasn1-0.4.8-py2.py3-none-any.whl", hash = "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d"}, {file = "pyasn1-0.4.8-py2.py3-none-any.whl", hash = "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d"},
{file = "pyasn1-0.4.8-py3.1.egg", hash = "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86"},
{file = "pyasn1-0.4.8-py3.2.egg", hash = "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7"},
{file = "pyasn1-0.4.8-py3.3.egg", hash = "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576"},
{file = "pyasn1-0.4.8-py3.4.egg", hash = "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12"},
{file = "pyasn1-0.4.8-py3.5.egg", hash = "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2"},
{file = "pyasn1-0.4.8-py3.6.egg", hash = "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359"},
{file = "pyasn1-0.4.8-py3.7.egg", hash = "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776"},
{file = "pyasn1-0.4.8.tar.gz", hash = "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"}, {file = "pyasn1-0.4.8.tar.gz", hash = "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"},
] ]
pycodestyle = [ pycodestyle = [

View File

@ -1,7 +1,7 @@
.mypy_cache/ .mypy_cache/
/.idea/ /.idea/
/.coverage /.coverage
/.coverage.* .coverage.*
/.nox/ /.nox/
/.python-version /.python-version
/.pytype/ /.pytype/

View File

@ -7,7 +7,8 @@ def main() -> None:
"""Main.""" """Main."""
app = get_hacked_up_app_for_script() app = get_hacked_up_app_for_script()
with app.app_context(): with app.app_context():
AuthorizationService.delete_all_permissions_and_recreate() AuthorizationService.delete_all_permissions()
AuthorizationService.import_permissions_from_yaml_file()
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -1,5 +1,4 @@
"""Get the bpmn process json for a given process instance id and store it in /tmp.""" """Get the bpmn process json for a given process instance id and store it in /tmp."""
#!/usr/bin/env python
import os import os
import sys import sys
@ -18,15 +17,17 @@ def main(process_instance_id: str):
id=process_instance_id id=process_instance_id
).first() ).first()
file_path = f"/tmp/{process_instance_id}_bpmn_json.json"
if not process_instance: if not process_instance:
raise Exception( raise Exception(
f"Could not find a process instance with id: {process_instance_id}" f"Could not find a process instance with id: {process_instance_id}"
) )
with open( with open(
f"/tmp/{process_instance_id}_bpmn_json.json", "w", encoding="utf-8" file_path, "w", encoding="utf-8"
) as f: ) as f:
f.write(process_instance.bpmn_json) f.write(process_instance.bpmn_json)
print(f"Saved to {file_path}")
if len(sys.argv) < 2: if len(sys.argv) < 2:

View File

@ -7,4 +7,5 @@ function error_handler() {
trap 'error_handler ${LINENO} $?' ERR trap 'error_handler ${LINENO} $?' ERR
set -o errtrace -o errexit -o nounset -o pipefail set -o errtrace -o errexit -o nounset -o pipefail
docker compose logs "$@" # "docker compose logs" is only getting the db logs so specify them both
docker compose logs db spiffworkflow-backend

View File

@ -0,0 +1,11 @@
#!/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
set -x
mysql -uroot spiffworkflow_backend_development -e 'select pa.id, g.identifier group_identifier, pt.uri, permission from permission_assignment pa join principal p on p.id = pa.principal_id join `group` g on g.id = p.group_id join permission_target pt on pt.id = pa.permission_target_id;'

View File

@ -0,0 +1,10 @@
#!/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
grep -E '^ +\/' src/spiffworkflow_backend/api.yml | sort

View File

@ -27,7 +27,6 @@ def main():
"""Main.""" """Main."""
app = get_hacked_up_app_for_script() app = get_hacked_up_app_for_script()
with app.app_context(): with app.app_context():
process_model_identifier_ticket = "ticket" process_model_identifier_ticket = "ticket"
db.session.query(ProcessInstanceModel).filter( db.session.query(ProcessInstanceModel).filter(
ProcessInstanceModel.process_model_identifier ProcessInstanceModel.process_model_identifier

View File

@ -40,7 +40,8 @@ def hello_world():
return ( return (
'Hello, %s, <a href="/private">See private</a> ' 'Hello, %s, <a href="/private">See private</a> '
'<a href="/logout">Log out</a>' '<a href="/logout">Log out</a>'
) % oidc.user_getfield("preferred_username") % oidc.user_getfield("preferred_username")
)
else: else:
return 'Welcome anonymous, <a href="/private">Log in</a>' return 'Welcome anonymous, <a href="/private">Log in</a>'

View File

@ -61,3 +61,7 @@ for task in $tasks; do
done done
SPIFFWORKFLOW_BACKEND_ENV=testing FLASK_APP=src/spiffworkflow_backend poetry run flask db upgrade SPIFFWORKFLOW_BACKEND_ENV=testing FLASK_APP=src/spiffworkflow_backend poetry run flask db upgrade
if [[ -n "${SPIFFWORKFLOW_BACKEND_ENV:-}" ]] && ! grep -Eq '^(development|testing)$' <<< "$SPIFFWORKFLOW_BACKEND_ENV"; then
mysql -uroot -e "CREATE DATABASE IF NOT EXISTS spiffworkflow_backend_$SPIFFWORKFLOW_BACKEND_ENV"
FLASK_APP=src/spiffworkflow_backend poetry run flask db upgrade
fi

View File

@ -426,6 +426,7 @@
"emailVerified" : false, "emailVerified" : false,
"firstName" : "", "firstName" : "",
"lastName" : "", "lastName" : "",
"email" : "admin@spiffworkflow.org",
"credentials" : [ { "credentials" : [ {
"id" : "ef435043-ef0c-407a-af5b-ced13182a408", "id" : "ef435043-ef0c-407a-af5b-ced13182a408",
"type" : "password", "type" : "password",
@ -446,6 +447,7 @@
"enabled" : true, "enabled" : true,
"totp" : false, "totp" : false,
"emailVerified" : false, "emailVerified" : false,
"email" : "alex@sartography.com",
"credentials" : [ { "credentials" : [ {
"id" : "81a61a3b-228d-42b3-b39a-f62d8e7f57ca", "id" : "81a61a3b-228d-42b3-b39a-f62d8e7f57ca",
"type" : "password", "type" : "password",
@ -465,6 +467,7 @@
"enabled" : true, "enabled" : true,
"totp" : false, "totp" : false,
"emailVerified" : false, "emailVerified" : false,
"email" : "amir@status.im",
"credentials" : [ { "credentials" : [ {
"id" : "e589f3ad-bf7b-4756-89f7-7894c03c2831", "id" : "e589f3ad-bf7b-4756-89f7-7894c03c2831",
"type" : "password", "type" : "password",
@ -484,6 +487,9 @@
"enabled" : true, "enabled" : true,
"totp" : false, "totp" : false,
"emailVerified" : false, "emailVerified" : false,
"firstName" : "",
"lastName" : "",
"email" : "ciadmin1@spiffworkflow.org",
"credentials" : [ { "credentials" : [ {
"id" : "111b5ea1-c2ab-470a-a16b-2373bc94de7a", "id" : "111b5ea1-c2ab-470a-a16b-2373bc94de7a",
"type" : "password", "type" : "password",
@ -499,28 +505,6 @@
}, },
"notBefore" : 0, "notBefore" : 0,
"groups" : [ ] "groups" : [ ]
}, {
"id" : "56457e8f-47c6-4f9f-a72b-473dea5edfeb",
"createdTimestamp" : 1657139955336,
"username" : "ciuser1",
"enabled" : true,
"totp" : false,
"emailVerified" : false,
"credentials" : [ {
"id" : "762f36e9-47af-44da-8520-cf09d752497a",
"type" : "password",
"createdDate" : 1657139966468,
"secretData" : "{\"value\":\"Dpn9QBJSxvl54b0Fu+OKrKRwmDJbk28FQ3xhlOdJPvZVJU/SpdrcsH7ktYAIkVLkRC5qILSZuNPQ3vDGzE2r1Q==\",\"salt\":\"yXd7N8XIQBkJ7swHDeRzXw==\",\"additionalParameters\":{}}",
"credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}"
} ],
"disableableCredentialTypes" : [ ],
"requiredActions" : [ ],
"realmRoles" : [ "default-roles-spiffworkflow" ],
"clientRoles" : {
"spiffworkflow-backend" : [ "uma_protection" ]
},
"notBefore" : 0,
"groups" : [ ]
}, { }, {
"id" : "d58b61cc-a77e-488f-a427-05f4e0572e20", "id" : "d58b61cc-a77e-488f-a427-05f4e0572e20",
"createdTimestamp" : 1669132945413, "createdTimestamp" : 1669132945413,
@ -530,6 +514,7 @@
"emailVerified" : false, "emailVerified" : false,
"firstName" : "", "firstName" : "",
"lastName" : "", "lastName" : "",
"email" : "core@status.im",
"credentials" : [ { "credentials" : [ {
"id" : "ee80092b-8ee6-4699-8492-566e088b48f5", "id" : "ee80092b-8ee6-4699-8492-566e088b48f5",
"type" : "password", "type" : "password",
@ -550,6 +535,7 @@
"enabled" : true, "enabled" : true,
"totp" : false, "totp" : false,
"emailVerified" : false, "emailVerified" : false,
"email" : "dan@sartography.com",
"credentials" : [ { "credentials" : [ {
"id" : "d517c520-f500-4542-80e5-7144daef1e32", "id" : "d517c520-f500-4542-80e5-7144daef1e32",
"type" : "password", "type" : "password",
@ -569,6 +555,7 @@
"enabled" : true, "enabled" : true,
"totp" : false, "totp" : false,
"emailVerified" : false, "emailVerified" : false,
"email" : "daniel@sartography.com",
"credentials" : [ { "credentials" : [ {
"id" : "f240495c-265b-42fc-99db-46928580d07d", "id" : "f240495c-265b-42fc-99db-46928580d07d",
"type" : "password", "type" : "password",
@ -588,6 +575,7 @@
"enabled" : true, "enabled" : true,
"totp" : false, "totp" : false,
"emailVerified" : false, "emailVerified" : false,
"email" : "elizabeth@sartography.com",
"credentials" : [ { "credentials" : [ {
"id" : "ae951ec8-9fc9-4f1b-b340-bbbe463ae5c2", "id" : "ae951ec8-9fc9-4f1b-b340-bbbe463ae5c2",
"type" : "password", "type" : "password",
@ -609,6 +597,7 @@
"emailVerified" : false, "emailVerified" : false,
"firstName" : "", "firstName" : "",
"lastName" : "", "lastName" : "",
"email" : "fin@status.im",
"credentials" : [ { "credentials" : [ {
"id" : "2379940c-98b4-481a-b629-0bd1a4e91acf", "id" : "2379940c-98b4-481a-b629-0bd1a4e91acf",
"type" : "password", "type" : "password",
@ -631,6 +620,7 @@
"emailVerified" : false, "emailVerified" : false,
"firstName" : "", "firstName" : "",
"lastName" : "", "lastName" : "",
"email" : "fin1@status.im",
"credentials" : [ { "credentials" : [ {
"id" : "96216746-ff72-454e-8288-232428d10b42", "id" : "96216746-ff72-454e-8288-232428d10b42",
"type" : "password", "type" : "password",
@ -651,6 +641,7 @@
"enabled" : true, "enabled" : true,
"totp" : false, "totp" : false,
"emailVerified" : false, "emailVerified" : false,
"email" : "finance_user1@status.im",
"credentials" : [ { "credentials" : [ {
"id" : "f14722ec-13a7-4d35-a4ec-0475d405ae58", "id" : "f14722ec-13a7-4d35-a4ec-0475d405ae58",
"type" : "password", "type" : "password",
@ -670,6 +661,7 @@
"enabled" : true, "enabled" : true,
"totp" : false, "totp" : false,
"emailVerified" : false, "emailVerified" : false,
"email" : "harmeet@status.im",
"credentials" : [ { "credentials" : [ {
"id" : "89c26090-9bd3-46ac-b038-883d02e3f125", "id" : "89c26090-9bd3-46ac-b038-883d02e3f125",
"type" : "password", "type" : "password",
@ -691,6 +683,7 @@
"emailVerified" : false, "emailVerified" : false,
"firstName" : "", "firstName" : "",
"lastName" : "", "lastName" : "",
"email" : "j@status.im",
"credentials" : [ { "credentials" : [ {
"id" : "e71ec785-9133-4b7d-8015-1978379af0bb", "id" : "e71ec785-9133-4b7d-8015-1978379af0bb",
"type" : "password", "type" : "password",
@ -711,6 +704,7 @@
"enabled" : true, "enabled" : true,
"totp" : false, "totp" : false,
"emailVerified" : false, "emailVerified" : false,
"email" : "jakub@status.im",
"credentials" : [ { "credentials" : [ {
"id" : "ce141fa5-b8d5-4bbe-93e7-22e7119f97c2", "id" : "ce141fa5-b8d5-4bbe-93e7-22e7119f97c2",
"type" : "password", "type" : "password",
@ -730,6 +724,7 @@
"enabled" : true, "enabled" : true,
"totp" : false, "totp" : false,
"emailVerified" : false, "emailVerified" : false,
"email" : "jarrad@status.im",
"credentials" : [ { "credentials" : [ {
"id" : "113e0343-1069-476d-83f9-21d98edb9cfa", "id" : "113e0343-1069-476d-83f9-21d98edb9cfa",
"type" : "password", "type" : "password",
@ -749,6 +744,7 @@
"enabled" : true, "enabled" : true,
"totp" : false, "totp" : false,
"emailVerified" : false, "emailVerified" : false,
"email" : "jason@sartography.com",
"credentials" : [ { "credentials" : [ {
"id" : "40abf32e-f0cc-4a17-8231-1a69a02c1b0b", "id" : "40abf32e-f0cc-4a17-8231-1a69a02c1b0b",
"type" : "password", "type" : "password",
@ -768,6 +764,7 @@
"enabled" : true, "enabled" : true,
"totp" : false, "totp" : false,
"emailVerified" : false, "emailVerified" : false,
"email" : "jon@sartography.com",
"credentials" : [ { "credentials" : [ {
"id" : "8b520e01-5b9b-44ab-9ee8-505bd0831a45", "id" : "8b520e01-5b9b-44ab-9ee8-505bd0831a45",
"type" : "password", "type" : "password",
@ -787,6 +784,7 @@
"enabled" : true, "enabled" : true,
"totp" : false, "totp" : false,
"emailVerified" : false, "emailVerified" : false,
"email" : "kb@sartography.com",
"credentials" : [ { "credentials" : [ {
"id" : "2c0be363-038f-48f1-86d6-91fdd28657cf", "id" : "2c0be363-038f-48f1-86d6-91fdd28657cf",
"type" : "password", "type" : "password",
@ -808,6 +806,7 @@
"emailVerified" : false, "emailVerified" : false,
"firstName" : "", "firstName" : "",
"lastName" : "", "lastName" : "",
"email" : "lead@status.im",
"credentials" : [ { "credentials" : [ {
"id" : "96e836a4-1a84-45c5-a9ed-651b0c90195e", "id" : "96e836a4-1a84-45c5-a9ed-651b0c90195e",
"type" : "password", "type" : "password",
@ -830,6 +829,7 @@
"emailVerified" : false, "emailVerified" : false,
"firstName" : "", "firstName" : "",
"lastName" : "", "lastName" : "",
"email" : "lead1@status.im",
"credentials" : [ { "credentials" : [ {
"id" : "4e17388b-6c44-44e1-b20a-a873c0feb9a8", "id" : "4e17388b-6c44-44e1-b20a-a873c0feb9a8",
"type" : "password", "type" : "password",
@ -850,6 +850,7 @@
"enabled" : true, "enabled" : true,
"totp" : false, "totp" : false,
"emailVerified" : false, "emailVerified" : false,
"email" : "manuchehr@status.im",
"credentials" : [ { "credentials" : [ {
"id" : "07dabf55-b5d3-4f98-abba-3334086ecf5e", "id" : "07dabf55-b5d3-4f98-abba-3334086ecf5e",
"type" : "password", "type" : "password",
@ -869,6 +870,7 @@
"enabled" : true, "enabled" : true,
"totp" : false, "totp" : false,
"emailVerified" : false, "emailVerified" : false,
"email" : "mike@sartography.com",
"credentials" : [ { "credentials" : [ {
"id" : "1ed375fb-0f1a-4c2a-9243-2477242cf7bd", "id" : "1ed375fb-0f1a-4c2a-9243-2477242cf7bd",
"type" : "password", "type" : "password",
@ -887,7 +889,10 @@
"username" : "natalia", "username" : "natalia",
"enabled" : true, "enabled" : true,
"totp" : false, "totp" : false,
"emailVerified" : false, "emailVerified" : true,
"firstName" : "",
"lastName" : "",
"email" : "natalia@sartography.com",
"credentials" : [ { "credentials" : [ {
"id" : "b6aa9936-39cc-4931-bfeb-60e6753de5ba", "id" : "b6aa9936-39cc-4931-bfeb-60e6753de5ba",
"type" : "password", "type" : "password",
@ -907,6 +912,7 @@
"enabled" : true, "enabled" : true,
"totp" : false, "totp" : false,
"emailVerified" : false, "emailVerified" : false,
"email" : "sasha@status.im",
"credentials" : [ { "credentials" : [ {
"id" : "4a170af4-6f0c-4e7b-b70c-e674edf619df", "id" : "4a170af4-6f0c-4e7b-b70c-e674edf619df",
"type" : "password", "type" : "password",
@ -926,6 +932,7 @@
"enabled" : true, "enabled" : true,
"totp" : false, "totp" : false,
"emailVerified" : false, "emailVerified" : false,
"email" : "service-account@status.im",
"serviceAccountClientId" : "spiffworkflow-backend", "serviceAccountClientId" : "spiffworkflow-backend",
"credentials" : [ ], "credentials" : [ ],
"disableableCredentialTypes" : [ ], "disableableCredentialTypes" : [ ],
@ -943,6 +950,7 @@
"enabled" : true, "enabled" : true,
"totp" : false, "totp" : false,
"emailVerified" : false, "emailVerified" : false,
"email" : "service-account-withauth@status.im",
"serviceAccountClientId" : "withAuth", "serviceAccountClientId" : "withAuth",
"credentials" : [ ], "credentials" : [ ],
"disableableCredentialTypes" : [ ], "disableableCredentialTypes" : [ ],
@ -2166,7 +2174,7 @@
"subType" : "authenticated", "subType" : "authenticated",
"subComponents" : { }, "subComponents" : { },
"config" : { "config" : {
"allowed-protocol-mapper-types" : [ "oidc-usermodel-attribute-mapper", "oidc-sha256-pairwise-sub-mapper", "saml-user-property-mapper", "oidc-usermodel-property-mapper", "saml-role-list-mapper", "saml-user-attribute-mapper", "oidc-full-name-mapper", "oidc-address-mapper" ] "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" ]
} }
}, { }, {
"id" : "d68e938d-dde6-47d9-bdc8-8e8523eb08cd", "id" : "d68e938d-dde6-47d9-bdc8-8e8523eb08cd",
@ -2184,7 +2192,7 @@
"subType" : "anonymous", "subType" : "anonymous",
"subComponents" : { }, "subComponents" : { },
"config" : { "config" : {
"allowed-protocol-mapper-types" : [ "oidc-address-mapper", "oidc-full-name-mapper", "oidc-usermodel-attribute-mapper", "saml-user-property-mapper", "saml-user-attribute-mapper", "oidc-sha256-pairwise-sub-mapper", "oidc-usermodel-property-mapper", "saml-role-list-mapper" ] "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" ]
} }
}, { }, {
"id" : "3854361d-3fe5-47fb-9417-a99592e3dc5c", "id" : "3854361d-3fe5-47fb-9417-a99592e3dc5c",
@ -2274,7 +2282,7 @@
"internationalizationEnabled" : false, "internationalizationEnabled" : false,
"supportedLocales" : [ ], "supportedLocales" : [ ],
"authenticationFlows" : [ { "authenticationFlows" : [ {
"id" : "b896c673-57ab-4f24-bbb1-334bdadbecd3", "id" : "76ae522e-7ab3-48dc-af76-9cb8069368a2",
"alias" : "Account verification options", "alias" : "Account verification options",
"description" : "Method with which to verity the existing account", "description" : "Method with which to verity the existing account",
"providerId" : "basic-flow", "providerId" : "basic-flow",
@ -2296,7 +2304,7 @@
"userSetupAllowed" : false "userSetupAllowed" : false
} ] } ]
}, { }, {
"id" : "4da99e29-371e-4f4b-a863-e5079f30a714", "id" : "ddf80243-ec40-4c21-ae94-2967d841f84c",
"alias" : "Authentication Options", "alias" : "Authentication Options",
"description" : "Authentication options.", "description" : "Authentication options.",
"providerId" : "basic-flow", "providerId" : "basic-flow",
@ -2325,7 +2333,7 @@
"userSetupAllowed" : false "userSetupAllowed" : false
} ] } ]
}, { }, {
"id" : "d398c928-e201-4e8b-ab09-289bb351cd2e", "id" : "4f075680-46b7-49eb-b94c-d7425f105cb9",
"alias" : "Browser - Conditional OTP", "alias" : "Browser - Conditional OTP",
"description" : "Flow to determine if the OTP is required for the authentication", "description" : "Flow to determine if the OTP is required for the authentication",
"providerId" : "basic-flow", "providerId" : "basic-flow",
@ -2347,7 +2355,7 @@
"userSetupAllowed" : false "userSetupAllowed" : false
} ] } ]
}, { }, {
"id" : "663b7aa3-84f6-4347-8ed4-588c2464b75d", "id" : "a0467c77-c3dc-4df6-acd2-c05ca13601ed",
"alias" : "Direct Grant - Conditional OTP", "alias" : "Direct Grant - Conditional OTP",
"description" : "Flow to determine if the OTP is required for the authentication", "description" : "Flow to determine if the OTP is required for the authentication",
"providerId" : "basic-flow", "providerId" : "basic-flow",
@ -2369,7 +2377,7 @@
"userSetupAllowed" : false "userSetupAllowed" : false
} ] } ]
}, { }, {
"id" : "98013bc1-e4dd-41f7-9849-1f898143b944", "id" : "07536fec-8d41-4c73-845f-ca85002022e0",
"alias" : "First broker login - Conditional OTP", "alias" : "First broker login - Conditional OTP",
"description" : "Flow to determine if the OTP is required for the authentication", "description" : "Flow to determine if the OTP is required for the authentication",
"providerId" : "basic-flow", "providerId" : "basic-flow",
@ -2391,7 +2399,7 @@
"userSetupAllowed" : false "userSetupAllowed" : false
} ] } ]
}, { }, {
"id" : "b77e7545-9e39-4d72-93f8-1b38c954c2e2", "id" : "f123f912-71fb-4596-97f9-c0628a59413d",
"alias" : "Handle Existing Account", "alias" : "Handle Existing Account",
"description" : "Handle what to do if there is existing account with same email/username like authenticated identity provider", "description" : "Handle what to do if there is existing account with same email/username like authenticated identity provider",
"providerId" : "basic-flow", "providerId" : "basic-flow",
@ -2413,7 +2421,7 @@
"userSetupAllowed" : false "userSetupAllowed" : false
} ] } ]
}, { }, {
"id" : "2470e6f4-9a01-476a-9057-75d78e577182", "id" : "03c26cc5-366b-462d-9297-b4016f8d7c57",
"alias" : "Reset - Conditional OTP", "alias" : "Reset - Conditional OTP",
"description" : "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", "description" : "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.",
"providerId" : "basic-flow", "providerId" : "basic-flow",
@ -2435,7 +2443,7 @@
"userSetupAllowed" : false "userSetupAllowed" : false
} ] } ]
}, { }, {
"id" : "8e7dad0b-f4e1-4534-b618-b635b0a0e4f9", "id" : "1b4f474e-aa64-45cc-90f1-63504585d89c",
"alias" : "User creation or linking", "alias" : "User creation or linking",
"description" : "Flow for the existing/non-existing user alternatives", "description" : "Flow for the existing/non-existing user alternatives",
"providerId" : "basic-flow", "providerId" : "basic-flow",
@ -2458,7 +2466,7 @@
"userSetupAllowed" : false "userSetupAllowed" : false
} ] } ]
}, { }, {
"id" : "97c83e43-cba8-4d92-b108-9181bca07a1e", "id" : "38024dd6-daff-45de-8782-06b07b7bfa56",
"alias" : "Verify Existing Account by Re-authentication", "alias" : "Verify Existing Account by Re-authentication",
"description" : "Reauthentication of existing account", "description" : "Reauthentication of existing account",
"providerId" : "basic-flow", "providerId" : "basic-flow",
@ -2480,7 +2488,7 @@
"userSetupAllowed" : false "userSetupAllowed" : false
} ] } ]
}, { }, {
"id" : "fbabd64c-20de-4b8c-bfd2-be6822572278", "id" : "b7e30fca-e4ac-4886-a2e7-642fe2a27ee7",
"alias" : "browser", "alias" : "browser",
"description" : "browser based authentication", "description" : "browser based authentication",
"providerId" : "basic-flow", "providerId" : "basic-flow",
@ -2516,7 +2524,7 @@
"userSetupAllowed" : false "userSetupAllowed" : false
} ] } ]
}, { }, {
"id" : "0628a99f-b194-495d-8e54-cc4ca8684956", "id" : "92e3571d-ac3e-4e79-a391-5315954e866f",
"alias" : "clients", "alias" : "clients",
"description" : "Base authentication for clients", "description" : "Base authentication for clients",
"providerId" : "client-flow", "providerId" : "client-flow",
@ -2552,7 +2560,7 @@
"userSetupAllowed" : false "userSetupAllowed" : false
} ] } ]
}, { }, {
"id" : "ce6bf7af-3bff-48ce-b214-7fed08503a2a", "id" : "5093dd2d-fe5d-4f41-a54d-03cd648d9b7f",
"alias" : "direct grant", "alias" : "direct grant",
"description" : "OpenID Connect Resource Owner Grant", "description" : "OpenID Connect Resource Owner Grant",
"providerId" : "basic-flow", "providerId" : "basic-flow",
@ -2581,7 +2589,7 @@
"userSetupAllowed" : false "userSetupAllowed" : false
} ] } ]
}, { }, {
"id" : "60ce729b-d055-4ae7-83cb-85dbcf8cfdaa", "id" : "95d2f1ff-6907-47ce-a93c-db462fe04844",
"alias" : "docker auth", "alias" : "docker auth",
"description" : "Used by Docker clients to authenticate against the IDP", "description" : "Used by Docker clients to authenticate against the IDP",
"providerId" : "basic-flow", "providerId" : "basic-flow",
@ -2596,7 +2604,7 @@
"userSetupAllowed" : false "userSetupAllowed" : false
} ] } ]
}, { }, {
"id" : "0bd3cf93-7f33-46b2-ad1f-85cdfb0a87f9", "id" : "27405ee8-5730-419c-944c-a7c67edd91ce",
"alias" : "first broker login", "alias" : "first broker login",
"description" : "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", "description" : "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account",
"providerId" : "basic-flow", "providerId" : "basic-flow",
@ -2619,7 +2627,7 @@
"userSetupAllowed" : false "userSetupAllowed" : false
} ] } ]
}, { }, {
"id" : "3e52f178-9b9d-4a62-97d5-f9f3f872bcd9", "id" : "fce6d926-3a99-40ee-b79e-cae84493dbd8",
"alias" : "forms", "alias" : "forms",
"description" : "Username, password, otp and other auth forms.", "description" : "Username, password, otp and other auth forms.",
"providerId" : "basic-flow", "providerId" : "basic-flow",
@ -2641,7 +2649,7 @@
"userSetupAllowed" : false "userSetupAllowed" : false
} ] } ]
}, { }, {
"id" : "3f5fd6cc-2935-45d8-9bef-6857bba3657a", "id" : "75d93596-b7fb-4a2c-a780-e6a038e66fe9",
"alias" : "http challenge", "alias" : "http challenge",
"description" : "An authentication flow based on challenge-response HTTP Authentication Schemes", "description" : "An authentication flow based on challenge-response HTTP Authentication Schemes",
"providerId" : "basic-flow", "providerId" : "basic-flow",
@ -2663,7 +2671,7 @@
"userSetupAllowed" : false "userSetupAllowed" : false
} ] } ]
}, { }, {
"id" : "2c2b32dd-57dc-45d7-9a24-b4a253cb6a03", "id" : "04cdc1ac-c58d-4f8c-bc10-7d5e2bb99485",
"alias" : "registration", "alias" : "registration",
"description" : "registration flow", "description" : "registration flow",
"providerId" : "basic-flow", "providerId" : "basic-flow",
@ -2679,7 +2687,7 @@
"userSetupAllowed" : false "userSetupAllowed" : false
} ] } ]
}, { }, {
"id" : "dbc28b13-dba7-42a0-a8ab-faa8762979c3", "id" : "99593c1e-f2a5-4198-ad41-634694259110",
"alias" : "registration form", "alias" : "registration form",
"description" : "registration form", "description" : "registration form",
"providerId" : "form-flow", "providerId" : "form-flow",
@ -2715,7 +2723,7 @@
"userSetupAllowed" : false "userSetupAllowed" : false
} ] } ]
}, { }, {
"id" : "b4a901d5-e7b9-4eb6-9f8e-1d3305846828", "id" : "7d53f026-b05e-4a9c-aba6-23b17826a4d4",
"alias" : "reset credentials", "alias" : "reset credentials",
"description" : "Reset credentials for a user if they forgot their password or something", "description" : "Reset credentials for a user if they forgot their password or something",
"providerId" : "basic-flow", "providerId" : "basic-flow",
@ -2751,7 +2759,7 @@
"userSetupAllowed" : false "userSetupAllowed" : false
} ] } ]
}, { }, {
"id" : "824fe757-cc5c-4e13-ab98-9a2132e10f5c", "id" : "7ca17e64-f916-4d6c-91f0-815ec66f50e8",
"alias" : "saml ecp", "alias" : "saml ecp",
"description" : "SAML ECP Profile Authentication Flow", "description" : "SAML ECP Profile Authentication Flow",
"providerId" : "basic-flow", "providerId" : "basic-flow",
@ -2767,13 +2775,13 @@
} ] } ]
} ], } ],
"authenticatorConfig" : [ { "authenticatorConfig" : [ {
"id" : "817a93da-29df-447f-ab05-cd9557e66745", "id" : "9b71d817-b999-479d-97f8-07e39dd9e9fa",
"alias" : "create unique user config", "alias" : "create unique user config",
"config" : { "config" : {
"require.password.update.after.registration" : "false" "require.password.update.after.registration" : "false"
} }
}, { }, {
"id" : "4a8a9659-fa0d-4da8-907b-3b6daec1c878", "id" : "f9f13ba1-6a17-436b-a80b-6ccc042f9fc2",
"alias" : "review profile config", "alias" : "review profile config",
"config" : { "config" : {
"update.profile.on.first.login" : "missing" "update.profile.on.first.login" : "missing"

View File

@ -1,8 +1,8 @@
"""empty message """empty message
Revision ID: b99a4cb94b5b Revision ID: 907bcf0c3d75
Revises: Revises:
Create Date: 2022-12-20 10:45:08.295317 Create Date: 2022-12-28 13:52:13.030028
""" """
from alembic import op from alembic import op
@ -10,7 +10,7 @@ import sqlalchemy as sa
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision = 'b99a4cb94b5b' revision = '907bcf0c3d75'
down_revision = None down_revision = None
branch_labels = None branch_labels = None
depends_on = None depends_on = None
@ -72,16 +72,15 @@ def upgrade():
op.create_table('user', op.create_table('user',
sa.Column('id', sa.Integer(), nullable=False), sa.Column('id', sa.Integer(), nullable=False),
sa.Column('username', sa.String(length=255), nullable=False), sa.Column('username', sa.String(length=255), nullable=False),
sa.Column('uid', sa.String(length=50), nullable=True), sa.Column('service', sa.String(length=255), nullable=False),
sa.Column('service', sa.String(length=50), nullable=False),
sa.Column('service_id', sa.String(length=255), nullable=False), sa.Column('service_id', sa.String(length=255), nullable=False),
sa.Column('name', sa.String(length=255), nullable=True), sa.Column('display_name', sa.String(length=255), nullable=True),
sa.Column('email', sa.String(length=255), nullable=True), sa.Column('email', sa.String(length=255), nullable=True),
sa.Column('updated_at_in_seconds', sa.Integer(), nullable=True), sa.Column('updated_at_in_seconds', sa.Integer(), nullable=True),
sa.Column('created_at_in_seconds', sa.Integer(), nullable=True), sa.Column('created_at_in_seconds', sa.Integer(), nullable=True),
sa.PrimaryKeyConstraint('id'), sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('service', 'service_id', name='service_key'), sa.UniqueConstraint('service', 'service_id', name='service_key'),
sa.UniqueConstraint('uid') sa.UniqueConstraint('username')
) )
op.create_table('message_correlation_property', op.create_table('message_correlation_property',
sa.Column('id', sa.Integer(), nullable=False), sa.Column('id', sa.Integer(), nullable=False),
@ -176,6 +175,14 @@ def upgrade():
sa.PrimaryKeyConstraint('id'), sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('user_id', 'group_id', name='user_group_assignment_unique') sa.UniqueConstraint('user_id', 'group_id', name='user_group_assignment_unique')
) )
op.create_table('user_group_assignment_waiting',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('username', sa.String(length=255), nullable=False),
sa.Column('group_id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['group_id'], ['group.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('username', 'group_id', name='user_group_assignment_staged_unique')
)
op.create_table('human_task', op.create_table('human_task',
sa.Column('id', sa.Integer(), nullable=False), sa.Column('id', sa.Integer(), nullable=False),
sa.Column('process_instance_id', sa.Integer(), nullable=False), sa.Column('process_instance_id', sa.Integer(), nullable=False),
@ -309,6 +316,7 @@ def downgrade():
op.drop_table('message_correlation') op.drop_table('message_correlation')
op.drop_index(op.f('ix_human_task_completed'), table_name='human_task') op.drop_index(op.f('ix_human_task_completed'), table_name='human_task')
op.drop_table('human_task') op.drop_table('human_task')
op.drop_table('user_group_assignment_waiting')
op.drop_table('user_group_assignment') op.drop_table('user_group_assignment')
op.drop_table('secret') op.drop_table('secret')
op.drop_table('refresh_token') op.drop_table('refresh_token')

View File

@ -654,7 +654,7 @@ werkzeug = "*"
type = "git" type = "git"
url = "https://github.com/sartography/flask-bpmn" url = "https://github.com/sartography/flask-bpmn"
reference = "main" reference = "main"
resolved_reference = "0f2d249d0e799bec912d46132e9ef9754fdacbd7" resolved_reference = "c79c1e0b6d34ec05d82cce888b5e57b33d24403b"
[[package]] [[package]]
name = "Flask-Cors" name = "Flask-Cors"
@ -1851,7 +1851,7 @@ lxml = "*"
type = "git" type = "git"
url = "https://github.com/sartography/SpiffWorkflow" url = "https://github.com/sartography/SpiffWorkflow"
reference = "main" reference = "main"
resolved_reference = "841bd63017bb1d92858456393f144b4e5b23c994" resolved_reference = "80640024a8030481645f0c34f34c57e88f7b4f0c"
[[package]] [[package]]
name = "SQLAlchemy" name = "SQLAlchemy"
@ -2563,6 +2563,7 @@ 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_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_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-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-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-win32.whl", hash = "sha256:ea688d11707d30e212e0110a1aac7f7f3f542a259235d396f88be68b649e47d1"},
{file = "greenlet-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:afe07421c969e259e9403c3bb658968702bc3b78ec0b6fde3ae1e73440529c23"}, {file = "greenlet-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:afe07421c969e259e9403c3bb658968702bc3b78ec0b6fde3ae1e73440529c23"},
@ -2571,6 +2572,7 @@ 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_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_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-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-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-win32.whl", hash = "sha256:88c8d517e78acdf7df8a2134a3c4b964415b575d2840a2746ddb1cc6175f8608"},
{file = "greenlet-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:d6ee1aa7ab36475035eb48c01efae87d37936a8173fc4d7b10bb02c2d75dd8f6"}, {file = "greenlet-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:d6ee1aa7ab36475035eb48c01efae87d37936a8173fc4d7b10bb02c2d75dd8f6"},
@ -2579,6 +2581,7 @@ 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_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_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-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-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-win32.whl", hash = "sha256:db38f80540083ea33bdab614a9d28bcec4b54daa5aff1668d7827a9fc769ae0a"},
{file = "greenlet-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:b23d2a46d53210b498e5b701a1913697671988f4bf8e10f935433f6e7c332fb6"}, {file = "greenlet-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:b23d2a46d53210b498e5b701a1913697671988f4bf8e10f935433f6e7c332fb6"},
@ -2877,10 +2880,7 @@ 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_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-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-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-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_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-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"}, {file = "orjson-3.8.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1418feeb8b698b9224b1f024555895169d481604d5d884498c1838d7412794c"},
@ -2989,7 +2989,18 @@ psycopg2 = [
{file = "psycopg2-2.9.4.tar.gz", hash = "sha256:d529926254e093a1b669f692a3aa50069bc71faf5b0ecd91686a78f62767d52f"}, {file = "psycopg2-2.9.4.tar.gz", hash = "sha256:d529926254e093a1b669f692a3aa50069bc71faf5b0ecd91686a78f62767d52f"},
] ]
pyasn1 = [ pyasn1 = [
{file = "pyasn1-0.4.8-py2.4.egg", hash = "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"},
{file = "pyasn1-0.4.8-py2.5.egg", hash = "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf"},
{file = "pyasn1-0.4.8-py2.6.egg", hash = "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00"},
{file = "pyasn1-0.4.8-py2.7.egg", hash = "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8"},
{file = "pyasn1-0.4.8-py2.py3-none-any.whl", hash = "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d"}, {file = "pyasn1-0.4.8-py2.py3-none-any.whl", hash = "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d"},
{file = "pyasn1-0.4.8-py3.1.egg", hash = "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86"},
{file = "pyasn1-0.4.8-py3.2.egg", hash = "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7"},
{file = "pyasn1-0.4.8-py3.3.egg", hash = "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576"},
{file = "pyasn1-0.4.8-py3.4.egg", hash = "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12"},
{file = "pyasn1-0.4.8-py3.5.egg", hash = "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2"},
{file = "pyasn1-0.4.8-py3.6.egg", hash = "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359"},
{file = "pyasn1-0.4.8-py3.7.egg", hash = "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776"},
{file = "pyasn1-0.4.8.tar.gz", hash = "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"}, {file = "pyasn1-0.4.8.tar.gz", hash = "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"},
] ]
pycodestyle = [ pycodestyle = [

View File

@ -18,11 +18,11 @@ from werkzeug.exceptions import NotFound
import spiffworkflow_backend.load_database_models # noqa: F401 import spiffworkflow_backend.load_database_models # noqa: F401
from spiffworkflow_backend.config import setup_config from spiffworkflow_backend.config import setup_config
from spiffworkflow_backend.helpers.api_version import V1_API_PATH_PREFIX
from spiffworkflow_backend.routes.admin_blueprint.admin_blueprint import admin_blueprint from spiffworkflow_backend.routes.admin_blueprint.admin_blueprint import admin_blueprint
from spiffworkflow_backend.routes.openid_blueprint.openid_blueprint import ( from spiffworkflow_backend.routes.openid_blueprint.openid_blueprint import (
openid_blueprint, openid_blueprint,
) )
from spiffworkflow_backend.routes.process_api_blueprint import process_api_blueprint
from spiffworkflow_backend.routes.user import verify_token from spiffworkflow_backend.routes.user import verify_token
from spiffworkflow_backend.routes.user_blueprint import user_blueprint from spiffworkflow_backend.routes.user_blueprint import user_blueprint
from spiffworkflow_backend.services.authorization_service import AuthorizationService from spiffworkflow_backend.services.authorization_service import AuthorizationService
@ -93,7 +93,8 @@ def create_app() -> flask.app.Flask:
if os.environ.get("FLASK_SESSION_SECRET_KEY") is None: if os.environ.get("FLASK_SESSION_SECRET_KEY") is None:
raise KeyError( raise KeyError(
"Cannot find the secret_key from the environment. Please set FLASK_SESSION_SECRET_KEY" "Cannot find the secret_key from the environment. Please set"
" FLASK_SESSION_SECRET_KEY"
) )
app.secret_key = os.environ.get("FLASK_SESSION_SECRET_KEY") app.secret_key = os.environ.get("FLASK_SESSION_SECRET_KEY")
@ -103,7 +104,6 @@ def create_app() -> flask.app.Flask:
migrate.init_app(app, db) migrate.init_app(app, db)
app.register_blueprint(user_blueprint) app.register_blueprint(user_blueprint)
app.register_blueprint(process_api_blueprint)
app.register_blueprint(api_error_blueprint) app.register_blueprint(api_error_blueprint)
app.register_blueprint(admin_blueprint, url_prefix="/admin") app.register_blueprint(admin_blueprint, url_prefix="/admin")
app.register_blueprint(openid_blueprint, url_prefix="/openid") app.register_blueprint(openid_blueprint, url_prefix="/openid")
@ -117,7 +117,7 @@ def create_app() -> flask.app.Flask:
] ]
CORS(app, origins=origins_re, max_age=3600) CORS(app, origins=origins_re, max_age=3600)
connexion_app.add_api("api.yml", base_path="/v1.0") connexion_app.add_api("api.yml", base_path=V1_API_PATH_PREFIX)
mail = Mail(app) mail = Mail(app)
app.config["MAIL_APP"] = mail app.config["MAIL_APP"] = mail

View File

@ -8,10 +8,6 @@ servers:
- url: http://localhost:5000/v1.0 - url: http://localhost:5000/v1.0
# this is handled in flask now # this is handled in flask now
security: [] security: []
# - jwt: ["secret"]
# - oAuth2AuthCode:
# - read_email
# - uid
paths: paths:
/login: /login:
@ -22,7 +18,6 @@ paths:
schema: schema:
type: string type: string
get: get:
security: []
summary: redirect to open id authentication server summary: redirect to open id authentication server
operationId: spiffworkflow_backend.routes.user.login operationId: spiffworkflow_backend.routes.user.login
tags: tags:
@ -48,7 +43,6 @@ paths:
schema: schema:
type: string type: string
get: get:
security: []
operationId: spiffworkflow_backend.routes.user.login_return operationId: spiffworkflow_backend.routes.user.login_return
tags: tags:
- Authentication - Authentication
@ -68,7 +62,6 @@ paths:
schema: schema:
type: string type: string
get: get:
security: []
operationId: spiffworkflow_backend.routes.user.logout operationId: spiffworkflow_backend.routes.user.logout
summary: Logout authenticated user summary: Logout authenticated user
tags: tags:
@ -78,7 +71,6 @@ paths:
description: Logout Authenticated User description: Logout Authenticated User
/logout_return: /logout_return:
get: get:
security: []
operationId: spiffworkflow_backend.routes.user.logout_return operationId: spiffworkflow_backend.routes.user.logout_return
summary: Logout authenticated user summary: Logout authenticated user
tags: tags:
@ -89,7 +81,6 @@ paths:
/login_api: /login_api:
get: get:
security: []
operationId: spiffworkflow_backend.routes.user.login_api operationId: spiffworkflow_backend.routes.user.login_api
summary: Authenticate user for API access summary: Authenticate user for API access
tags: tags:
@ -115,7 +106,6 @@ paths:
schema: schema:
type: string type: string
get: get:
security: []
operationId: spiffworkflow_backend.routes.user.login_api_return operationId: spiffworkflow_backend.routes.user.login_api_return
tags: tags:
- Authentication - Authentication
@ -125,8 +115,7 @@ paths:
/status: /status:
get: get:
security: [] operationId: spiffworkflow_backend.routes.health_controller.status
operationId: spiffworkflow_backend.routes.process_api_blueprint.status
summary: Returns 200 if the server is Responding summary: Returns 200 if the server is Responding
tags: tags:
- Liveness - Liveness
@ -160,7 +149,7 @@ paths:
schema: schema:
type: integer type: integer
get: get:
operationId: spiffworkflow_backend.routes.process_api_blueprint.process_group_list operationId: spiffworkflow_backend.routes.process_groups_controller.process_group_list
summary: get list summary: get list
tags: tags:
- Process Groups - Process Groups
@ -174,7 +163,7 @@ paths:
items: items:
$ref: "#/components/schemas/ProcessModelCategory" $ref: "#/components/schemas/ProcessModelCategory"
post: post:
operationId: spiffworkflow_backend.routes.process_api_blueprint.process_group_add operationId: spiffworkflow_backend.routes.process_groups_controller.process_group_create
summary: Add process group summary: Add process group
tags: tags:
- Process Groups - Process Groups
@ -201,7 +190,7 @@ paths:
type: string type: string
# process_group_show # process_group_show
get: get:
operationId: spiffworkflow_backend.routes.process_api_blueprint.process_group_show operationId: spiffworkflow_backend.routes.process_groups_controller.process_group_show
summary: Returns a single process group summary: Returns a single process group
tags: tags:
- Process Groups - Process Groups
@ -213,7 +202,7 @@ paths:
schema: schema:
$ref: "#/components/schemas/ProcessModelCategory" $ref: "#/components/schemas/ProcessModelCategory"
delete: delete:
operationId: spiffworkflow_backend.routes.process_api_blueprint.process_group_delete operationId: spiffworkflow_backend.routes.process_groups_controller.process_group_delete
summary: Deletes a single process group summary: Deletes a single process group
tags: tags:
- Process Groups - Process Groups
@ -221,7 +210,7 @@ paths:
"200": "200":
description: The process group was deleted. description: The process group was deleted.
put: put:
operationId: spiffworkflow_backend.routes.process_api_blueprint.process_group_update operationId: spiffworkflow_backend.routes.process_groups_controller.process_group_update
summary: Updates a single process group summary: Updates a single process group
tags: tags:
- Process Groups - Process Groups
@ -253,7 +242,7 @@ paths:
schema: schema:
type: string type: string
put: put:
operationId: spiffworkflow_backend.routes.process_api_blueprint.process_group_move operationId: spiffworkflow_backend.routes.process_groups_controller.process_group_move
summary: returns the new group summary: returns the new group
tags: tags:
- Process Groups - Process Groups
@ -285,6 +274,12 @@ paths:
description: Get only the process models that the user can run description: Get only the process models that the user can run
schema: schema:
type: boolean type: boolean
- name: include_parent_groups
in: query
required: false
description: Get the display names for the parent groups as well
schema:
type: boolean
- name: page - name: page
in: query in: query
required: false required: false
@ -298,7 +293,7 @@ paths:
schema: schema:
type: integer type: integer
get: get:
operationId: spiffworkflow_backend.routes.process_api_blueprint.process_model_list operationId: spiffworkflow_backend.routes.process_models_controller.process_model_list
summary: Return a list of process models for a given process group summary: Return a list of process models for a given process group
tags: tags:
- Process Models - Process Models
@ -321,7 +316,33 @@ paths:
schema: schema:
type: string type: string
post: post:
operationId: spiffworkflow_backend.routes.process_api_blueprint.process_model_create operationId: spiffworkflow_backend.routes.process_models_controller.process_model_create
summary: Creates a new process model with the given parameters.
tags:
- Process Models
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/ProcessModel"
responses:
"201":
description: Process model created successfully.
content:
application/json:
schema:
$ref: "#/components/schemas/ProcessModel"
/process-models-natural-language/{modified_process_group_id}:
parameters:
- name: modified_process_group_id
in: path
required: true
description: modified id of an existing process group
schema:
type: string
post:
operationId: spiffworkflow_backend.routes.process_models_controller.process_model_create_with_natural_language
summary: Creates a new process model with the given parameters. summary: Creates a new process model with the given parameters.
tags: tags:
- Process Models - Process Models
@ -347,7 +368,7 @@ paths:
schema: schema:
type: string type: string
post: post:
operationId: spiffworkflow_backend.routes.process_api_blueprint.add_file operationId: spiffworkflow_backend.routes.process_models_controller.process_model_file_create
summary: Add a new workflow spec file summary: Add a new workflow spec file
tags: tags:
- Process Model Files - Process Model Files
@ -377,7 +398,7 @@ paths:
schema: schema:
type: string type: string
get: get:
operationId: spiffworkflow_backend.routes.process_api_blueprint.process_model_show operationId: spiffworkflow_backend.routes.process_models_controller.process_model_show
summary: Returns a single process model summary: Returns a single process model
tags: tags:
- Process Models - Process Models
@ -389,7 +410,7 @@ paths:
schema: schema:
$ref: "#/components/schemas/ProcessModel" $ref: "#/components/schemas/ProcessModel"
put: put:
operationId: spiffworkflow_backend.routes.process_api_blueprint.process_model_update operationId: spiffworkflow_backend.routes.process_models_controller.process_model_update
summary: Modifies an existing process model with the given parameters. summary: Modifies an existing process model with the given parameters.
tags: tags:
- Process Models - Process Models
@ -406,7 +427,7 @@ paths:
schema: schema:
$ref: "#/components/schemas/ProcessModel" $ref: "#/components/schemas/ProcessModel"
delete: delete:
operationId: spiffworkflow_backend.routes.process_api_blueprint.process_model_delete operationId: spiffworkflow_backend.routes.process_models_controller.process_model_delete
summary: Removes an existing process model summary: Removes an existing process model
tags: tags:
- Process Models - Process Models
@ -433,7 +454,7 @@ paths:
schema: schema:
type: string type: string
put: put:
operationId: spiffworkflow_backend.routes.process_api_blueprint.process_model_move operationId: spiffworkflow_backend.routes.process_models_controller.process_model_move
summary: returns the new model summary: returns the new model
tags: tags:
- Process Models - Process Models
@ -460,7 +481,7 @@ paths:
schema: schema:
type: string type: string
post: post:
operationId: spiffworkflow_backend.routes.process_api_blueprint.process_model_publish operationId: spiffworkflow_backend.routes.process_models_controller.process_model_publish
summary: Merge changes from this model to another branch. summary: Merge changes from this model to another branch.
tags: tags:
- Process Models - Process Models
@ -608,7 +629,7 @@ paths:
schema: schema:
type: string type: string
get: get:
operationId: spiffworkflow_backend.routes.process_api_blueprint.process_instance_list_for_me operationId: spiffworkflow_backend.routes.process_instances_controller.process_instance_list_for_me
summary: Returns a list of process instances that are associated with me. summary: Returns a list of process instances that are associated with me.
tags: tags:
- Process Instances - Process Instances
@ -721,7 +742,7 @@ paths:
schema: schema:
type: string type: string
get: get:
operationId: spiffworkflow_backend.routes.process_api_blueprint.process_instance_list operationId: spiffworkflow_backend.routes.process_instances_controller.process_instance_list
summary: Returns a list of process instances. summary: Returns a list of process instances.
tags: tags:
- Process Instances - Process Instances
@ -744,7 +765,7 @@ paths:
schema: schema:
type: string type: string
post: post:
operationId: spiffworkflow_backend.routes.process_api_blueprint.script_unit_test_create operationId: spiffworkflow_backend.routes.script_unit_tests_controller.script_unit_test_create
summary: Create script unit test based on given criteria summary: Create script unit test based on given criteria
tags: tags:
- Script Unit Test - Script Unit Test
@ -765,7 +786,7 @@ paths:
schema: schema:
type: string type: string
post: post:
operationId: spiffworkflow_backend.routes.process_api_blueprint.script_unit_test_run operationId: spiffworkflow_backend.routes.script_unit_tests_controller.script_unit_test_run
summary: Run a given script unit test. summary: Run a given script unit test.
tags: tags:
- Script Unit Test - Script Unit Test
@ -786,7 +807,7 @@ paths:
schema: schema:
type: string type: string
post: post:
operationId: spiffworkflow_backend.routes.process_api_blueprint.process_instance_create operationId: spiffworkflow_backend.routes.process_instances_controller.process_instance_create
summary: Creates an process instance from a process model and returns the instance summary: Creates an process instance from a process model and returns the instance
tags: tags:
- Process Instances - Process Instances
@ -833,7 +854,7 @@ paths:
get: get:
tags: tags:
- Process Instances - Process Instances
operationId: spiffworkflow_backend.routes.process_api_blueprint.process_instance_task_list_without_task_data_for_me operationId: spiffworkflow_backend.routes.process_instances_controller.process_instance_task_list_without_task_data_for_me
summary: returns the list of all user tasks associated with process instance without the task data summary: returns the list of all user tasks associated with process instance without the task data
responses: responses:
"200": "200":
@ -880,7 +901,7 @@ paths:
get: get:
tags: tags:
- Process Instances - Process Instances
operationId: spiffworkflow_backend.routes.process_api_blueprint.process_instance_task_list_without_task_data operationId: spiffworkflow_backend.routes.process_instances_controller.process_instance_task_list_without_task_data
summary: returns the list of all user tasks associated with process instance without the task data summary: returns the list of all user tasks associated with process instance without the task data
responses: responses:
"200": "200":
@ -915,7 +936,7 @@ paths:
get: get:
tags: tags:
- Process Instances - Process Instances
operationId: spiffworkflow_backend.routes.process_api_blueprint.process_instance_show_for_me operationId: spiffworkflow_backend.routes.process_instances_controller.process_instance_show_for_me
summary: Show information about a process instance that is associated with me summary: Show information about a process instance that is associated with me
responses: responses:
"200": "200":
@ -948,7 +969,7 @@ paths:
get: get:
tags: tags:
- Process Instances - Process Instances
operationId: spiffworkflow_backend.routes.process_api_blueprint.process_instance_show operationId: spiffworkflow_backend.routes.process_instances_controller.process_instance_show
summary: Show information about a process instance summary: Show information about a process instance
responses: responses:
"200": "200":
@ -958,7 +979,7 @@ paths:
schema: schema:
$ref: "#/components/schemas/Workflow" $ref: "#/components/schemas/Workflow"
delete: delete:
operationId: spiffworkflow_backend.routes.process_api_blueprint.process_instance_delete operationId: spiffworkflow_backend.routes.process_instances_controller.process_instance_delete
summary: Deletes a single process instance summary: Deletes a single process instance
tags: tags:
- Process Instances - Process Instances
@ -985,7 +1006,7 @@ paths:
schema: schema:
type: boolean type: boolean
post: post:
operationId: spiffworkflow_backend.routes.process_api_blueprint.process_instance_run operationId: spiffworkflow_backend.routes.process_instances_controller.process_instance_run
summary: Run a process instance summary: Run a process instance
tags: tags:
- Process Instances - Process Instances
@ -1006,7 +1027,7 @@ paths:
schema: schema:
type: integer type: integer
post: post:
operationId: spiffworkflow_backend.routes.process_api_blueprint.process_instance_terminate operationId: spiffworkflow_backend.routes.process_instances_controller.process_instance_terminate
summary: Terminate a process instance summary: Terminate a process instance
tags: tags:
- Process Instances - Process Instances
@ -1027,7 +1048,7 @@ paths:
schema: schema:
type: integer type: integer
post: post:
operationId: spiffworkflow_backend.routes.process_api_blueprint.process_instance_suspend operationId: spiffworkflow_backend.routes.process_instances_controller.process_instance_suspend
summary: Suspend a process instance summary: Suspend a process instance
tags: tags:
- Process Instances - Process Instances
@ -1048,7 +1069,7 @@ paths:
schema: schema:
type: integer type: integer
post: post:
operationId: spiffworkflow_backend.routes.process_api_blueprint.process_instance_resume operationId: spiffworkflow_backend.routes.process_instances_controller.process_instance_resume
summary: Resume a process instance summary: Resume a process instance
tags: tags:
- Process Instances - Process Instances
@ -1060,6 +1081,39 @@ paths:
schema: schema:
$ref: "#/components/schemas/OkTrue" $ref: "#/components/schemas/OkTrue"
/process-instance-reset/{modified_process_model_identifier}/{process_instance_id}/{spiff_step}:
parameters:
- name: modified_process_model_identifier
in: path
required: true
description: The modified process model id
schema:
type: string
- name: process_instance_id
in: path
required: true
description: The unique id of an existing process instance.
schema:
type: integer
- name: spiff_step
in: query
required: false
description: Reset the process to this state
schema:
type: integer
post:
operationId: spiffworkflow_backend.routes.process_instances_controller.process_instance_reset
summary: Reset a process instance to an earlier step
tags:
- Process Instances
responses:
"200":
description: Empty ok true response on successful resume.
content:
application/json:
schema:
$ref: "#/components/schemas/OkTrue"
/process-instances/reports: /process-instances/reports:
parameters: parameters:
- name: page - name: page
@ -1075,7 +1129,7 @@ paths:
schema: schema:
type: integer type: integer
get: get:
operationId: spiffworkflow_backend.routes.process_api_blueprint.process_instance_report_list operationId: spiffworkflow_backend.routes.process_instances_controller.process_instance_report_list
summary: Returns all process instance reports for process model summary: Returns all process instance reports for process model
tags: tags:
- Process Instances - Process Instances
@ -1089,7 +1143,7 @@ paths:
items: items:
$ref: "#/components/schemas/Workflow" $ref: "#/components/schemas/Workflow"
post: post:
operationId: spiffworkflow_backend.routes.process_api_blueprint.process_instance_report_create operationId: spiffworkflow_backend.routes.process_instances_controller.process_instance_report_create
summary: Returns all process instance reports for process model summary: Returns all process instance reports for process model
tags: tags:
- Process Instances - Process Instances
@ -1103,7 +1157,7 @@ paths:
/process-instances/reports/columns: /process-instances/reports/columns:
get: get:
operationId: spiffworkflow_backend.routes.process_api_blueprint.process_instance_report_column_list operationId: spiffworkflow_backend.routes.process_instances_controller.process_instance_report_column_list
summary: Returns all available columns for a process instance report. summary: Returns all available columns for a process instance report.
tags: tags:
- Process Instances - Process Instances
@ -1138,7 +1192,7 @@ paths:
schema: schema:
type: integer type: integer
get: get:
operationId: spiffworkflow_backend.routes.process_api_blueprint.process_instance_report_show operationId: spiffworkflow_backend.routes.process_instances_controller.process_instance_report_show
summary: Returns a report of process instances for a given process model summary: Returns a report of process instances for a given process model
tags: tags:
- Process Instances - Process Instances
@ -1152,7 +1206,7 @@ paths:
items: items:
$ref: "#/components/schemas/Workflow" $ref: "#/components/schemas/Workflow"
put: put:
operationId: spiffworkflow_backend.routes.process_api_blueprint.process_instance_report_update operationId: spiffworkflow_backend.routes.process_instances_controller.process_instance_report_update
summary: Updates a process instance report summary: Updates a process instance report
tags: tags:
- Process Instances - Process Instances
@ -1164,7 +1218,7 @@ paths:
schema: schema:
$ref: "#/components/schemas/OkTrue" $ref: "#/components/schemas/OkTrue"
delete: delete:
operationId: spiffworkflow_backend.routes.process_api_blueprint.process_instance_report_delete operationId: spiffworkflow_backend.routes.process_instances_controller.process_instance_report_delete
summary: Delete a process instance report summary: Delete a process instance report
tags: tags:
- Process Instances - Process Instances
@ -1191,7 +1245,7 @@ paths:
schema: schema:
type: string type: string
get: get:
operationId: spiffworkflow_backend.routes.process_api_blueprint.get_file operationId: spiffworkflow_backend.routes.process_models_controller.process_model_file_show
summary: Returns metadata about the file summary: Returns metadata about the file
tags: tags:
- Process Model Files - Process Model Files
@ -1203,7 +1257,7 @@ paths:
schema: schema:
$ref: "#/components/schemas/File" $ref: "#/components/schemas/File"
put: put:
operationId: spiffworkflow_backend.routes.process_api_blueprint.process_model_file_update operationId: spiffworkflow_backend.routes.process_models_controller.process_model_file_update
summary: save the contents to the given file summary: save the contents to the given file
tags: tags:
- Process Model Files - Process Model Files
@ -1226,7 +1280,7 @@ paths:
schema: schema:
$ref: "#/components/schemas/OkTrue" $ref: "#/components/schemas/OkTrue"
delete: delete:
operationId: spiffworkflow_backend.routes.process_api_blueprint.process_model_file_delete operationId: spiffworkflow_backend.routes.process_models_controller.process_model_file_delete
summary: Removes an existing process model file summary: Removes an existing process model file
tags: tags:
- Process Model Files - Process Model Files
@ -1255,8 +1309,7 @@ paths:
get: get:
tags: tags:
- Tasks - Tasks
# security: [] operationId: spiffworkflow_backend.routes.tasks_controller.task_list_my_tasks
operationId: spiffworkflow_backend.routes.process_api_blueprint.task_list_my_tasks
summary: returns the list of ready or waiting tasks for a user summary: returns the list of ready or waiting tasks for a user
responses: responses:
"200": "200":
@ -1285,7 +1338,7 @@ paths:
get: get:
tags: tags:
- Process Instances - Process Instances
operationId: spiffworkflow_backend.routes.process_api_blueprint.task_list_for_my_open_processes operationId: spiffworkflow_backend.routes.tasks_controller.task_list_for_my_open_processes
summary: returns the list of tasks for given user's open process instances summary: returns the list of tasks for given user's open process instances
responses: responses:
"200": "200":
@ -1314,7 +1367,7 @@ paths:
get: get:
tags: tags:
- Process Instances - Process Instances
operationId: spiffworkflow_backend.routes.process_api_blueprint.task_list_for_me operationId: spiffworkflow_backend.routes.tasks_controller.task_list_for_me
summary: returns the list of tasks for given user's open process instances summary: returns the list of tasks for given user's open process instances
responses: responses:
"200": "200":
@ -1349,7 +1402,7 @@ paths:
get: get:
tags: tags:
- Process Instances - Process Instances
operationId: spiffworkflow_backend.routes.process_api_blueprint.task_list_for_my_groups operationId: spiffworkflow_backend.routes.tasks_controller.task_list_for_my_groups
summary: returns the list of tasks for given user's open process instances summary: returns the list of tasks for given user's open process instances
responses: responses:
"200": "200":
@ -1361,11 +1414,34 @@ paths:
items: items:
$ref: "#/components/schemas/Task" $ref: "#/components/schemas/Task"
/users/search:
parameters:
- name: username_prefix
in: query
required: true
description: The prefix of the user
schema:
type: string
get:
tags:
- Users
operationId: spiffworkflow_backend.routes.users_controller.user_search
summary: Returns a list of users that the search param
responses:
"200":
description: list of users
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/User"
/user-groups/for-current-user: /user-groups/for-current-user:
get: get:
tags: tags:
- Process Instances - User Groups
operationId: spiffworkflow_backend.routes.process_api_blueprint.user_group_list_for_current_user operationId: spiffworkflow_backend.routes.users_controller.user_group_list_for_current_user
summary: Group identifiers for current logged in user summary: Group identifiers for current logged in user
responses: responses:
"200": "200":
@ -1406,7 +1482,7 @@ paths:
get: get:
tags: tags:
- Process Instances - Process Instances
operationId: spiffworkflow_backend.routes.process_api_blueprint.process_instance_task_list_with_task_data operationId: spiffworkflow_backend.routes.process_instances_controller.process_instance_task_list_with_task_data
summary: returns the list of all user tasks associated with process instance with the task data summary: returns the list of all user tasks associated with process instance with the task data
responses: responses:
"200": "200":
@ -1439,7 +1515,7 @@ paths:
schema: schema:
type: string type: string
put: put:
operationId: spiffworkflow_backend.routes.process_api_blueprint.update_task_data operationId: spiffworkflow_backend.routes.process_api_blueprint.task_data_update
summary: Update the task data for requested instance and task summary: Update the task data for requested instance and task
tags: tags:
- Process Instances - Process Instances
@ -1451,11 +1527,104 @@ paths:
schema: schema:
$ref: "#/components/schemas/Workflow" $ref: "#/components/schemas/Workflow"
/process-data/{modified_process_model_identifier}/{process_instance_id}/{process_data_identifier}:
parameters:
- name: modified_process_model_identifier
in: path
required: true
description: The modified id of an existing process model
schema:
type: string
- name: process_instance_id
in: path
required: true
description: The unique id of an existing process instance.
schema:
type: integer
- name: process_data_identifier
in: path
required: true
description: The identifier of the process data.
schema:
type: string
get:
operationId: spiffworkflow_backend.routes.process_api_blueprint.process_data_show
summary: Fetch the process data value.
tags:
- Data Objects
responses:
"200":
description: Fetch succeeded.
content:
application/json:
schema:
$ref: "#/components/schemas/Workflow"
/send-event/{modified_process_model_identifier}/{process_instance_id}:
parameters:
- name: modified_process_model_identifier
in: path
required: true
description: The modified id of an existing process model
schema:
type: string
- name: process_instance_id
in: path
required: true
description: The unique id of the process instance
schema:
type: string
post:
operationId: spiffworkflow_backend.routes.process_api_blueprint.send_bpmn_event
summary: Send a BPMN event to the process
tags:
- Process Instances
responses:
"200":
description: Event Sent Successfully
content:
application/json:
schema:
$ref: "#/components/schemas/Workflow"
/task-complete/{modified_process_model_identifier}/{process_instance_id}/{task_id}:
parameters:
- name: modified_process_model_identifier
in: path
required: true
description: The modified id of an existing process model
schema:
type: string
- name: process_instance_id
in: path
required: true
description: The unique id of the process instance
schema:
type: string
- name: task_id
in: path
required: true
description: The unique id of the task.
schema:
type: string
post:
operationId: spiffworkflow_backend.routes.process_api_blueprint.manual_complete_task
summary: Mark a task complete without executing it
tags:
- Process Instances
responses:
"200":
description: Event Sent Successfully
content:
application/json:
schema:
$ref: "#/components/schemas/Workflow"
/service-tasks: /service-tasks:
get: get:
tags: tags:
- Service Tasks - Service Tasks
operationId: spiffworkflow_backend.routes.process_api_blueprint.service_task_list operationId: spiffworkflow_backend.routes.service_tasks_controller.service_task_list
summary: Gets all available service task connectors summary: Gets all available service task connectors
responses: responses:
"200": "200":
@ -1469,7 +1638,7 @@ paths:
get: get:
tags: tags:
- Authentications - Authentications
operationId: spiffworkflow_backend.routes.process_api_blueprint.authentication_list operationId: spiffworkflow_backend.routes.service_tasks_controller.authentication_list
summary: Gets all available authentications from connector proxy summary: Gets all available authentications from connector proxy
responses: responses:
"200": "200":
@ -1506,11 +1675,9 @@ paths:
schema: schema:
type: string type: string
get: get:
# disable security so we can get the token from query params instead
security: []
tags: tags:
- Authentications - Authentications
operationId: spiffworkflow_backend.routes.process_api_blueprint.authentication_callback operationId: spiffworkflow_backend.routes.service_tasks_controller.authentication_callback
summary: Callback to backend summary: Callback to backend
responses: responses:
"200": "200":
@ -1543,7 +1710,7 @@ paths:
get: get:
tags: tags:
- Tasks - Tasks
operationId: spiffworkflow_backend.routes.process_api_blueprint.task_show operationId: spiffworkflow_backend.routes.tasks_controller.task_show
summary: Gets one task that a user wants to complete summary: Gets one task that a user wants to complete
responses: responses:
"200": "200":
@ -1555,7 +1722,7 @@ paths:
put: put:
tags: tags:
- Tasks - Tasks
operationId: spiffworkflow_backend.routes.process_api_blueprint.task_submit operationId: spiffworkflow_backend.routes.tasks_controller.task_submit
summary: Update the form data for a tasks summary: Update the form data for a tasks
requestBody: requestBody:
content: content:
@ -1599,7 +1766,7 @@ paths:
get: get:
tags: tags:
- Messages - Messages
operationId: spiffworkflow_backend.routes.process_api_blueprint.message_instance_list operationId: spiffworkflow_backend.routes.messages_controller.message_instance_list
summary: Get a list of message instances summary: Get a list of message instances
responses: responses:
"200": "200":
@ -1620,7 +1787,7 @@ paths:
post: post:
tags: tags:
- Messages - Messages
operationId: spiffworkflow_backend.routes.process_api_blueprint.message_start operationId: spiffworkflow_backend.routes.messages_controller.message_start
summary: Instantiate and run a given process model with a message start event matching given identifier summary: Instantiate and run a given process model with a message start event matching given identifier
requestBody: requestBody:
content: content:
@ -1664,7 +1831,7 @@ paths:
get: get:
tags: tags:
- Process Instances - Process Instances
operationId: spiffworkflow_backend.routes.process_api_blueprint.process_instance_log_list operationId: spiffworkflow_backend.routes.process_instances_controller.process_instance_log_list
summary: returns a list of logs associated with the process instance summary: returns a list of logs associated with the process instance
responses: responses:
"200": "200":
@ -1689,7 +1856,7 @@ paths:
schema: schema:
type: integer type: integer
post: post:
operationId: spiffworkflow_backend.routes.process_api_blueprint.add_secret operationId: spiffworkflow_backend.routes.secrets_controller.secret_create
summary: Create a secret for a key and value summary: Create a secret for a key and value
tags: tags:
- Secrets - Secrets
@ -1706,7 +1873,7 @@ paths:
schema: schema:
type: number type: number
get: get:
operationId: spiffworkflow_backend.routes.process_api_blueprint.secret_list operationId: spiffworkflow_backend.routes.secrets_controller.secret_list
summary: Return list of all secrets summary: Return list of all secrets
tags: tags:
- Secrets - Secrets
@ -1727,7 +1894,7 @@ paths:
schema: schema:
type: string type: string
get: get:
operationId: spiffworkflow_backend.routes.process_api_blueprint.get_secret operationId: spiffworkflow_backend.routes.secrets_controller.secret_show
summary: Return a secret value for a key summary: Return a secret value for a key
tags: tags:
- Secrets - Secrets
@ -1739,7 +1906,7 @@ paths:
schema: schema:
$ref: "#/components/schemas/Secret" $ref: "#/components/schemas/Secret"
delete: delete:
operationId: spiffworkflow_backend.routes.process_api_blueprint.delete_secret operationId: spiffworkflow_backend.routes.secrets_controller.secret_delete
summary: Delete an existing secret summary: Delete an existing secret
tags: tags:
- Secrets - Secrets
@ -1751,7 +1918,7 @@ paths:
"404": "404":
description: Secret does not exist description: Secret does not exist
put: put:
operationId: spiffworkflow_backend.routes.process_api_blueprint.update_secret operationId: spiffworkflow_backend.routes.secrets_controller.secret_update
summary: Modify an existing secret summary: Modify an existing secret
tags: tags:
- Secrets - Secrets
@ -1810,16 +1977,6 @@ components:
scopes: scopes:
read_email: read email read_email: read email
x-tokenInfoFunc: spiffworkflow_backend.routes.user.get_scope x-tokenInfoFunc: spiffworkflow_backend.routes.user.get_scope
# oAuth2AuthCode:
# type: oauth2
# description: authenticate with openid server
# flows:
# implicit:
# authorizationUrl: /v1.0/login_api
# scopes:
# uid: uid
# x-tokenInfoUrl: localhost:7000/v1.0/login_api_return
# x-tokenInfoFunc: spiffworkflow_backend.routes.user.get_scope
schemas: schemas:
OkTrue: OkTrue:

View File

@ -17,21 +17,21 @@ def setup_database_uri(app: Flask) -> None:
if app.config.get("SPIFFWORKFLOW_BACKEND_DATABASE_URI") is None: if app.config.get("SPIFFWORKFLOW_BACKEND_DATABASE_URI") is None:
database_name = f"spiffworkflow_backend_{app.config['ENV_IDENTIFIER']}" database_name = f"spiffworkflow_backend_{app.config['ENV_IDENTIFIER']}"
if app.config.get("SPIFF_DATABASE_TYPE") == "sqlite": if app.config.get("SPIFF_DATABASE_TYPE") == "sqlite":
app.config[ app.config["SQLALCHEMY_DATABASE_URI"] = (
"SQLALCHEMY_DATABASE_URI" f"sqlite:///{app.instance_path}/db_{app.config['ENV_IDENTIFIER']}.sqlite3"
] = f"sqlite:///{app.instance_path}/db_{app.config['ENV_IDENTIFIER']}.sqlite3" )
elif app.config.get("SPIFF_DATABASE_TYPE") == "postgres": elif app.config.get("SPIFF_DATABASE_TYPE") == "postgres":
app.config[ app.config["SQLALCHEMY_DATABASE_URI"] = (
"SQLALCHEMY_DATABASE_URI" f"postgresql://spiffworkflow_backend:spiffworkflow_backend@localhost:5432/{database_name}"
] = f"postgresql://spiffworkflow_backend:spiffworkflow_backend@localhost:5432/{database_name}" )
else: else:
# use pswd to trick flake8 with hardcoded passwords # use pswd to trick flake8 with hardcoded passwords
db_pswd = os.environ.get("DB_PASSWORD") db_pswd = os.environ.get("DB_PASSWORD")
if db_pswd is None: if db_pswd is None:
db_pswd = "" db_pswd = ""
app.config[ app.config["SQLALCHEMY_DATABASE_URI"] = (
"SQLALCHEMY_DATABASE_URI" f"mysql+mysqlconnector://root:{db_pswd}@localhost/{database_name}"
] = f"mysql+mysqlconnector://root:{db_pswd}@localhost/{database_name}" )
else: else:
app.config["SQLALCHEMY_DATABASE_URI"] = app.config.get( app.config["SQLALCHEMY_DATABASE_URI"] = app.config.get(
"SPIFFWORKFLOW_BACKEND_DATABASE_URI" "SPIFFWORKFLOW_BACKEND_DATABASE_URI"
@ -42,6 +42,7 @@ def load_config_file(app: Flask, env_config_module: str) -> None:
"""Load_config_file.""" """Load_config_file."""
try: try:
app.config.from_object(env_config_module) app.config.from_object(env_config_module)
print(f"loaded config: {env_config_module}")
except ImportStringError as exception: except ImportStringError as exception:
if os.environ.get("TERRAFORM_DEPLOYED_ENVIRONMENT") != "true": if os.environ.get("TERRAFORM_DEPLOYED_ENVIRONMENT") != "true":
raise ModuleNotFoundError( raise ModuleNotFoundError(
@ -62,6 +63,7 @@ def setup_config(app: Flask) -> None:
) )
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config.from_object("spiffworkflow_backend.config.default") app.config.from_object("spiffworkflow_backend.config.default")
print("loaded config: default")
env_config_prefix = "spiffworkflow_backend.config." env_config_prefix = "spiffworkflow_backend.config."
if ( if (
@ -69,6 +71,7 @@ def setup_config(app: Flask) -> None:
and os.environ.get("SPIFFWORKFLOW_BACKEND_ENV") is not None and os.environ.get("SPIFFWORKFLOW_BACKEND_ENV") is not None
): ):
load_config_file(app, f"{env_config_prefix}terraform_deployed_environment") load_config_file(app, f"{env_config_prefix}terraform_deployed_environment")
print("loaded config: terraform_deployed_environment")
env_config_module = env_config_prefix + app.config["ENV_IDENTIFIER"] env_config_module = env_config_prefix + app.config["ENV_IDENTIFIER"]
load_config_file(app, env_config_module) load_config_file(app, env_config_module)
@ -87,6 +90,14 @@ def setup_config(app: Flask) -> None:
"permissions", "permissions",
app.config["SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME"], app.config["SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME"],
) )
print(
"set permissions file name config:"
f" {app.config['SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME']}"
)
print(
"set permissions file name full path:"
f" {app.config['PERMISSIONS_FILE_FULLPATH']}"
)
# unversioned (see .gitignore) config that can override everything and include secrets. # unversioned (see .gitignore) config that can override everything and include secrets.
# src/spiffworkflow_backend/config/secrets.py # src/spiffworkflow_backend/config/secrets.py

View File

@ -6,3 +6,4 @@ GIT_USERNAME = environ.get("GIT_USERNAME", default="sartography-automated-commit
GIT_USER_EMAIL = environ.get( GIT_USER_EMAIL = environ.get(
"GIT_USER_EMAIL", default="sartography-automated-committer@users.noreply.github.com" "GIT_USER_EMAIL", default="sartography-automated-committer@users.noreply.github.com"
) )
SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME = "dev.yml"

View File

@ -1,13 +1,10 @@
groups: groups:
admin: admin:
users: [ciadmin1] users: [ciadmin1@spiffworkflow.org]
common-user:
users: [ciuser1]
permissions: permissions:
admin: admin:
groups: [admin, common-user] groups: [admin]
users: [] users: []
allowed_permissions: [create, read, update, delete] allowed_permissions: [create, read, update, delete]
uri: /* uri: /*

View File

@ -0,0 +1,151 @@
default_group: everybody
groups:
admin:
users:
[
admin@spiffworkflow.org,
jakub@status.im,
jarrad@status.im,
kb@sartography.com,
alex@sartography.com,
dan@sartography.com,
mike@sartography.com,
jason@sartography.com,
j@sartography.com,
elizabeth@sartography.com,
jon@sartography.com,
]
Finance Team:
users:
[
jakub@status.im,
amir@status.im,
jarrad@status.im,
sasha@status.im,
fin@status.im,
fin1@status.im,
alex@sartography.com,
dan@sartography.com,
mike@sartography.com,
jason@sartography.com,
j@sartography.com,
elizabeth@sartography.com,
jon@sartography.com,
]
demo:
users:
[
harmeet@status.im,
sasha@status.im,
manuchehr@status.im,
core@status.im,
fin@status.im,
fin1@status.im,
lead@status.im,
lead1@status.im,
]
test:
users:
[
natalia@sartography.com,
]
permissions:
admin:
groups: [admin]
users: []
allowed_permissions: [create, read, update, delete]
uri: /*
# open system defaults for everybody
read-all-process-groups:
groups: [everybody]
users: []
allowed_permissions: [read]
uri: /process-groups/*
read-all-process-models:
groups: [everybody]
users: []
allowed_permissions: [read]
uri: /process-models/*
# basic perms for everybody
read-all-process-instances-for-me:
groups: [everybody]
users: []
allowed_permissions: [read]
uri: /process-instances/for-me/*
read-process-instance-reports:
groups: [everybody]
users: []
allowed_permissions: [create, read, update, delete]
uri: /process-instances/reports/*
processes-read:
groups: [everybody]
users: []
allowed_permissions: [read]
uri: /processes
service-tasks:
groups: [everybody]
users: []
allowed_permissions: [read]
uri: /service-tasks
tasks-crud:
groups: [everybody]
users: []
allowed_permissions: [create, read, update, delete]
uri: /tasks/*
user-groups-for-current-user:
groups: [everybody]
users: []
allowed_permissions: [read]
uri: /user-groups/for-current-user
finance-admin:
groups: ["Finance Team"]
users: []
allowed_permissions: [create, read, update, delete]
uri: /process-groups/manage-procurement:procurement:*
manage-revenue-streams-instances:
groups: ["demo"]
users: []
allowed_permissions: [create]
uri: /process-instances/manage-revenue-streams:product-revenue-streams:customer-contracts-trade-terms/*
manage-procurement-invoice-instances:
groups: ["demo"]
users: []
allowed_permissions: [create]
uri: /process-instances/manage-procurement:procurement:core-contributor-invoice-management:*
manage-procurement-instances:
groups: ["demo"]
users: []
allowed_permissions: [create]
uri: /process-instances/manage-procurement:vendor-lifecycle-management:*
manage-revenue-streams-instances-for-me:
groups: ["demo"]
users: []
allowed_permissions: [read]
uri: /process-instances/for-me/manage-revenue-streams:product-revenue-streams:customer-contracts-trade-terms/*
manage-procurement-invoice-instances-for-me:
groups: ["demo"]
users: []
allowed_permissions: [read]
uri: /process-instances/for-me/manage-procurement:procurement:core-contributor-invoice-management:*
manage-procurement-instances-for-me:
groups: ["demo"]
users: []
allowed_permissions: [read]
uri: /process-instances/for-me/manage-procurement:vendor-lifecycle-management:*
create-test-instances:
groups: ["test"]
users: []
allowed_permissions: [create, read]
uri: /process-instances/misc:test:*

View File

@ -10,72 +10,68 @@ groups:
admin: admin:
users: users:
[ [
admin, admin@spiffworkflow.org,
jakub, jakub@status.im,
kb, jarrad@status.im,
alex, kb@sartography.com,
dan, alex@sartography.com,
mike, dan@sartography.com,
jason, mike@sartography.com,
jarrad, jason@sartography.com,
elizabeth, j@sartography.com,
jon, elizabeth@sartography.com,
jon@sartography.com,
] ]
Finance Team: Finance Team:
users: users:
[ [
jakub, jakub@status.im,
alex, amir@status.im,
dan, jarrad@status.im,
mike, sasha@status.im,
jason, fin@status.im,
amir, fin1@status.im,
jarrad, alex@sartography.com,
elizabeth, dan@sartography.com,
jon, mike@sartography.com,
sasha, jason@sartography.com,
fin, j@sartography.com,
fin1, elizabeth@sartography.com,
jon@sartography.com,
] ]
demo: demo:
users: users:
[ [
core, harmeet@status.im,
fin, sasha@status.im,
fin1, manuchehr@status.im,
harmeet, core@status.im,
jason, fin@status.im,
sasha, fin1@status.im,
manuchehr, lead@status.im,
lead, lead1@status.im,
lead1
] ]
core-contributor: test:
users: users:
[ [
core, natalia@sartography.com,
harmeet,
] ]
admin-ro: admin-ro:
users: users:
[ [
j, j@sartography.com,
] ]
test:
users: [natalia]
permissions: permissions:
admin: admin:
groups: [admin] groups: [admin]
users: [] users: []
allowed_permissions: [create, read, update, delete] allowed_permissions: [create, read, update, delete]
uri: /* uri: /*
admin-readonly: admin-readonly:
groups: [admin-ro] groups: [admin-ro]
users: [] users: []
@ -85,121 +81,93 @@ permissions:
groups: [admin-ro] groups: [admin-ro]
users: [] users: []
allowed_permissions: [create, read, update, delete] allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-instances/* uri: /process-instances/*
tasks-crud: # open system defaults for everybody
groups: [everybody]
users: []
allowed_permissions: [create, read, update, delete]
uri: /v1.0/tasks/*
service-tasks:
groups: [everybody]
users: []
allowed_permissions: [read]
uri: /v1.0/service-tasks
user-groups-for-current-user:
groups: [everybody]
users: []
allowed_permissions: [read]
uri: /v1.0/user-groups/for-current-user
# read all for everybody
read-all-process-groups: read-all-process-groups:
groups: [everybody] groups: [everybody]
users: [] users: []
allowed_permissions: [read] allowed_permissions: [read]
uri: /v1.0/process-groups/* uri: /process-groups/*
read-all-process-models: read-all-process-models:
groups: [everybody] groups: [everybody]
users: [] users: []
allowed_permissions: [read] allowed_permissions: [read]
uri: /v1.0/process-models/* uri: /process-models/*
# basic perms for everybody
read-all-process-instances-for-me: read-all-process-instances-for-me:
groups: [everybody] groups: [everybody]
users: [] users: []
allowed_permissions: [read] allowed_permissions: [read]
uri: /v1.0/process-instances/for-me/* uri: /process-instances/for-me/*
read-process-instance-reports: read-process-instance-reports:
groups: [everybody] groups: [everybody]
users: [] users: []
allowed_permissions: [create, read, update, delete] allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-instances/reports/* uri: /process-instances/reports/*
processes-read: processes-read:
groups: [everybody] groups: [everybody]
users: [] users: []
allowed_permissions: [read] allowed_permissions: [read]
uri: /v1.0/processes uri: /processes
service-tasks:
groups: [everybody]
users: []
allowed_permissions: [read]
uri: /service-tasks
tasks-crud:
groups: [everybody]
users: []
allowed_permissions: [create, read, update, delete]
uri: /tasks/*
user-groups-for-current-user:
groups: [everybody]
users: []
allowed_permissions: [read]
uri: /user-groups/for-current-user
manage-procurement-admin:
groups: ["Project Lead"]
users: []
allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-groups/manage-procurement:*
manage-procurement-admin-slash:
groups: ["Project Lead"]
users: []
allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-groups/manage-procurement/*
manage-procurement-admin-models:
groups: ["Project Lead"]
users: []
allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-models/manage-procurement:*
manage-procurement-admin-models-slash:
groups: ["Project Lead"]
users: []
allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-models/manage-procurement/*
manage-procurement-admin-instances:
groups: ["Project Lead"]
users: []
allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-instances/manage-procurement:*
manage-procurement-admin-instances-slash:
groups: ["Project Lead"]
users: []
allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-instances/manage-procurement/*
finance-admin: finance-admin:
groups: ["Finance Team"] groups: ["Finance Team"]
users: [] users: []
allowed_permissions: [create, read, update, delete] allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-groups/manage-procurement:procurement:* uri: /process-groups/manage-procurement:procurement:*
manage-revenue-streams-instances: manage-revenue-streams-instances:
groups: ["core-contributor", "demo"] groups: ["demo"]
users: [] users: []
allowed_permissions: [create, read] allowed_permissions: [create]
uri: /v1.0/process-instances/manage-revenue-streams:product-revenue-streams:customer-contracts-trade-terms/* uri: /process-instances/manage-revenue-streams:product-revenue-streams:customer-contracts-trade-terms/*
manage-procurement-invoice-instances: manage-procurement-invoice-instances:
groups: ["core-contributor", "demo"] groups: ["demo"]
users: [] users: []
allowed_permissions: [create, read] allowed_permissions: [create]
uri: /v1.0/process-instances/manage-procurement:procurement:core-contributor-invoice-management:* uri: /process-instances/manage-procurement:procurement:core-contributor-invoice-management:*
manage-procurement-instances: manage-procurement-instances:
groups: ["core-contributor", "demo"] groups: ["demo"]
users: [] users: []
allowed_permissions: [create, read] allowed_permissions: [create]
uri: /v1.0/process-instances/manage-procurement:vendor-lifecycle-management:* uri: /process-instances/manage-procurement:vendor-lifecycle-management:*
manage-revenue-streams-instances-for-me:
groups: ["demo"]
users: []
allowed_permissions: [read]
uri: /process-instances/for-me/manage-revenue-streams:product-revenue-streams:customer-contracts-trade-terms/*
manage-procurement-invoice-instances-for-me:
groups: ["demo"]
users: []
allowed_permissions: [read]
uri: /process-instances/for-me/manage-procurement:procurement:core-contributor-invoice-management:*
manage-procurement-instances-for-me:
groups: ["demo"]
users: []
allowed_permissions: [read]
uri: /process-instances/for-me/manage-procurement:vendor-lifecycle-management:*
create-test-instances: create-test-instances:
groups: ["test"] groups: ["test"]
users: [] users: []
allowed_permissions: [create, read] allowed_permissions: [create, read]
uri: /v1.0/process-instances/misc:test:* uri: /process-instances/misc:test:*
core1-admin-instances:
groups: ["core-contributor", "Finance Team"]
users: []
allowed_permissions: [create, read]
uri: /v1.0/process-instances/misc:category_number_one:process-model-with-form:*
core1-admin-instances-slash:
groups: ["core-contributor", "Finance Team"]
users: []
allowed_permissions: [create, read]
uri: /v1.0/process-instances/misc:category_number_one:process-model-with-form/*

View File

@ -2,14 +2,17 @@ default_group: everybody
users: users:
admin: admin:
service: local_open_id
email: admin@spiffworkflow.org email: admin@spiffworkflow.org
password: admin password: admin
preferred_username: Admin preferred_username: Admin
nelson: nelson:
service: local_open_id
email: nelson@spiffworkflow.org email: nelson@spiffworkflow.org
password: nelson password: nelson
preferred_username: Nelson preferred_username: Nelson
malala: malala:
service: local_open_id
email: malala@spiffworkflow.org email: malala@spiffworkflow.org
password: malala password: malala
preferred_username: Malala preferred_username: Malala
@ -18,17 +21,17 @@ groups:
admin: admin:
users: users:
[ [
admin, admin@spiffworkflow.org,
] ]
Education: Education:
users: users:
[ [
malala malala@spiffworkflow.org
] ]
President: President:
users: users:
[ [
nelson nelson@spiffworkflow.org
] ]
permissions: permissions:
@ -44,45 +47,44 @@ permissions:
groups: [everybody] groups: [everybody]
users: [] users: []
allowed_permissions: [create, read, update, delete] allowed_permissions: [create, read, update, delete]
uri: /v1.0/tasks/* uri: /tasks/*
# Everyone can see everything (all groups, and processes are visible) # Everyone can see everything (all groups, and processes are visible)
read-all-process-groups: read-all-process-groups:
groups: [ everybody ] groups: [ everybody ]
users: [ ] users: [ ]
allowed_permissions: [ read ] allowed_permissions: [ read ]
uri: /v1.0/process-groups/* uri: /process-groups/*
read-all-process-models: read-all-process-models:
groups: [ everybody ] groups: [ everybody ]
users: [ ] users: [ ]
allowed_permissions: [ read ] allowed_permissions: [ read ]
uri: /v1.0/process-models/* uri: /process-models/*
read-all-process-instance: read-all-process-instance:
groups: [ everybody ] groups: [ everybody ]
users: [ ] users: [ ]
allowed_permissions: [ read ] allowed_permissions: [ read ]
uri: /v1.0/process-instances/* uri: /process-instances/*
read-process-instance-reports: read-process-instance-reports:
groups: [ everybody ] groups: [ everybody ]
users: [ ] users: [ ]
allowed_permissions: [ read ] allowed_permissions: [ read ]
uri: /v1.0/process-instances/reports/* uri: /process-instances/reports/*
processes-read: processes-read:
groups: [ everybody ] groups: [ everybody ]
users: [ ] users: [ ]
allowed_permissions: [ read ] allowed_permissions: [ read ]
uri: /v1.0/processes uri: /processes
# Members of the Education group can change the processes under "education".
# Members of the Education group can change they processes work.
education-admin: education-admin:
groups: ["Education", "President"] groups: ["Education", "President"]
users: [] users: []
allowed_permissions: [create, read, update, delete] allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-groups/education:* uri: /process-groups/education:*
# Anyone can start an education process. # Anyone can start an education process.
education-everybody: education-everybody:
groups: [everybody] groups: [everybody]
users: [] users: []
allowed_permissions: [create, read] allowed_permissions: [create, read]
uri: /v1.0/process-instances/misc:category_number_one:process-model-with-form/* uri: /process-instances/misc:category_number_one:process-model-with-form/*

View File

@ -0,0 +1,12 @@
default_group: everybody
groups:
admin:
users: [admin@spiffworkflow.org]
permissions:
admin:
groups: [admin]
users: []
allowed_permissions: [create, read, update, delete]
uri: /*

View File

@ -4,57 +4,53 @@ groups:
admin: admin:
users: users:
[ [
admin, admin@spiffworkflow.org,
jakub, jakub@status.im,
kb, jarrad@status.im,
alex, kb@sartography.com,
dan, alex@sartography.com,
mike, dan@sartography.com,
jason, mike@sartography.com,
j, jason@sartography.com,
jarrad, j@sartography.com,
elizabeth, elizabeth@sartography.com,
jon, jon@sartography.com,
natalia,
] ]
Finance Team: Finance Team:
users: users:
[ [
jakub, jakub@status.im,
alex, amir@status.im,
dan, jarrad@status.im,
mike, sasha@status.im,
jason, fin@status.im,
j, fin1@status.im,
amir, alex@sartography.com,
jarrad, dan@sartography.com,
elizabeth, mike@sartography.com,
jon, jason@sartography.com,
natalia, j@sartography.com,
sasha, elizabeth@sartography.com,
fin, jon@sartography.com,
fin1,
] ]
demo: demo:
users: users:
[ [
core, harmeet@status.im,
fin, sasha@status.im,
fin1, manuchehr@status.im,
harmeet, core@status.im,
sasha, fin@status.im,
manuchehr, fin1@status.im,
lead, lead@status.im,
lead1 lead1@status.im,
] ]
test:
core-contributor:
users: users:
[ [
core, natalia@sartography.com,
harmeet,
] ]
permissions: permissions:
@ -67,104 +63,86 @@ permissions:
groups: [admin] groups: [admin]
users: [] users: []
allowed_permissions: [create, read, update, delete] allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-instances/* uri: /process-instances/*
tasks-crud: # open system defaults for everybody
groups: [everybody]
users: []
allowed_permissions: [create, read, update, delete]
uri: /v1.0/tasks/*
service-tasks:
groups: [everybody]
users: []
allowed_permissions: [read]
uri: /v1.0/service-tasks
user-groups-for-current-user:
groups: [everybody]
users: []
allowed_permissions: [read]
uri: /v1.0/user-groups/for-current-user
# read all for everybody
read-all-process-groups: read-all-process-groups:
groups: [everybody] groups: [everybody]
users: [] users: []
allowed_permissions: [read] allowed_permissions: [read]
uri: /v1.0/process-groups/* uri: /process-groups/*
read-all-process-models: read-all-process-models:
groups: [everybody] groups: [everybody]
users: [] users: []
allowed_permissions: [read] allowed_permissions: [read]
uri: /v1.0/process-models/* uri: /process-models/*
# basic perms for everybody
read-all-process-instances-for-me: read-all-process-instances-for-me:
groups: [everybody] groups: [everybody]
users: [] users: []
allowed_permissions: [read] allowed_permissions: [read]
uri: /v1.0/process-instances/for-me/* uri: /process-instances/for-me/*
manage-process-instance-reports: read-process-instance-reports:
groups: [everybody] groups: [everybody]
users: [] users: []
allowed_permissions: [create, read, update, delete] allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-instances/reports/* uri: /process-instances/reports/*
processes-read: processes-read:
groups: [everybody] groups: [everybody]
users: [] users: []
allowed_permissions: [read] allowed_permissions: [read]
uri: /v1.0/processes uri: /processes
service-tasks:
groups: [everybody]
manage-procurement-admin-instances:
groups: ["Project Lead"]
users: []
allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-instances/manage-procurement:*
manage-procurement-admin-instances-slash:
groups: ["Project Lead"]
users: []
allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-instances/manage-procurement/*
manage-procurement-admin-instance-logs:
groups: ["Project Lead"]
users: [] users: []
allowed_permissions: [read] allowed_permissions: [read]
uri: /v1.0/logs/manage-procurement:* uri: /service-tasks
manage-procurement-admin-instance-logs-slash: tasks-crud:
groups: ["Project Lead"] groups: [everybody]
users: []
allowed_permissions: [create, read, update, delete]
uri: /tasks/*
user-groups-for-current-user:
groups: [everybody]
users: [] users: []
allowed_permissions: [read] allowed_permissions: [read]
uri: /v1.0/logs/manage-procurement/* uri: /user-groups/for-current-user
manage-revenue-streams-instances: manage-revenue-streams-instances:
groups: ["core-contributor", "demo"] groups: ["demo"]
users: [] users: []
allowed_permissions: [create, read] allowed_permissions: [create]
uri: /v1.0/process-instances/manage-revenue-streams:product-revenue-streams:customer-contracts-trade-terms/* uri: /process-instances/manage-revenue-streams:product-revenue-streams:customer-contracts-trade-terms/*
manage-revenue-streams-instance-logs:
groups: ["core-contributor", "demo"]
users: []
allowed_permissions: [read]
uri: /v1.0/logs/manage-revenue-streams:product-revenue-streams:customer-contracts-trade-terms/*
manage-procurement-invoice-instances: manage-procurement-invoice-instances:
groups: ["core-contributor", "demo"] groups: ["demo"]
users: [] users: []
allowed_permissions: [create, read] allowed_permissions: [create]
uri: /v1.0/process-instances/manage-procurement:procurement:core-contributor-invoice-management:* uri: /process-instances/manage-procurement:procurement:core-contributor-invoice-management:*
manage-procurement-invoice-instance-logs:
groups: ["core-contributor", "demo"]
users: []
allowed_permissions: [read]
uri: /v1.0/logs/manage-procurement:procurement:core-contributor-invoice-management:*
manage-procurement-instances: manage-procurement-instances:
groups: ["core-contributor", "demo"] groups: ["demo"]
users: [] users: []
allowed_permissions: [create, read] allowed_permissions: [create]
uri: /v1.0/process-instances/manage-procurement:vendor-lifecycle-management:* uri: /process-instances/manage-procurement:vendor-lifecycle-management:*
manage-procurement-instance-logs:
groups: ["core-contributor", "demo"] manage-revenue-streams-instances-for-me:
groups: ["demo"]
users: [] users: []
allowed_permissions: [read] allowed_permissions: [read]
uri: /v1.0/logs/manage-procurement:vendor-lifecycle-management:* uri: /process-instances/for-me/manage-revenue-streams:product-revenue-streams:customer-contracts-trade-terms/*
manage-procurement-invoice-instances-for-me:
groups: ["demo"]
users: []
allowed_permissions: [read]
uri: /process-instances/for-me/manage-procurement:procurement:core-contributor-invoice-management:*
manage-procurement-instances-for-me:
groups: ["demo"]
users: []
allowed_permissions: [read]
uri: /process-instances/for-me/manage-procurement:vendor-lifecycle-management:*
create-test-instances:
groups: ["test"]
users: []
allowed_permissions: [create, read]
uri: /process-instances/misc:test:*

View File

@ -2,60 +2,7 @@ default_group: everybody
groups: groups:
admin: admin:
users: users: [admin@spiffworkflow.org]
[
admin,
jakub,
kb,
alex,
dan,
mike,
jason,
j,
jarrad,
elizabeth,
jon,
]
Finance Team:
users:
[
jakub,
alex,
dan,
mike,
jason,
j,
amir,
jarrad,
elizabeth,
jon,
sasha,
fin,
fin1,
]
demo:
users:
[
core,
fin,
fin1,
harmeet,
sasha,
manuchehr,
lead,
lead1
]
core-contributor:
users:
[
core,
harmeet,
]
test:
users: [natalia]
permissions: permissions:
admin: admin:
@ -63,110 +10,3 @@ permissions:
users: [] users: []
allowed_permissions: [create, read, update, delete] allowed_permissions: [create, read, update, delete]
uri: /* uri: /*
tasks-crud:
groups: [everybody]
users: []
allowed_permissions: [create, read, update, delete]
uri: /v1.0/tasks/*
service-tasks:
groups: [everybody]
users: []
allowed_permissions: [read]
uri: /v1.0/service-tasks
user-groups-for-current-user:
groups: [everybody]
users: []
allowed_permissions: [read]
uri: /v1.0/user-groups/for-current-user
# read all for everybody
read-all-process-groups:
groups: [everybody]
users: []
allowed_permissions: [read]
uri: /v1.0/process-groups/*
read-all-process-models:
groups: [everybody]
users: []
allowed_permissions: [read]
uri: /v1.0/process-models/*
read-all-process-instances-for-me:
groups: [everybody]
users: []
allowed_permissions: [read]
uri: /v1.0/process-instances/for-me/*
read-process-instance-reports:
groups: [everybody]
users: []
allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-instances/reports/*
processes-read:
groups: [everybody]
users: []
allowed_permissions: [read]
uri: /v1.0/processes
manage-procurement-admin:
groups: ["Project Lead"]
users: []
allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-groups/manage-procurement:*
manage-procurement-admin-slash:
groups: ["Project Lead"]
users: []
allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-groups/manage-procurement/*
manage-procurement-admin-models:
groups: ["Project Lead"]
users: []
allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-models/manage-procurement:*
manage-procurement-admin-models-slash:
groups: ["Project Lead"]
users: []
allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-models/manage-procurement/*
manage-procurement-admin-instances:
groups: ["Project Lead"]
users: []
allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-instances/manage-procurement:*
manage-procurement-admin-instances-slash:
groups: ["Project Lead"]
users: []
allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-instances/manage-procurement/*
finance-admin:
groups: ["Finance Team"]
users: []
allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-groups/manage-procurement:procurement:*
manage-revenue-streams-instances:
groups: ["core-contributor", "demo"]
users: []
allowed_permissions: [create, read]
uri: /v1.0/process-instances/manage-revenue-streams:product-revenue-streams:customer-contracts-trade-terms/*
manage-procurement-invoice-instances:
groups: ["core-contributor", "demo"]
users: []
allowed_permissions: [create, read]
uri: /v1.0/process-instances/manage-procurement:procurement:core-contributor-invoice-management:*
manage-procurement-instances:
groups: ["core-contributor", "demo"]
users: []
allowed_permissions: [create, read]
uri: /v1.0/process-instances/manage-procurement:vendor-lifecycle-management:*
create-test-instances:
groups: ["test"]
users: []
allowed_permissions: [create, read]
uri: /v1.0/process-instances/misc:test:*

View File

@ -1,5 +1,12 @@
default_group: everybody default_group: everybody
users:
testadmin1:
service: https://testing/openid/thing
email: testadmin1@spiffworkflow.org
password: admin
preferred_username: El administrador de la muerte
groups: groups:
admin: admin:
users: [testadmin1, testadmin2] users: [testadmin1, testadmin2]
@ -14,7 +21,7 @@ permissions:
admin: admin:
groups: [admin] groups: [admin]
users: [] users: []
allowed_permissions: [create, read, update, delete, list, instantiate] allowed_permissions: [create, read, update, delete]
uri: /* uri: /*
read-all: read-all:
@ -27,29 +34,29 @@ permissions:
groups: [everybody] groups: [everybody]
users: [] users: []
allowed_permissions: [create, read, update, delete] allowed_permissions: [create, read, update, delete]
uri: /v1.0/tasks/* uri: /tasks/*
# TODO: all uris should really have the same structure # TODO: all uris should really have the same structure
finance-admin-group: finance-admin-group:
groups: ["Finance Team"] groups: ["Finance Team"]
users: [testuser4] users: [testuser4]
allowed_permissions: [create, read, update, delete] allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-groups/finance/* uri: /process-groups/finance/*
finance-admin-model: finance-admin-model:
groups: ["Finance Team"] groups: ["Finance Team"]
users: [testuser4] users: [testuser4]
allowed_permissions: [create, read, update, delete] allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-models/finance/* uri: /process-models/finance/*
finance-admin-model-lanes: finance-admin-model-lanes:
groups: ["Finance Team"] groups: ["Finance Team"]
users: [testuser4] users: [testuser4]
allowed_permissions: [create, read, update, delete] allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-models/finance:model_with_lanes/* uri: /process-models/finance:model_with_lanes/*
finance-admin-instance-run: finance-admin-instance-run:
groups: ["Finance Team"] groups: ["Finance Team"]
users: [testuser4] users: [testuser4]
allowed_permissions: [create, read, update, delete] allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-instances/* uri: /process-instances/*

View File

@ -0,0 +1,11 @@
"""Qa1."""
from os import environ
GIT_BRANCH_TO_PUBLISH_TO = environ.get("GIT_BRANCH_TO_PUBLISH_TO", default="qa2")
GIT_USERNAME = environ.get("GIT_USERNAME", default="sartography-automated-committer")
GIT_USER_EMAIL = environ.get(
"GIT_USER_EMAIL", default="sartography-automated-committer@users.noreply.github.com"
)
SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME = environ.get(
"SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME", default="qa1.yml"
)

View File

@ -1,7 +1,7 @@
"""Staging.""" """Staging."""
from os import environ from os import environ
GIT_BRANCH = environ.get("GIT_BRANCH_TO_PUBLISH_TO", default="staging") GIT_BRANCH = environ.get("GIT_BRANCH", default="staging")
GIT_BRANCH_TO_PUBLISH_TO = environ.get("GIT_BRANCH_TO_PUBLISH_TO", default="main") GIT_BRANCH_TO_PUBLISH_TO = environ.get("GIT_BRANCH_TO_PUBLISH_TO", default="main")
GIT_COMMIT_ON_SAVE = False GIT_COMMIT_ON_SAVE = False
SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME = "staging.yml" SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME = "staging.yml"

View File

@ -0,0 +1,2 @@
"""Api_version."""
V1_API_PATH_PREFIX = "/v1.0"

View File

@ -0,0 +1,24 @@
"""Interfaces."""
from typing import NewType
from typing import TYPE_CHECKING
from typing import TypedDict
if TYPE_CHECKING:
from spiffworkflow_backend.models.process_group import ProcessGroup
IdToProcessGroupMapping = NewType("IdToProcessGroupMapping", dict[str, "ProcessGroup"])
class ProcessGroupLite(TypedDict):
"""ProcessGroupLite."""
id: str
display_name: str
class ProcessGroupLitesWithCache(TypedDict):
"""ProcessGroupLitesWithCache."""
cache: dict[str, "ProcessGroup"]
process_groups: list[ProcessGroupLite]

View File

@ -27,6 +27,9 @@ class GroupModel(FlaskBpmnGroupModel):
identifier = db.Column(db.String(255)) identifier = db.Column(db.String(255))
user_group_assignments = relationship("UserGroupAssignmentModel", cascade="delete") user_group_assignments = relationship("UserGroupAssignmentModel", cascade="delete")
user_group_assignments_waiting = relationship( # type: ignore
"UserGroupAssignmentWaitingModel", cascade="delete"
)
users = relationship( # type: ignore users = relationship( # type: ignore
"UserModel", "UserModel",
viewonly=True, viewonly=True,

View File

@ -35,9 +35,9 @@ class HumanTaskModel(SpiffworkflowBaseDBModel):
ForeignKey(ProcessInstanceModel.id), nullable=False # type: ignore ForeignKey(ProcessInstanceModel.id), nullable=False # type: ignore
) )
lane_assignment_id: int | None = db.Column(ForeignKey(GroupModel.id)) lane_assignment_id: int | None = db.Column(ForeignKey(GroupModel.id))
completed_by_user_id: int = db.Column(ForeignKey(UserModel.id), nullable=True) completed_by_user_id: int = db.Column(ForeignKey(UserModel.id), nullable=True) # type: ignore
actual_owner_id: int = db.Column(ForeignKey(UserModel.id)) actual_owner_id: int = db.Column(ForeignKey(UserModel.id)) # type: ignore
# actual_owner: RelationshipProperty[UserModel] = relationship(UserModel) # actual_owner: RelationshipProperty[UserModel] = relationship(UserModel)
form_file_name: str | None = db.Column(db.String(50)) form_file_name: str | None = db.Column(db.String(50))

View File

@ -29,4 +29,4 @@ class HumanTaskUserModel(SpiffworkflowBaseDBModel):
human_task_id = db.Column( human_task_id = db.Column(
ForeignKey(HumanTaskModel.id), nullable=False, index=True # type: ignore ForeignKey(HumanTaskModel.id), nullable=False, index=True # type: ignore
) )
user_id = db.Column(ForeignKey(UserModel.id), nullable=False, index=True) user_id = db.Column(ForeignKey(UserModel.id), nullable=False, index=True) # type: ignore

View File

@ -86,5 +86,6 @@ def ensure_failure_cause_is_set_if_message_instance_failed(
if isinstance(instance, MessageInstanceModel): if isinstance(instance, MessageInstanceModel):
if instance.status == "failed" and instance.failure_cause is None: if instance.status == "failed" and instance.failure_cause is None:
raise ValueError( raise ValueError(
f"{instance.__class__.__name__}: failure_cause must be set if status is failed" f"{instance.__class__.__name__}: failure_cause must be set if"
" status is failed"
) )

View File

@ -32,14 +32,6 @@ class Permission(enum.Enum):
update = "update" update = "update"
delete = "delete" delete = "delete"
# maybe read to GET process_model/process-instances instead?
list = "list"
# maybe use create instead on
# POST http://localhost:7000/v1.0/process-models/category_number_one/call-activity/process-instances/*
# POST http://localhost:7000/v1.0/process-models/category_number_one/call-activity/process-instances/332/run
instantiate = "instantiate" # this is something you do to a process model
class PermissionAssignmentModel(SpiffworkflowBaseDBModel): class PermissionAssignmentModel(SpiffworkflowBaseDBModel):
"""PermissionAssignmentModel.""" """PermissionAssignmentModel."""

View File

@ -27,7 +27,7 @@ class PrincipalModel(SpiffworkflowBaseDBModel):
__table_args__ = (CheckConstraint("NOT(user_id IS NULL AND group_id IS NULL)"),) __table_args__ = (CheckConstraint("NOT(user_id IS NULL AND group_id IS NULL)"),)
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(ForeignKey(UserModel.id), nullable=True, unique=True) user_id = db.Column(ForeignKey(UserModel.id), nullable=True, unique=True) # type: ignore
group_id = db.Column(ForeignKey(GroupModel.id), nullable=True, unique=True) group_id = db.Column(ForeignKey(GroupModel.id), nullable=True, unique=True)
user = relationship("UserModel", viewonly=True) user = relationship("UserModel", viewonly=True)

View File

@ -11,6 +11,7 @@ import marshmallow
from marshmallow import post_load from marshmallow import post_load
from marshmallow import Schema from marshmallow import Schema
from spiffworkflow_backend.interfaces import ProcessGroupLite
from spiffworkflow_backend.models.process_model import ProcessModelInfo from spiffworkflow_backend.models.process_model import ProcessModelInfo
@ -29,7 +30,7 @@ class ProcessGroup:
default_factory=list[ProcessModelInfo] default_factory=list[ProcessModelInfo]
) )
process_groups: list[ProcessGroup] = field(default_factory=list["ProcessGroup"]) process_groups: list[ProcessGroup] = field(default_factory=list["ProcessGroup"])
parent_groups: list[dict] | None = None parent_groups: list[ProcessGroupLite] | None = None
def __post_init__(self) -> None: def __post_init__(self) -> None:
"""__post_init__.""" """__post_init__."""

View File

@ -57,13 +57,21 @@ class ProcessInstanceModel(SpiffworkflowBaseDBModel):
process_model_display_name: str = db.Column( process_model_display_name: str = db.Column(
db.String(255), nullable=False, index=True db.String(255), nullable=False, index=True
) )
process_initiator_id: int = db.Column(ForeignKey(UserModel.id), nullable=False) process_initiator_id: int = db.Column(ForeignKey(UserModel.id), nullable=False) # type: ignore
process_initiator = relationship("UserModel") process_initiator = relationship("UserModel")
active_human_tasks = relationship(
"HumanTaskModel",
primaryjoin=(
"and_(HumanTaskModel.process_instance_id==ProcessInstanceModel.id,"
" HumanTaskModel.completed == False)"
),
) # type: ignore
human_tasks = relationship( human_tasks = relationship(
"HumanTaskModel", "HumanTaskModel",
cascade="delete", cascade="delete",
primaryjoin="and_(HumanTaskModel.process_instance_id==ProcessInstanceModel.id, HumanTaskModel.completed == False)", overlaps="active_human_tasks",
) # type: ignore ) # type: ignore
message_instances = relationship("MessageInstanceModel", cascade="delete") # type: ignore message_instances = relationship("MessageInstanceModel", cascade="delete") # type: ignore
message_correlations = relationship("MessageCorrelationModel", cascade="delete") # type: ignore message_correlations = relationship("MessageCorrelationModel", cascade="delete") # type: ignore

View File

@ -70,7 +70,7 @@ class ProcessInstanceReportModel(SpiffworkflowBaseDBModel):
id: int = db.Column(db.Integer, primary_key=True) id: int = db.Column(db.Integer, primary_key=True)
identifier: str = db.Column(db.String(50), nullable=False, index=True) identifier: str = db.Column(db.String(50), nullable=False, index=True)
report_metadata: dict = deferred(db.Column(db.JSON)) # type: ignore report_metadata: dict = deferred(db.Column(db.JSON)) # type: ignore
created_by_id = db.Column(ForeignKey(UserModel.id), nullable=False, index=True) created_by_id = db.Column(ForeignKey(UserModel.id), nullable=False, index=True) # type: ignore
created_by = relationship("UserModel") created_by = relationship("UserModel")
created_at_in_seconds = db.Column(db.Integer) created_at_in_seconds = db.Column(db.Integer)
updated_at_in_seconds = db.Column(db.Integer) updated_at_in_seconds = db.Column(db.Integer)

View File

@ -11,6 +11,7 @@ import marshmallow
from marshmallow import Schema from marshmallow import Schema
from marshmallow.decorators import post_load from marshmallow.decorators import post_load
from spiffworkflow_backend.interfaces import ProcessGroupLite
from spiffworkflow_backend.models.file import File from spiffworkflow_backend.models.file import File
@ -37,7 +38,7 @@ class ProcessModelInfo:
files: list[File] | None = field(default_factory=list[File]) files: list[File] | None = field(default_factory=list[File])
fault_or_suspend_on_exception: str = NotificationType.fault.value fault_or_suspend_on_exception: str = NotificationType.fault.value
exception_notification_addresses: list[str] = field(default_factory=list) exception_notification_addresses: list[str] = field(default_factory=list)
parent_groups: list[dict] | None = None parent_groups: list[ProcessGroupLite] | None = None
metadata_extraction_paths: list[dict[str, str]] | None = None metadata_extraction_paths: list[dict[str, str]] | None = None
def __post_init__(self) -> None: def __post_init__(self) -> None:

View File

@ -17,7 +17,7 @@ class SecretModel(SpiffworkflowBaseDBModel):
id: int = db.Column(db.Integer, primary_key=True) id: int = db.Column(db.Integer, primary_key=True)
key: str = db.Column(db.String(50), unique=True, nullable=False) key: str = db.Column(db.String(50), unique=True, nullable=False)
value: str = db.Column(db.Text(), nullable=False) value: str = db.Column(db.Text(), nullable=False)
user_id: int = db.Column(ForeignKey(UserModel.id), nullable=False) user_id: int = db.Column(ForeignKey(UserModel.id), nullable=False) # type: ignore
updated_at_in_seconds: int = db.Column(db.Integer) updated_at_in_seconds: int = db.Column(db.Integer)
created_at_in_seconds: int = db.Column(db.Integer) created_at_in_seconds: int = db.Column(db.Integer)

View File

@ -43,8 +43,8 @@ class Task:
FIELD_TYPE_EMAIL = "email" # email: Email address FIELD_TYPE_EMAIL = "email" # email: Email address
FIELD_TYPE_URL = "url" # url: Website address FIELD_TYPE_URL = "url" # url: Website address
FIELD_PROP_AUTO_COMPLETE_MAX = ( FIELD_PROP_AUTO_COMPLETE_MAX = ( # Not used directly, passed in from the front end.
"autocomplete_num" # Not used directly, passed in from the front end. "autocomplete_num"
) )
# Required field # Required field
@ -77,8 +77,8 @@ class Task:
# File specific field properties # File specific field properties
FIELD_PROP_DOC_CODE = "doc_code" # to associate a file upload field with a doc code FIELD_PROP_DOC_CODE = "doc_code" # to associate a file upload field with a doc code
FIELD_PROP_FILE_DATA = ( FIELD_PROP_FILE_DATA = ( # to associate a bit of data with a specific file upload file.
"file_data" # to associate a bit of data with a specific file upload file. "file_data"
) )
# Additional properties # Additional properties
@ -118,6 +118,7 @@ class Task:
form_schema: Union[str, None] = None, form_schema: Union[str, None] = None,
form_ui_schema: Union[str, None] = None, form_ui_schema: Union[str, None] = None,
parent: Optional[str] = None, parent: Optional[str] = None,
event_definition: Union[dict[str, Any], None] = None,
call_activity_process_identifier: Optional[str] = None, call_activity_process_identifier: Optional[str] = None,
): ):
"""__init__.""" """__init__."""
@ -130,6 +131,7 @@ class Task:
self.documentation = documentation self.documentation = documentation
self.lane = lane self.lane = lane
self.parent = parent self.parent = parent
self.event_definition = event_definition
self.call_activity_process_identifier = call_activity_process_identifier self.call_activity_process_identifier = call_activity_process_identifier
self.data = data self.data = data
@ -189,6 +191,7 @@ class Task:
"form_schema": self.form_schema, "form_schema": self.form_schema,
"form_ui_schema": self.form_ui_schema, "form_ui_schema": self.form_ui_schema,
"parent": self.parent, "parent": self.parent,
"event_definition": self.event_definition,
"call_activity_process_identifier": self.call_activity_process_identifier, "call_activity_process_identifier": self.call_activity_process_identifier,
} }
@ -290,6 +293,7 @@ class TaskSchema(Schema):
"process_instance_id", "process_instance_id",
"form_schema", "form_schema",
"form_ui_schema", "form_ui_schema",
"event_definition",
] ]
multi_instance_type = EnumField(MultiInstanceType) multi_instance_type = EnumField(MultiInstanceType)

View File

@ -1,41 +1,38 @@
"""User.""" """User."""
from __future__ import annotations from __future__ import annotations
from typing import Any from dataclasses import dataclass
import jwt import jwt
import marshmallow import marshmallow
from flask import current_app from flask import current_app
from flask_bpmn.api.api_error import ApiError
from flask_bpmn.models.db import db from flask_bpmn.models.db import db
from flask_bpmn.models.db import SpiffworkflowBaseDBModel from flask_bpmn.models.db import SpiffworkflowBaseDBModel
from marshmallow import Schema from marshmallow import Schema
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from sqlalchemy.orm import validates
from spiffworkflow_backend.models.group import GroupModel from spiffworkflow_backend.models.group import GroupModel
from spiffworkflow_backend.services.authentication_service import (
AuthenticationProviderTypes,
)
class UserNotFoundError(Exception): class UserNotFoundError(Exception):
"""UserNotFoundError.""" """UserNotFoundError."""
@dataclass
class UserModel(SpiffworkflowBaseDBModel): class UserModel(SpiffworkflowBaseDBModel):
"""UserModel.""" """UserModel."""
__tablename__ = "user" __tablename__ = "user"
__table_args__ = (db.UniqueConstraint("service", "service_id", name="service_key"),) __table_args__ = (db.UniqueConstraint("service", "service_id", name="service_key"),)
id = db.Column(db.Integer, primary_key=True) id: int = db.Column(db.Integer, primary_key=True)
# server and service id must be unique, not username. username: str = db.Column(db.String(255), nullable=False, unique=True)
username = db.Column(db.String(255), nullable=False, unique=False)
uid = db.Column(db.String(50), unique=True) service = db.Column(
service = db.Column(db.String(50), nullable=False, unique=False) db.String(255), nullable=False, unique=False
) # not 'openid' -- google, aws
service_id = db.Column(db.String(255), nullable=False, unique=False) service_id = db.Column(db.String(255), nullable=False, unique=False)
name = db.Column(db.String(255)) display_name = db.Column(db.String(255))
email = db.Column(db.String(255)) email = db.Column(db.String(255))
updated_at_in_seconds: int = db.Column(db.Integer) updated_at_in_seconds: int = db.Column(db.Integer)
created_at_in_seconds: int = db.Column(db.Integer) created_at_in_seconds: int = db.Column(db.Integer)
@ -49,21 +46,6 @@ class UserModel(SpiffworkflowBaseDBModel):
) )
principal = relationship("PrincipalModel", uselist=False) # type: ignore principal = relationship("PrincipalModel", uselist=False) # type: ignore
@validates("service")
def validate_service(self, key: str, value: Any) -> str:
"""Validate_service."""
try:
ap_type = getattr(AuthenticationProviderTypes, value, None)
except Exception as e:
raise ValueError(f"invalid service type: {value}") from e
if ap_type is not None:
ap_value: str = ap_type.value
return ap_value
raise ApiError(
error_code="invalid_service",
message=f"Could not validate service with value: {value}",
)
def encode_auth_token(self) -> str: def encode_auth_token(self) -> str:
"""Generate the Auth Token. """Generate the Auth Token.

View File

@ -17,7 +17,7 @@ class UserGroupAssignmentModel(SpiffworkflowBaseDBModel):
) )
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(ForeignKey(UserModel.id), nullable=False) user_id = db.Column(ForeignKey(UserModel.id), nullable=False) # type: ignore
group_id = db.Column(ForeignKey(GroupModel.id), nullable=False) group_id = db.Column(ForeignKey(GroupModel.id), nullable=False)
group = relationship("GroupModel", overlaps="groups,user_group_assignments,users") # type: ignore group = relationship("GroupModel", overlaps="groups,user_group_assignments,users") # type: ignore

View File

@ -0,0 +1,34 @@
"""UserGroupAssignment."""
from flask_bpmn.models.db import db
from flask_bpmn.models.db import SpiffworkflowBaseDBModel
from sqlalchemy import ForeignKey
from sqlalchemy.orm import relationship
from spiffworkflow_backend.models.group import GroupModel
class UserGroupAssignmentWaitingModel(SpiffworkflowBaseDBModel):
"""When a user is assigned to a group, but that username does not exist.
We cache it here to be applied in the event the user does log in to the system.
"""
MATCH_ALL_USERS = "*"
__tablename__ = "user_group_assignment_waiting"
__table_args__ = (
db.UniqueConstraint(
"username", "group_id", name="user_group_assignment_staged_unique"
),
)
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(255), nullable=False)
group_id = db.Column(ForeignKey(GroupModel.id), nullable=False)
group = relationship("GroupModel", overlaps="groups,user_group_assignments_waiting,users") # type: ignore
def is_match_all(self) -> bool:
"""Is_match_all."""
if self.username == self.MATCH_ALL_USERS:
return True
return False

View File

@ -141,7 +141,7 @@ def process_model_save(process_model_id: str, file_name: str) -> Union[str, Resp
@admin_blueprint.route("/process-models/<process_model_id>/run", methods=["GET"]) @admin_blueprint.route("/process-models/<process_model_id>/run", methods=["GET"])
def process_model_run(process_model_id: str) -> Union[str, Response]: def process_model_run(process_model_id: str) -> Union[str, Response]:
"""Process_model_run.""" """Process_model_run."""
user = UserService.create_user("internal", "Mr. Test", username="Mr. Test") user = UserService.create_user("Mr. Test", "internal", "Mr. Test")
process_instance = ( process_instance = (
ProcessInstanceService.create_process_instance_from_process_model_identifier( ProcessInstanceService.create_process_instance_from_process_model_identifier(
process_model_id, user process_model_id, user

View File

@ -0,0 +1,13 @@
"""APIs for dealing with process groups, process models, and process instances."""
import json
import flask.wrappers
from flask.wrappers import Response
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
def status() -> flask.wrappers.Response:
"""Status."""
ProcessInstanceModel.query.filter().first()
return Response(json.dumps({"ok": True}), status=200, mimetype="application/json")

View File

@ -0,0 +1,176 @@
"""APIs for dealing with process groups, process models, and process instances."""
import json
from typing import Any
from typing import Dict
from typing import Optional
import flask.wrappers
from flask import g
from flask import jsonify
from flask import make_response
from flask.wrappers import Response
from flask_bpmn.api.api_error import ApiError
from spiffworkflow_backend.models.message_correlation import MessageCorrelationModel
from spiffworkflow_backend.models.message_instance import MessageInstanceModel
from spiffworkflow_backend.models.message_model import MessageModel
from spiffworkflow_backend.models.message_triggerable_process_model import (
MessageTriggerableProcessModel,
)
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
from spiffworkflow_backend.models.process_instance import ProcessInstanceModelSchema
from spiffworkflow_backend.routes.process_api_blueprint import (
_find_process_instance_by_id_or_raise,
)
from spiffworkflow_backend.services.message_service import MessageService
def message_instance_list(
process_instance_id: Optional[int] = None,
page: int = 1,
per_page: int = 100,
) -> flask.wrappers.Response:
"""Message_instance_list."""
# to make sure the process instance exists
message_instances_query = MessageInstanceModel.query
if process_instance_id:
message_instances_query = message_instances_query.filter_by(
process_instance_id=process_instance_id
)
message_instances = (
message_instances_query.order_by(
MessageInstanceModel.created_at_in_seconds.desc(), # type: ignore
MessageInstanceModel.id.desc(), # type: ignore
)
.join(MessageModel, MessageModel.id == MessageInstanceModel.message_model_id)
.join(ProcessInstanceModel)
.add_columns(
MessageModel.identifier.label("message_identifier"),
ProcessInstanceModel.process_model_identifier,
ProcessInstanceModel.process_model_display_name,
)
.paginate(page=page, per_page=per_page, error_out=False)
)
for message_instance in message_instances:
message_correlations: dict = {}
for (
mcmi
) in (
message_instance.MessageInstanceModel.message_correlations_message_instances
):
mc = MessageCorrelationModel.query.filter_by(
id=mcmi.message_correlation_id
).all()
for m in mc:
if m.name not in message_correlations:
message_correlations[m.name] = {}
message_correlations[m.name][
m.message_correlation_property.identifier
] = m.value
message_instance.MessageInstanceModel.message_correlations = (
message_correlations
)
response_json = {
"results": message_instances.items,
"pagination": {
"count": len(message_instances.items),
"total": message_instances.total,
"pages": message_instances.pages,
},
}
return make_response(jsonify(response_json), 200)
# body: {
# payload: dict,
# process_instance_id: Optional[int],
# }
def message_start(
message_identifier: str,
body: Dict[str, Any],
) -> flask.wrappers.Response:
"""Message_start."""
message_model = MessageModel.query.filter_by(identifier=message_identifier).first()
if message_model is None:
raise (
ApiError(
error_code="unknown_message",
message=f"Could not find message with identifier: {message_identifier}",
status_code=404,
)
)
if "payload" not in body:
raise (
ApiError(
error_code="missing_payload",
message="Body is missing payload.",
status_code=400,
)
)
process_instance = None
if "process_instance_id" in body:
# to make sure we have a valid process_instance_id
process_instance = _find_process_instance_by_id_or_raise(
body["process_instance_id"]
)
message_instance = MessageInstanceModel.query.filter_by(
process_instance_id=process_instance.id,
message_model_id=message_model.id,
message_type="receive",
status="ready",
).first()
if message_instance is None:
raise (
ApiError(
error_code="cannot_find_waiting_message",
message=(
"Could not find waiting message for identifier"
f" {message_identifier} and process instance"
f" {process_instance.id}"
),
status_code=400,
)
)
MessageService.process_message_receive(
message_instance, message_model.name, body["payload"]
)
else:
message_triggerable_process_model = (
MessageTriggerableProcessModel.query.filter_by(
message_model_id=message_model.id
).first()
)
if message_triggerable_process_model is None:
raise (
ApiError(
error_code="cannot_start_message",
message=(
"Message with identifier cannot be start with message:"
f" {message_identifier}"
),
status_code=400,
)
)
process_instance = MessageService.process_message_triggerable_process_model(
message_triggerable_process_model,
message_model.name,
body["payload"],
g.user,
)
return Response(
json.dumps(ProcessInstanceModelSchema().dump(process_instance)),
status=200,
mimetype="application/json",
)

View File

@ -111,6 +111,7 @@ def token() -> dict:
"iat": time.time(), "iat": time.time(),
"exp": time.time() + 86400, # Expire after a day. "exp": time.time() + 86400, # Expire after a day.
"sub": user_name, "sub": user_name,
"email": user_details["email"],
"preferred_username": user_details.get("preferred_username", user_name), "preferred_username": user_details.get("preferred_username", user_name),
}, },
client_secret, client_secret,

View File

@ -0,0 +1,130 @@
"""APIs for dealing with process groups, process models, and process instances."""
import json
from typing import Any
from typing import Optional
import flask.wrappers
from flask import g
from flask import jsonify
from flask import make_response
from flask.wrappers import Response
from flask_bpmn.api.api_error import ApiError
from spiffworkflow_backend.exceptions.process_entity_not_found_error import (
ProcessEntityNotFoundError,
)
from spiffworkflow_backend.models.process_group import ProcessGroup
from spiffworkflow_backend.models.process_group import ProcessGroupSchema
from spiffworkflow_backend.routes.process_api_blueprint import _commit_and_push_to_git
from spiffworkflow_backend.routes.process_api_blueprint import (
_un_modify_modified_process_model_id,
)
from spiffworkflow_backend.services.process_model_service import ProcessModelService
def process_group_create(body: dict) -> flask.wrappers.Response:
"""Add_process_group."""
process_group = ProcessGroup(**body)
ProcessModelService.add_process_group(process_group)
_commit_and_push_to_git(
f"User: {g.user.username} added process group {process_group.id}"
)
return make_response(jsonify(process_group), 201)
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)
_commit_and_push_to_git(
f"User: {g.user.username} deleted process group {process_group_id}"
)
return Response(json.dumps({"ok": True}), status=200, mimetype="application/json")
def process_group_update(
modified_process_group_id: str, body: dict
) -> flask.wrappers.Response:
"""Process Group Update."""
body_include_list = ["display_name", "description"]
body_filtered = {
include_item: body[include_item]
for include_item in body_include_list
if include_item in body
}
process_group_id = _un_modify_modified_process_model_id(modified_process_group_id)
process_group = ProcessGroup(id=process_group_id, **body_filtered)
ProcessModelService.update_process_group(process_group)
_commit_and_push_to_git(
f"User: {g.user.username} updated process group {process_group_id}"
)
return make_response(jsonify(process_group), 200)
def process_group_list(
process_group_identifier: Optional[str] = None, page: int = 1, per_page: int = 100
) -> flask.wrappers.Response:
"""Process_group_list."""
if process_group_identifier is not None:
process_groups = ProcessModelService.get_process_groups(
process_group_identifier
)
else:
process_groups = ProcessModelService.get_process_groups()
batch = ProcessModelService().get_batch(
items=process_groups, page=page, per_page=per_page
)
pages = len(process_groups) // per_page
remainder = len(process_groups) % per_page
if remainder > 0:
pages += 1
response_json = {
"results": ProcessGroupSchema(many=True).dump(batch),
"pagination": {
"count": len(batch),
"total": len(process_groups),
"pages": pages,
},
}
return Response(json.dumps(response_json), status=200, mimetype="application/json")
def process_group_show(
modified_process_group_id: str,
) -> Any:
"""Process_group_show."""
process_group_id = _un_modify_modified_process_model_id(modified_process_group_id)
try:
process_group = ProcessModelService.get_process_group(process_group_id)
except ProcessEntityNotFoundError as exception:
raise (
ApiError(
error_code="process_group_cannot_be_found",
message=f"Process group cannot be found: {process_group_id}",
status_code=400,
)
) from exception
process_group.parent_groups = ProcessModelService.get_parent_group_array(
process_group.id
)
return make_response(jsonify(process_group), 200)
def process_group_move(
modified_process_group_identifier: str, new_location: str
) -> flask.wrappers.Response:
"""Process_group_move."""
original_process_group_id = _un_modify_modified_process_model_id(
modified_process_group_identifier
)
new_process_group = ProcessModelService().process_group_move(
original_process_group_id, new_location
)
_commit_and_push_to_git(
f"User: {g.user.username} moved process group {original_process_group_id} to"
f" {new_process_group.id}"
)
return make_response(jsonify(new_process_group), 200)

View File

@ -0,0 +1,693 @@
"""APIs for dealing with process groups, process models, and process instances."""
import json
from typing import Any
from typing import Dict
from typing import Optional
import flask.wrappers
from flask import current_app
from flask import g
from flask import jsonify
from flask import make_response
from flask import request
from flask.wrappers import Response
from flask_bpmn.api.api_error import ApiError
from flask_bpmn.models.db import db
from SpiffWorkflow.task import TaskState # type: ignore
from sqlalchemy import and_
from sqlalchemy import or_
from spiffworkflow_backend.models.human_task import HumanTaskModel
from spiffworkflow_backend.models.human_task_user import HumanTaskUserModel
from spiffworkflow_backend.models.process_instance import ProcessInstanceApiSchema
from spiffworkflow_backend.models.process_instance import (
ProcessInstanceCannotBeDeletedError,
)
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
from spiffworkflow_backend.models.process_instance import ProcessInstanceModelSchema
from spiffworkflow_backend.models.process_instance_metadata import (
ProcessInstanceMetadataModel,
)
from spiffworkflow_backend.models.process_instance_report import (
ProcessInstanceReportModel,
)
from spiffworkflow_backend.models.spec_reference import SpecReferenceCache
from spiffworkflow_backend.models.spec_reference import SpecReferenceNotFoundError
from spiffworkflow_backend.models.spiff_logging import SpiffLoggingModel
from spiffworkflow_backend.models.spiff_step_details import SpiffStepDetailsModel
from spiffworkflow_backend.models.user import UserModel
from spiffworkflow_backend.routes.process_api_blueprint import (
_find_process_instance_by_id_or_raise,
)
from spiffworkflow_backend.routes.process_api_blueprint import _get_process_model
from spiffworkflow_backend.routes.process_api_blueprint import (
_un_modify_modified_process_model_id,
)
from spiffworkflow_backend.services.error_handling_service import ErrorHandlingService
from spiffworkflow_backend.services.git_service import GitCommandError
from spiffworkflow_backend.services.git_service import GitService
from spiffworkflow_backend.services.message_service import MessageService
from spiffworkflow_backend.services.process_instance_processor import (
ProcessInstanceProcessor,
)
from spiffworkflow_backend.services.process_instance_report_service import (
ProcessInstanceReportFilter,
)
from spiffworkflow_backend.services.process_instance_report_service import (
ProcessInstanceReportService,
)
from spiffworkflow_backend.services.process_instance_service import (
ProcessInstanceService,
)
from spiffworkflow_backend.services.process_model_service import ProcessModelService
from spiffworkflow_backend.services.spec_file_service import SpecFileService
def process_instance_create(
modified_process_model_identifier: str,
) -> flask.wrappers.Response:
"""Create_process_instance."""
process_model_identifier = _un_modify_modified_process_model_id(
modified_process_model_identifier
)
process_instance = (
ProcessInstanceService.create_process_instance_from_process_model_identifier(
process_model_identifier, g.user
)
)
return Response(
json.dumps(ProcessInstanceModelSchema().dump(process_instance)),
status=201,
mimetype="application/json",
)
def process_instance_run(
modified_process_model_identifier: str,
process_instance_id: int,
do_engine_steps: bool = True,
) -> flask.wrappers.Response:
"""Process_instance_run."""
process_instance = ProcessInstanceService().get_process_instance(
process_instance_id
)
if process_instance.status != "not_started":
raise ApiError(
error_code="process_instance_not_runnable",
message=(
f"Process Instance ({process_instance.id}) is currently running or has"
" already run."
),
status_code=400,
)
processor = ProcessInstanceProcessor(process_instance)
if do_engine_steps:
try:
processor.do_engine_steps(save=True)
except ApiError as e:
ErrorHandlingService().handle_error(processor, e)
raise e
except Exception as e:
ErrorHandlingService().handle_error(processor, e)
task = processor.bpmn_process_instance.last_task
raise ApiError.from_task(
error_code="unknown_exception",
message=f"An unknown error occurred. Original error: {e}",
status_code=400,
task=task,
) from e
if not current_app.config["RUN_BACKGROUND_SCHEDULER"]:
MessageService.process_message_instances()
process_instance_api = ProcessInstanceService.processor_to_process_instance_api(
processor
)
process_instance_data = processor.get_data()
process_instance_metadata = ProcessInstanceApiSchema().dump(process_instance_api)
process_instance_metadata["data"] = process_instance_data
return Response(
json.dumps(process_instance_metadata), status=200, mimetype="application/json"
)
def process_instance_terminate(
process_instance_id: int,
modified_process_model_identifier: str,
) -> flask.wrappers.Response:
"""Process_instance_run."""
process_instance = ProcessInstanceService().get_process_instance(
process_instance_id
)
processor = ProcessInstanceProcessor(process_instance)
processor.terminate()
return Response(json.dumps({"ok": True}), status=200, mimetype="application/json")
def process_instance_suspend(
process_instance_id: int,
modified_process_model_identifier: str,
) -> flask.wrappers.Response:
"""Process_instance_suspend."""
process_instance = ProcessInstanceService().get_process_instance(
process_instance_id
)
processor = ProcessInstanceProcessor(process_instance)
processor.suspend()
return Response(json.dumps({"ok": True}), status=200, mimetype="application/json")
def process_instance_resume(
process_instance_id: int,
modified_process_model_identifier: str,
) -> flask.wrappers.Response:
"""Process_instance_resume."""
process_instance = ProcessInstanceService().get_process_instance(
process_instance_id
)
processor = ProcessInstanceProcessor(process_instance)
processor.resume()
return Response(json.dumps({"ok": True}), status=200, mimetype="application/json")
def process_instance_log_list(
modified_process_model_identifier: str,
process_instance_id: int,
page: int = 1,
per_page: int = 100,
detailed: bool = False,
) -> flask.wrappers.Response:
"""Process_instance_log_list."""
# to make sure the process instance exists
process_instance = _find_process_instance_by_id_or_raise(process_instance_id)
log_query = SpiffLoggingModel.query.filter(
SpiffLoggingModel.process_instance_id == process_instance.id
)
if not detailed:
log_query = log_query.filter(SpiffLoggingModel.message.in_(["State change to COMPLETED"])) # type: ignore
logs = (
log_query.order_by(SpiffLoggingModel.timestamp.desc()) # type: ignore
.join(
UserModel, UserModel.id == SpiffLoggingModel.current_user_id, isouter=True
) # isouter since if we don't have a user, we still want the log
.add_columns(
UserModel.username,
)
.paginate(page=page, per_page=per_page, error_out=False)
)
response_json = {
"results": logs.items,
"pagination": {
"count": len(logs.items),
"total": logs.total,
"pages": logs.pages,
},
}
return make_response(jsonify(response_json), 200)
def process_instance_list_for_me(
process_model_identifier: Optional[str] = None,
page: int = 1,
per_page: int = 100,
start_from: Optional[int] = None,
start_to: Optional[int] = None,
end_from: Optional[int] = None,
end_to: Optional[int] = None,
process_status: Optional[str] = None,
user_filter: Optional[bool] = False,
report_identifier: Optional[str] = None,
report_id: Optional[int] = None,
user_group_identifier: Optional[str] = None,
) -> flask.wrappers.Response:
"""Process_instance_list_for_me."""
return process_instance_list(
process_model_identifier=process_model_identifier,
page=page,
per_page=per_page,
start_from=start_from,
start_to=start_to,
end_from=end_from,
end_to=end_to,
process_status=process_status,
user_filter=user_filter,
report_identifier=report_identifier,
report_id=report_id,
user_group_identifier=user_group_identifier,
with_relation_to_me=True,
)
def process_instance_list(
process_model_identifier: Optional[str] = None,
page: int = 1,
per_page: int = 100,
start_from: Optional[int] = None,
start_to: Optional[int] = None,
end_from: Optional[int] = None,
end_to: Optional[int] = None,
process_status: Optional[str] = None,
with_relation_to_me: Optional[bool] = None,
user_filter: Optional[bool] = False,
report_identifier: Optional[str] = None,
report_id: Optional[int] = None,
user_group_identifier: Optional[str] = None,
) -> flask.wrappers.Response:
"""Process_instance_list."""
process_instance_report = ProcessInstanceReportService.report_with_identifier(
g.user, report_id, report_identifier
)
if user_filter:
report_filter = ProcessInstanceReportFilter(
process_model_identifier=process_model_identifier,
user_group_identifier=user_group_identifier,
start_from=start_from,
start_to=start_to,
end_from=end_from,
end_to=end_to,
with_relation_to_me=with_relation_to_me,
process_status=process_status.split(",") if process_status else None,
)
else:
report_filter = (
ProcessInstanceReportService.filter_from_metadata_with_overrides(
process_instance_report=process_instance_report,
process_model_identifier=process_model_identifier,
user_group_identifier=user_group_identifier,
start_from=start_from,
start_to=start_to,
end_from=end_from,
end_to=end_to,
process_status=process_status,
with_relation_to_me=with_relation_to_me,
)
)
response_json = ProcessInstanceReportService.run_process_instance_report(
report_filter=report_filter,
process_instance_report=process_instance_report,
page=page,
per_page=per_page,
user=g.user,
)
return make_response(jsonify(response_json), 200)
def process_instance_report_column_list() -> flask.wrappers.Response:
"""Process_instance_report_column_list."""
table_columns = ProcessInstanceReportService.builtin_column_options()
columns_for_metadata = (
db.session.query(ProcessInstanceMetadataModel.key)
.order_by(ProcessInstanceMetadataModel.key)
.distinct() # type: ignore
.all()
)
columns_for_metadata_strings = [
{"Header": i[0], "accessor": i[0], "filterable": True}
for i in columns_for_metadata
]
return make_response(jsonify(table_columns + columns_for_metadata_strings), 200)
def process_instance_show_for_me(
modified_process_model_identifier: str,
process_instance_id: int,
process_identifier: Optional[str] = None,
) -> flask.wrappers.Response:
"""Process_instance_show_for_me."""
process_instance = _find_process_instance_for_me_or_raise(process_instance_id)
return _get_process_instance(
process_instance=process_instance,
modified_process_model_identifier=modified_process_model_identifier,
process_identifier=process_identifier,
)
def process_instance_show(
modified_process_model_identifier: str,
process_instance_id: int,
process_identifier: Optional[str] = None,
) -> flask.wrappers.Response:
"""Create_process_instance."""
process_instance = _find_process_instance_by_id_or_raise(process_instance_id)
return _get_process_instance(
process_instance=process_instance,
modified_process_model_identifier=modified_process_model_identifier,
process_identifier=process_identifier,
)
def process_instance_delete(
process_instance_id: int, modified_process_model_identifier: str
) -> flask.wrappers.Response:
"""Create_process_instance."""
process_instance = _find_process_instance_by_id_or_raise(process_instance_id)
if not process_instance.has_terminal_status():
raise ProcessInstanceCannotBeDeletedError(
f"Process instance ({process_instance.id}) cannot be deleted since it does"
f" not have a terminal status. Current status is {process_instance.status}."
)
# (Pdb) db.session.delete
# <bound method delete of <sqlalchemy.orm.scoping.scoped_session object at 0x103eaab30>>
db.session.query(SpiffLoggingModel).filter_by(
process_instance_id=process_instance.id
).delete()
db.session.query(SpiffStepDetailsModel).filter_by(
process_instance_id=process_instance.id
).delete()
db.session.delete(process_instance)
db.session.commit()
return Response(json.dumps({"ok": True}), status=200, mimetype="application/json")
def process_instance_report_list(
page: int = 1, per_page: int = 100
) -> flask.wrappers.Response:
"""Process_instance_report_list."""
process_instance_reports = ProcessInstanceReportModel.query.filter_by(
created_by_id=g.user.id,
).all()
return make_response(jsonify(process_instance_reports), 200)
def process_instance_report_create(body: Dict[str, Any]) -> flask.wrappers.Response:
"""Process_instance_report_create."""
process_instance_report = ProcessInstanceReportModel.create_report(
identifier=body["identifier"],
user=g.user,
report_metadata=body["report_metadata"],
)
return make_response(jsonify(process_instance_report), 201)
def process_instance_report_update(
report_id: int,
body: Dict[str, Any],
) -> flask.wrappers.Response:
"""Process_instance_report_update."""
process_instance_report = ProcessInstanceReportModel.query.filter_by(
id=report_id,
created_by_id=g.user.id,
).first()
if process_instance_report is None:
raise ApiError(
error_code="unknown_process_instance_report",
message="Unknown process instance report",
status_code=404,
)
process_instance_report.report_metadata = body["report_metadata"]
db.session.commit()
return make_response(jsonify(process_instance_report), 201)
def process_instance_report_delete(
report_id: int,
) -> flask.wrappers.Response:
"""Process_instance_report_delete."""
process_instance_report = ProcessInstanceReportModel.query.filter_by(
id=report_id,
created_by_id=g.user.id,
).first()
if process_instance_report is None:
raise ApiError(
error_code="unknown_process_instance_report",
message="Unknown process instance report",
status_code=404,
)
db.session.delete(process_instance_report)
db.session.commit()
return Response(json.dumps({"ok": True}), status=200, mimetype="application/json")
def process_instance_report_show(
report_id: int,
page: int = 1,
per_page: int = 100,
) -> flask.wrappers.Response:
"""Process_instance_report_show."""
process_instances = ProcessInstanceModel.query.order_by(
ProcessInstanceModel.start_in_seconds.desc(), ProcessInstanceModel.id.desc() # type: ignore
).paginate(page=page, per_page=per_page, error_out=False)
process_instance_report = ProcessInstanceReportModel.query.filter_by(
id=report_id,
created_by_id=g.user.id,
).first()
if process_instance_report is None:
raise ApiError(
error_code="unknown_process_instance_report",
message="Unknown process instance report",
status_code=404,
)
substitution_variables = request.args.to_dict()
result_dict = process_instance_report.generate_report(
process_instances.items, substitution_variables
)
# update this if we go back to a database query instead of filtering in memory
result_dict["pagination"] = {
"count": len(result_dict["results"]),
"total": len(result_dict["results"]),
"pages": 1,
}
return Response(json.dumps(result_dict), status=200, mimetype="application/json")
def process_instance_task_list_without_task_data_for_me(
modified_process_model_identifier: str,
process_instance_id: int,
all_tasks: bool = False,
spiff_step: int = 0,
) -> flask.wrappers.Response:
"""Process_instance_task_list_without_task_data_for_me."""
process_instance = _find_process_instance_for_me_or_raise(process_instance_id)
return process_instance_task_list(
modified_process_model_identifier,
process_instance,
all_tasks,
spiff_step,
get_task_data=False,
)
def process_instance_task_list_without_task_data(
modified_process_model_identifier: str,
process_instance_id: int,
all_tasks: bool = False,
spiff_step: int = 0,
) -> flask.wrappers.Response:
"""Process_instance_task_list_without_task_data."""
process_instance = _find_process_instance_by_id_or_raise(process_instance_id)
return process_instance_task_list(
modified_process_model_identifier,
process_instance,
all_tasks,
spiff_step,
get_task_data=False,
)
def process_instance_task_list_with_task_data(
modified_process_model_identifier: str,
process_instance_id: int,
all_tasks: bool = False,
spiff_step: int = 0,
) -> flask.wrappers.Response:
"""Process_instance_task_list_with_task_data."""
process_instance = _find_process_instance_by_id_or_raise(process_instance_id)
return process_instance_task_list(
modified_process_model_identifier,
process_instance,
all_tasks,
spiff_step,
get_task_data=True,
)
def process_instance_task_list(
_modified_process_model_identifier: str,
process_instance: ProcessInstanceModel,
all_tasks: bool = False,
spiff_step: int = 0,
get_task_data: bool = False,
) -> flask.wrappers.Response:
"""Process_instance_task_list."""
if spiff_step > 0:
step_detail = (
db.session.query(SpiffStepDetailsModel)
.filter(
SpiffStepDetailsModel.process_instance_id == process_instance.id,
SpiffStepDetailsModel.spiff_step == spiff_step,
)
.first()
)
if step_detail is not None and process_instance.bpmn_json is not None:
bpmn_json = json.loads(process_instance.bpmn_json)
bpmn_json["tasks"] = step_detail.task_json["tasks"]
bpmn_json["subprocesses"] = step_detail.task_json["subprocesses"]
process_instance.bpmn_json = json.dumps(bpmn_json)
processor = ProcessInstanceProcessor(process_instance)
spiff_tasks = None
if all_tasks:
spiff_tasks = processor.bpmn_process_instance.get_tasks(TaskState.ANY_MASK)
else:
spiff_tasks = processor.get_all_user_tasks()
tasks = []
for spiff_task in spiff_tasks:
task = ProcessInstanceService.spiff_task_to_api_task(processor, spiff_task)
if get_task_data:
task.data = spiff_task.data
tasks.append(task)
return make_response(jsonify(tasks), 200)
def process_instance_reset(
process_instance_id: int,
modified_process_model_identifier: str,
spiff_step: int = 0,
) -> flask.wrappers.Response:
"""Process_instance_reset."""
process_instance = ProcessInstanceService().get_process_instance(
process_instance_id
)
step_detail = (
db.session.query(SpiffStepDetailsModel)
.filter(
SpiffStepDetailsModel.process_instance_id == process_instance.id,
SpiffStepDetailsModel.spiff_step == spiff_step,
)
.first()
)
if step_detail is not None and process_instance.bpmn_json is not None:
bpmn_json = json.loads(process_instance.bpmn_json)
bpmn_json["tasks"] = step_detail.task_json["tasks"]
bpmn_json["subprocesses"] = step_detail.task_json["subprocesses"]
process_instance.bpmn_json = json.dumps(bpmn_json)
db.session.add(process_instance)
try:
db.session.commit()
except Exception as e:
db.session.rollback()
raise ApiError(
error_code="reset_process_instance_error",
message=f"Could not update the Instance. Original error is {e}",
) from e
return Response(
json.dumps(ProcessInstanceModelSchema().dump(process_instance)),
status=200,
mimetype="application/json",
)
def _get_process_instance(
modified_process_model_identifier: str,
process_instance: ProcessInstanceModel,
process_identifier: Optional[str] = None,
) -> flask.wrappers.Response:
"""_get_process_instance."""
process_model_identifier = modified_process_model_identifier.replace(":", "/")
try:
current_version_control_revision = GitService.get_current_revision()
except GitCommandError:
current_version_control_revision = ""
process_model_with_diagram = None
name_of_file_with_diagram = None
if process_identifier:
spec_reference = SpecReferenceCache.query.filter_by(
identifier=process_identifier, type="process"
).first()
if spec_reference is None:
raise SpecReferenceNotFoundError(
"Could not find given process identifier in the cache:"
f" {process_identifier}"
)
process_model_with_diagram = ProcessModelService.get_process_model(
spec_reference.process_model_id
)
name_of_file_with_diagram = spec_reference.file_name
else:
process_model_with_diagram = _get_process_model(process_model_identifier)
if process_model_with_diagram.primary_file_name:
name_of_file_with_diagram = process_model_with_diagram.primary_file_name
if process_model_with_diagram and name_of_file_with_diagram:
if (
process_instance.bpmn_version_control_identifier
== current_version_control_revision
):
bpmn_xml_file_contents = SpecFileService.get_data(
process_model_with_diagram, name_of_file_with_diagram
).decode("utf-8")
else:
bpmn_xml_file_contents = GitService.get_instance_file_contents_for_revision(
process_model_with_diagram,
process_instance.bpmn_version_control_identifier,
file_name=name_of_file_with_diagram,
)
process_instance.bpmn_xml_file_contents = bpmn_xml_file_contents
return make_response(jsonify(process_instance), 200)
def _find_process_instance_for_me_or_raise(
process_instance_id: int,
) -> ProcessInstanceModel:
"""_find_process_instance_for_me_or_raise."""
process_instance: ProcessInstanceModel = (
ProcessInstanceModel.query.filter_by(id=process_instance_id)
.outerjoin(HumanTaskModel)
.outerjoin(
HumanTaskUserModel,
and_(
HumanTaskModel.id == HumanTaskUserModel.human_task_id,
HumanTaskUserModel.user_id == g.user.id,
),
)
.filter(
or_(
HumanTaskUserModel.id.is_not(None),
ProcessInstanceModel.process_initiator_id == g.user.id,
)
)
.first()
)
if process_instance is None:
raise (
ApiError(
error_code="process_instance_cannot_be_found",
message=(
f"Process instance with id {process_instance_id} cannot be found"
" that is associated with you."
),
status_code=400,
)
)
return process_instance

View File

@ -0,0 +1,496 @@
"""APIs for dealing with process groups, process models, and process instances."""
import json
import os
import re
from typing import Any
from typing import Dict
from typing import Optional
from typing import Union
import connexion # type: ignore
import flask.wrappers
from flask import current_app
from flask import g
from flask import jsonify
from flask import make_response
from flask.wrappers import Response
from flask_bpmn.api.api_error import ApiError
from spiffworkflow_backend.interfaces import IdToProcessGroupMapping
from spiffworkflow_backend.models.file import FileSchema
from spiffworkflow_backend.models.process_group import ProcessGroup
from spiffworkflow_backend.models.process_instance_report import (
ProcessInstanceReportModel,
)
from spiffworkflow_backend.models.process_model import ProcessModelInfo
from spiffworkflow_backend.models.process_model import ProcessModelInfoSchema
from spiffworkflow_backend.routes.process_api_blueprint import _commit_and_push_to_git
from spiffworkflow_backend.routes.process_api_blueprint import _get_process_model
from spiffworkflow_backend.routes.process_api_blueprint import (
_un_modify_modified_process_model_id,
)
from spiffworkflow_backend.services.git_service import GitService
from spiffworkflow_backend.services.git_service import MissingGitConfigsError
from spiffworkflow_backend.services.process_instance_report_service import (
ProcessInstanceReportService,
)
from spiffworkflow_backend.services.process_model_service import ProcessModelService
from spiffworkflow_backend.services.spec_file_service import SpecFileService
def process_model_create(
modified_process_group_id: str, body: Dict[str, Union[str, bool, int]]
) -> flask.wrappers.Response:
"""Process_model_create."""
body_include_list = [
"id",
"display_name",
"primary_file_name",
"primary_process_id",
"description",
"metadata_extraction_paths",
]
body_filtered = {
include_item: body[include_item]
for include_item in body_include_list
if include_item in body
}
_get_process_group_from_modified_identifier(modified_process_group_id)
process_model_info = ProcessModelInfo(**body_filtered) # type: ignore
if process_model_info is None:
raise ApiError(
error_code="process_model_could_not_be_created",
message=f"Process Model could not be created from given body: {body}",
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}"
)
return Response(
json.dumps(ProcessModelInfoSchema().dump(process_model_info)),
status=201,
mimetype="application/json",
)
def process_model_delete(
modified_process_model_identifier: str,
) -> flask.wrappers.Response:
"""Process_model_delete."""
process_model_identifier = modified_process_model_identifier.replace(":", "/")
ProcessModelService().process_model_delete(process_model_identifier)
_commit_and_push_to_git(
f"User: {g.user.username} deleted process model {process_model_identifier}"
)
return Response(json.dumps({"ok": True}), status=200, mimetype="application/json")
def process_model_update(
modified_process_model_identifier: str, body: Dict[str, Union[str, bool, int]]
) -> Any:
"""Process_model_update."""
process_model_identifier = modified_process_model_identifier.replace(":", "/")
body_include_list = [
"display_name",
"primary_file_name",
"primary_process_id",
"description",
"metadata_extraction_paths",
]
body_filtered = {
include_item: body[include_item]
for include_item in body_include_list
if include_item in body
}
process_model = _get_process_model(process_model_identifier)
ProcessModelService.update_process_model(process_model, body_filtered)
_commit_and_push_to_git(
f"User: {g.user.username} updated process model {process_model_identifier}"
)
return ProcessModelInfoSchema().dump(process_model)
def process_model_show(modified_process_model_identifier: str) -> Any:
"""Process_model_show."""
process_model_identifier = modified_process_model_identifier.replace(":", "/")
process_model = _get_process_model(process_model_identifier)
files = sorted(
SpecFileService.get_files(process_model),
key=lambda f: "" if f.name == process_model.primary_file_name else f.sort_index,
)
process_model.files = files
for file in process_model.files:
file.references = SpecFileService.get_references_for_file(file, process_model)
process_model.parent_groups = ProcessModelService.get_parent_group_array(
process_model.id
)
return make_response(jsonify(process_model), 200)
def process_model_move(
modified_process_model_identifier: str, new_location: str
) -> flask.wrappers.Response:
"""Process_model_move."""
original_process_model_id = _un_modify_modified_process_model_id(
modified_process_model_identifier
)
new_process_model = ProcessModelService().process_model_move(
original_process_model_id, new_location
)
_commit_and_push_to_git(
f"User: {g.user.username} moved process model {original_process_model_id} to"
f" {new_process_model.id}"
)
return make_response(jsonify(new_process_model), 200)
def process_model_publish(
modified_process_model_identifier: str, branch_to_update: Optional[str] = None
) -> flask.wrappers.Response:
"""Process_model_publish."""
if branch_to_update is None:
branch_to_update = current_app.config["GIT_BRANCH_TO_PUBLISH_TO"]
if branch_to_update is None:
raise MissingGitConfigsError(
"Missing config for GIT_BRANCH_TO_PUBLISH_TO. "
"This is required for publishing process models"
)
process_model_identifier = _un_modify_modified_process_model_id(
modified_process_model_identifier
)
pr_url = GitService().publish(process_model_identifier, branch_to_update)
data = {"ok": True, "pr_url": pr_url}
return Response(json.dumps(data), status=200, mimetype="application/json")
def process_model_list(
process_group_identifier: Optional[str] = None,
recursive: Optional[bool] = False,
filter_runnable_by_user: Optional[bool] = False,
include_parent_groups: Optional[bool] = False,
page: int = 1,
per_page: int = 100,
) -> flask.wrappers.Response:
"""Process model list!"""
process_models = ProcessModelService.get_process_models(
process_group_id=process_group_identifier,
recursive=recursive,
filter_runnable_by_user=filter_runnable_by_user,
)
process_models_to_return = ProcessModelService().get_batch(
process_models, page=page, per_page=per_page
)
if include_parent_groups:
process_group_cache = IdToProcessGroupMapping({})
for process_model in process_models_to_return:
parent_group_lites_with_cache = (
ProcessModelService.get_parent_group_array_and_cache_it(
process_model.id, process_group_cache
)
)
process_model.parent_groups = parent_group_lites_with_cache[
"process_groups"
]
pages = len(process_models) // per_page
remainder = len(process_models) % per_page
if remainder > 0:
pages += 1
response_json = {
"results": process_models_to_return,
"pagination": {
"count": len(process_models_to_return),
"total": len(process_models),
"pages": pages,
},
}
return make_response(jsonify(response_json), 200)
def process_model_file_update(
modified_process_model_identifier: str, file_name: str
) -> flask.wrappers.Response:
"""Process_model_file_update."""
process_model_identifier = modified_process_model_identifier.replace(":", "/")
process_model = _get_process_model(process_model_identifier)
request_file = _get_file_from_request()
request_file_contents = request_file.stream.read()
if not request_file_contents:
raise ApiError(
error_code="file_contents_empty",
message="Given request file does not have any content",
status_code=400,
)
SpecFileService.update_file(process_model, file_name, request_file_contents)
_commit_and_push_to_git(
f"User: {g.user.username} clicked save for"
f" {process_model_identifier}/{file_name}"
)
return Response(json.dumps({"ok": True}), status=200, mimetype="application/json")
def process_model_file_delete(
modified_process_model_identifier: str, file_name: str
) -> flask.wrappers.Response:
"""Process_model_file_delete."""
process_model_identifier = modified_process_model_identifier.replace(":", "/")
process_model = _get_process_model(process_model_identifier)
try:
SpecFileService.delete_file(process_model, file_name)
except FileNotFoundError as exception:
raise (
ApiError(
error_code="process_model_file_cannot_be_found",
message=f"Process model file cannot be found: {file_name}",
status_code=400,
)
) from exception
_commit_and_push_to_git(
f"User: {g.user.username} deleted process model file"
f" {process_model_identifier}/{file_name}"
)
return Response(json.dumps({"ok": True}), status=200, mimetype="application/json")
def process_model_file_create(
modified_process_model_identifier: str,
) -> flask.wrappers.Response:
"""Process_model_file_create."""
process_model_identifier = modified_process_model_identifier.replace(":", "/")
process_model = _get_process_model(process_model_identifier)
request_file = _get_file_from_request()
if not request_file.filename:
raise ApiError(
error_code="could_not_get_filename",
message="Could not get filename from request",
status_code=400,
)
file = SpecFileService.add_file(
process_model, request_file.filename, request_file.stream.read()
)
file_contents = SpecFileService.get_data(process_model, file.name)
file.file_contents = file_contents
file.process_model_id = process_model.id
_commit_and_push_to_git(
f"User: {g.user.username} added process model file"
f" {process_model_identifier}/{file.name}"
)
return Response(
json.dumps(FileSchema().dump(file)), status=201, mimetype="application/json"
)
def process_model_file_show(
modified_process_model_identifier: str, file_name: str
) -> Any:
"""Process_model_file_show."""
process_model_identifier = modified_process_model_identifier.replace(":", "/")
process_model = _get_process_model(process_model_identifier)
files = SpecFileService.get_files(process_model, file_name)
if len(files) == 0:
raise ApiError(
error_code="unknown file",
message=(
f"No information exists for file {file_name}"
f" it does not exist in workflow {process_model_identifier}."
),
status_code=404,
)
file = files[0]
file_contents = SpecFileService.get_data(process_model, file.name)
file.file_contents = file_contents
file.process_model_id = process_model.id
return FileSchema().dump(file)
# {
# "natural_language_text": "Create a bug tracker process model \
# with a bug-details form that collects summary, description, and priority"
# }
def process_model_create_with_natural_language(
modified_process_group_id: str, body: Dict[str, str]
) -> flask.wrappers.Response:
"""Process_model_create_with_natural_language."""
pattern = re.compile(
r"Create a (?P<pm_name>.*?) process model with a (?P<form_name>.*?) form that"
r" collects (?P<columns>.*)"
)
match = pattern.match(body["natural_language_text"])
if match is None:
raise ApiError(
error_code="natural_language_text_not_yet_supported",
message=(
"Natural language text is not yet supported. Please use the form:"
f" {pattern.pattern}"
),
status_code=400,
)
process_model_display_name = match.group("pm_name")
process_model_identifier = re.sub(r"[ _]", "-", process_model_display_name)
process_model_identifier = re.sub(r"-{2,}", "-", process_model_identifier).lower()
form_name = match.group("form_name")
form_identifier = re.sub(r"[ _]", "-", form_name)
form_identifier = re.sub(r"-{2,}", "-", form_identifier).lower()
column_names = match.group("columns")
columns = re.sub(r"(, (and )?)", ",", column_names).split(",")
process_group = _get_process_group_from_modified_identifier(
modified_process_group_id
)
qualified_process_model_identifier = (
f"{process_group.id}/{process_model_identifier}"
)
metadata_extraction_paths = []
for column in columns:
metadata_extraction_paths.append({"key": column, "path": column})
process_model_attributes = {
"id": qualified_process_model_identifier,
"display_name": process_model_display_name,
"description": None,
"metadata_extraction_paths": metadata_extraction_paths,
}
process_model_info = ProcessModelInfo(**process_model_attributes) # type: ignore
if process_model_info is None:
raise ApiError(
error_code="process_model_could_not_be_created",
message=f"Process Model could not be created from given body: {body}",
status_code=400,
)
bpmn_template_file = os.path.join(
current_app.root_path, "templates", "basic_with_user_task_template.bpmn"
)
if not os.path.exists(bpmn_template_file):
raise ApiError(
error_code="bpmn_template_file_does_not_exist",
message="Could not find the bpmn template file to create process model.",
status_code=500,
)
ProcessModelService.add_process_model(process_model_info)
bpmn_process_identifier = f"{process_model_identifier}_process"
bpmn_template_contents = ""
with open(bpmn_template_file, encoding="utf-8") as f:
bpmn_template_contents = f.read()
bpmn_template_contents = bpmn_template_contents.replace(
"natural_language_process_id_template", bpmn_process_identifier
)
bpmn_template_contents = bpmn_template_contents.replace(
"form-identifier-id-template", form_identifier
)
form_uischema_json: dict = {"ui:order": columns}
form_properties: dict = {}
for column in columns:
form_properties[column] = {
"type": "string",
"title": column,
}
form_schema_json = {
"title": form_identifier,
"description": "",
"properties": form_properties,
"required": [],
}
SpecFileService.add_file(
process_model_info,
f"{process_model_identifier}.bpmn",
str.encode(bpmn_template_contents),
)
SpecFileService.add_file(
process_model_info,
f"{form_identifier}-schema.json",
str.encode(json.dumps(form_schema_json)),
)
SpecFileService.add_file(
process_model_info,
f"{form_identifier}-uischema.json",
str.encode(json.dumps(form_uischema_json)),
)
_commit_and_push_to_git(
f"User: {g.user.username} created process model via natural language:"
f" {process_model_info.id}"
)
default_report_metadata = ProcessInstanceReportService.system_metadata_map(
"default"
)
for column in columns:
default_report_metadata["columns"].append(
{"Header": column, "accessor": column, "filterable": True}
)
ProcessInstanceReportModel.create_report(
identifier=process_model_identifier,
user=g.user,
report_metadata=default_report_metadata,
)
return Response(
json.dumps(ProcessModelInfoSchema().dump(process_model_info)),
status=201,
mimetype="application/json",
)
def _get_file_from_request() -> Any:
"""Get_file_from_request."""
request_file = connexion.request.files.get("file")
if not request_file:
raise ApiError(
error_code="no_file_given",
message="Given request does not contain a file",
status_code=400,
)
return request_file
def _get_process_group_from_modified_identifier(
modified_process_group_id: str,
) -> ProcessGroup:
"""_get_process_group_from_modified_identifier."""
if modified_process_group_id is None:
raise ApiError(
error_code="process_group_id_not_specified",
message=(
"Process Model could not be created when process_group_id path param is"
" unspecified"
),
status_code=400,
)
unmodified_process_group_id = _un_modify_modified_process_model_id(
modified_process_group_id
)
process_group = ProcessModelService.get_process_group(unmodified_process_group_id)
if process_group is None:
raise ApiError(
error_code="process_model_could_not_be_created",
message=(
"Process Model could not be created from given body because Process"
f" Group could not be found: {unmodified_process_group_id}"
),
status_code=400,
)
return process_group

View File

@ -0,0 +1,134 @@
"""APIs for dealing with process groups, process models, and process instances."""
import json
import random
import string
from typing import Dict
from typing import Union
import flask.wrappers
from flask import current_app
from flask import jsonify
from flask import make_response
from flask.wrappers import Response
from flask_bpmn.api.api_error import ApiError
from lxml import etree # type: ignore
from lxml.builder import ElementMaker # type: ignore
from spiffworkflow_backend.routes.process_api_blueprint import _get_process_model
from spiffworkflow_backend.routes.process_api_blueprint import (
_get_required_parameter_or_raise,
)
from spiffworkflow_backend.services.script_unit_test_runner import ScriptUnitTestRunner
from spiffworkflow_backend.services.spec_file_service import SpecFileService
def script_unit_test_create(
modified_process_model_identifier: str, body: Dict[str, Union[str, bool, int]]
) -> flask.wrappers.Response:
"""Script_unit_test_create."""
bpmn_task_identifier = _get_required_parameter_or_raise(
"bpmn_task_identifier", body
)
input_json = _get_required_parameter_or_raise("input_json", body)
expected_output_json = _get_required_parameter_or_raise(
"expected_output_json", body
)
process_model_identifier = modified_process_model_identifier.replace(":", "/")
process_model = _get_process_model(process_model_identifier)
file = SpecFileService.get_files(process_model, process_model.primary_file_name)[0]
if file is None:
raise ApiError(
error_code="cannot_find_file",
message=(
"Could not find the primary bpmn file for process_model:"
f" {process_model.id}"
),
status_code=404,
)
# 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)
nsmap = bpmn_etree_element.nsmap
spiff_element_maker = ElementMaker(
namespace="http://spiffworkflow.org/bpmn/schema/1.0/core", nsmap=nsmap
)
script_task_elements = bpmn_etree_element.xpath(
f"//bpmn:scriptTask[@id='{bpmn_task_identifier}']",
namespaces={"bpmn": "http://www.omg.org/spec/BPMN/20100524/MODEL"},
)
if len(script_task_elements) == 0:
raise ApiError(
error_code="missing_script_task",
message=f"Cannot find a script task with id: {bpmn_task_identifier}",
status_code=404,
)
script_task_element = script_task_elements[0]
extension_elements = None
extension_elements_array = script_task_element.xpath(
".//bpmn:extensionElements",
namespaces={"bpmn": "http://www.omg.org/spec/BPMN/20100524/MODEL"},
)
if len(extension_elements_array) == 0:
bpmn_element_maker = ElementMaker(
namespace="http://www.omg.org/spec/BPMN/20100524/MODEL", nsmap=nsmap
)
extension_elements = bpmn_element_maker("extensionElements")
script_task_element.append(extension_elements)
else:
extension_elements = extension_elements_array[0]
unit_test_elements = None
unit_test_elements_array = extension_elements.xpath(
"//spiffworkflow:unitTests",
namespaces={"spiffworkflow": "http://spiffworkflow.org/bpmn/schema/1.0/core"},
)
if len(unit_test_elements_array) == 0:
unit_test_elements = spiff_element_maker("unitTests")
extension_elements.append(unit_test_elements)
else:
unit_test_elements = unit_test_elements_array[0]
fuzz = "".join(
random.choice(string.ascii_uppercase + string.digits) # noqa: S311
for _ in range(7)
)
unit_test_id = f"unit_test_{fuzz}"
input_json_element = spiff_element_maker("inputJson", json.dumps(input_json))
expected_output_json_element = spiff_element_maker(
"expectedOutputJson", json.dumps(expected_output_json)
)
unit_test_element = spiff_element_maker("unitTest", id=unit_test_id)
unit_test_element.append(input_json_element)
unit_test_element.append(expected_output_json_element)
unit_test_elements.append(unit_test_element)
SpecFileService.update_file(
process_model, file.name, etree.tostring(bpmn_etree_element)
)
return Response(json.dumps({"ok": True}), status=202, mimetype="application/json")
def script_unit_test_run(
modified_process_model_identifier: str, body: Dict[str, Union[str, bool, int]]
) -> flask.wrappers.Response:
"""Script_unit_test_run."""
# FIXME: We should probably clear this somewhere else but this works
current_app.config["THREAD_LOCAL_DATA"].process_instance_id = None
current_app.config["THREAD_LOCAL_DATA"].spiff_step = None
python_script = _get_required_parameter_or_raise("python_script", body)
input_json = _get_required_parameter_or_raise("input_json", body)
expected_output_json = _get_required_parameter_or_raise(
"expected_output_json", body
)
result = ScriptUnitTestRunner.run_with_script_and_pre_post_contexts(
python_script, input_json, expected_output_json
)
return make_response(jsonify(result), 200)

View File

@ -0,0 +1,67 @@
"""APIs for dealing with process groups, process models, and process instances."""
import json
from typing import Dict
from typing import Optional
from flask import g
from flask import jsonify
from flask import make_response
from flask.wrappers import Response
from spiffworkflow_backend.models.secret_model import SecretModel
from spiffworkflow_backend.models.secret_model import SecretModelSchema
from spiffworkflow_backend.models.user import UserModel
from spiffworkflow_backend.services.secret_service import SecretService
from spiffworkflow_backend.services.user_service import UserService
def secret_show(key: str) -> Optional[str]:
"""Secret_show."""
return SecretService.get_secret(key)
def secret_list(
page: int = 1,
per_page: int = 100,
) -> Response:
"""Secret_list."""
secrets = (
SecretModel.query.order_by(SecretModel.key)
.join(UserModel)
.add_columns(
UserModel.username,
)
.paginate(page=page, per_page=per_page, error_out=False)
)
response_json = {
"results": secrets.items,
"pagination": {
"count": len(secrets.items),
"total": secrets.total,
"pages": secrets.pages,
},
}
return make_response(jsonify(response_json), 200)
def secret_create(body: Dict) -> Response:
"""Add secret."""
secret_model = SecretService().add_secret(body["key"], body["value"], g.user.id)
return Response(
json.dumps(SecretModelSchema().dump(secret_model)),
status=201,
mimetype="application/json",
)
def secret_update(key: str, body: dict) -> Response:
"""Update secret."""
SecretService().update_secret(key, body["value"], g.user.id)
return Response(json.dumps({"ok": True}), status=200, mimetype="application/json")
def secret_delete(key: str) -> Response:
"""Delete secret."""
current_user = UserService.current_user()
SecretService.delete_secret(key, current_user.id)
return Response(json.dumps({"ok": True}), status=200, mimetype="application/json")

View File

@ -0,0 +1,49 @@
"""APIs for dealing with process groups, process models, and process instances."""
import json
import flask.wrappers
import werkzeug
from flask import current_app
from flask import g
from flask import redirect
from flask import request
from flask.wrappers import Response
from spiffworkflow_backend.routes.user import verify_token
from spiffworkflow_backend.services.secret_service import SecretService
from spiffworkflow_backend.services.service_task_service import ServiceTaskService
def service_task_list() -> flask.wrappers.Response:
"""Service_task_list."""
available_connectors = ServiceTaskService.available_connectors()
return Response(
json.dumps(available_connectors), status=200, mimetype="application/json"
)
def authentication_list() -> flask.wrappers.Response:
"""Authentication_list."""
available_authentications = ServiceTaskService.authentication_list()
response_json = {
"results": available_authentications,
"connector_proxy_base_url": current_app.config["CONNECTOR_PROXY_URL"],
"redirect_url": f"{current_app.config['SPIFFWORKFLOW_BACKEND_URL']}/v1.0/authentication_callback",
}
return Response(json.dumps(response_json), status=200, mimetype="application/json")
def authentication_callback(
service: str,
auth_method: str,
) -> werkzeug.wrappers.Response:
"""Authentication_callback."""
verify_token(request.args.get("token"), force_run=True)
response = request.args["response"]
SecretService().update_secret(
f"{service}/{auth_method}", response, g.user.id, create_if_not_exists=True
)
return redirect(
f"{current_app.config['SPIFFWORKFLOW_FRONTEND_URL']}/admin/configuration"
)

View File

@ -0,0 +1,563 @@
"""APIs for dealing with process groups, process models, and process instances."""
import json
import os
import uuid
from typing import Any
from typing import Dict
from typing import Optional
from typing import TypedDict
from typing import Union
import flask.wrappers
import jinja2
from flask import g
from flask import jsonify
from flask import make_response
from flask.wrappers import Response
from flask_bpmn.api.api_error import ApiError
from flask_bpmn.models.db import db
from SpiffWorkflow.task import Task as SpiffTask # type: ignore
from SpiffWorkflow.task import TaskState
from sqlalchemy import and_
from sqlalchemy import asc
from sqlalchemy import desc
from sqlalchemy import func
from sqlalchemy.orm import aliased
from spiffworkflow_backend.models.group import GroupModel
from spiffworkflow_backend.models.human_task import HumanTaskModel
from spiffworkflow_backend.models.human_task_user import HumanTaskUserModel
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
from spiffworkflow_backend.models.process_instance import ProcessInstanceStatus
from spiffworkflow_backend.models.process_model import ProcessModelInfo
from spiffworkflow_backend.models.user import UserModel
from spiffworkflow_backend.routes.process_api_blueprint import (
_find_principal_or_raise,
)
from spiffworkflow_backend.routes.process_api_blueprint import (
_find_process_instance_by_id_or_raise,
)
from spiffworkflow_backend.routes.process_api_blueprint import _get_process_model
from spiffworkflow_backend.services.authorization_service import AuthorizationService
from spiffworkflow_backend.services.file_system_service import FileSystemService
from spiffworkflow_backend.services.process_instance_processor import (
ProcessInstanceProcessor,
)
from spiffworkflow_backend.services.process_instance_service import (
ProcessInstanceService,
)
from spiffworkflow_backend.services.process_model_service import ProcessModelService
from spiffworkflow_backend.services.spec_file_service import SpecFileService
class TaskDataSelectOption(TypedDict):
"""TaskDataSelectOption."""
value: str
label: str
class ReactJsonSchemaSelectOption(TypedDict):
"""ReactJsonSchemaSelectOption."""
type: str
title: str
enum: list[str]
# TODO: see comment for before_request
# @process_api_blueprint.route("/v1.0/tasks", methods=["GET"])
def task_list_my_tasks(page: int = 1, per_page: int = 100) -> flask.wrappers.Response:
"""Task_list_my_tasks."""
principal = _find_principal_or_raise()
human_tasks = (
HumanTaskModel.query.order_by(desc(HumanTaskModel.id)) # type: ignore
.join(ProcessInstanceModel)
.join(HumanTaskUserModel)
.filter_by(user_id=principal.user_id)
.filter(HumanTaskModel.completed == False) # noqa: E712
# just need this add_columns to add the process_model_identifier. Then add everything back that was removed.
.add_columns(
ProcessInstanceModel.process_model_identifier,
ProcessInstanceModel.process_model_display_name,
ProcessInstanceModel.status,
HumanTaskModel.task_name,
HumanTaskModel.task_title,
HumanTaskModel.task_type,
HumanTaskModel.task_status,
HumanTaskModel.task_id,
HumanTaskModel.id,
HumanTaskModel.process_model_display_name,
HumanTaskModel.process_instance_id,
)
.paginate(page=page, per_page=per_page, error_out=False)
)
tasks = [HumanTaskModel.to_task(human_task) for human_task in human_tasks.items]
response_json = {
"results": tasks,
"pagination": {
"count": len(human_tasks.items),
"total": human_tasks.total,
"pages": human_tasks.pages,
},
}
return make_response(jsonify(response_json), 200)
def task_list_for_my_open_processes(
page: int = 1, per_page: int = 100
) -> flask.wrappers.Response:
"""Task_list_for_my_open_processes."""
return _get_tasks(page=page, per_page=per_page)
def task_list_for_me(page: int = 1, per_page: int = 100) -> flask.wrappers.Response:
"""Task_list_for_me."""
return _get_tasks(
processes_started_by_user=False,
has_lane_assignment_id=False,
page=page,
per_page=per_page,
)
def task_list_for_my_groups(
user_group_identifier: Optional[str] = None, page: int = 1, per_page: int = 100
) -> flask.wrappers.Response:
"""Task_list_for_my_groups."""
return _get_tasks(
user_group_identifier=user_group_identifier,
processes_started_by_user=False,
page=page,
per_page=per_page,
)
def task_show(process_instance_id: int, task_id: str) -> flask.wrappers.Response:
"""Task_show."""
process_instance = _find_process_instance_by_id_or_raise(process_instance_id)
if process_instance.status == ProcessInstanceStatus.suspended.value:
raise ApiError(
error_code="error_suspended",
message="The process instance is suspended",
status_code=400,
)
process_model = _get_process_model(
process_instance.process_model_identifier,
)
human_task = HumanTaskModel.query.filter_by(
process_instance_id=process_instance_id, task_id=task_id
).first()
if human_task is None:
raise (
ApiError(
error_code="no_human_task",
message=(
f"Cannot find a task to complete for task id '{task_id}' and"
f" process instance {process_instance_id}."
),
status_code=500,
)
)
form_schema_file_name = ""
form_ui_schema_file_name = ""
spiff_task = _get_spiff_task_from_process_instance(task_id, process_instance)
extensions = spiff_task.task_spec.extensions
if "properties" in extensions:
properties = extensions["properties"]
if "formJsonSchemaFilename" in properties:
form_schema_file_name = properties["formJsonSchemaFilename"]
if "formUiSchemaFilename" in properties:
form_ui_schema_file_name = properties["formUiSchemaFilename"]
processor = ProcessInstanceProcessor(process_instance)
task = ProcessInstanceService.spiff_task_to_api_task(processor, spiff_task)
task.data = spiff_task.data
task.process_model_display_name = process_model.display_name
task.process_model_identifier = process_model.id
process_model_with_form = process_model
refs = SpecFileService.get_references_for_process(process_model_with_form)
all_processes = [i.identifier for i in refs]
if task.process_identifier not in all_processes:
bpmn_file_full_path = (
ProcessInstanceProcessor.bpmn_file_full_path_from_bpmn_process_identifier(
task.process_identifier
)
)
relative_path = os.path.relpath(
bpmn_file_full_path, start=FileSystemService.root_path()
)
process_model_relative_path = os.path.dirname(relative_path)
process_model_with_form = (
ProcessModelService.get_process_model_from_relative_path(
process_model_relative_path
)
)
if task.type == "User Task":
if not form_schema_file_name:
raise (
ApiError(
error_code="missing_form_file",
message=(
"Cannot find a form file for process_instance_id:"
f" {process_instance_id}, task_id: {task_id}"
),
status_code=400,
)
)
form_contents = _prepare_form_data(
form_schema_file_name,
task.data,
process_model_with_form,
)
try:
# form_contents is a str
form_dict = json.loads(form_contents)
except Exception as exception:
raise (
ApiError(
error_code="error_loading_form",
message=(
f"Could not load form schema from: {form_schema_file_name}."
f" Error was: {str(exception)}"
),
status_code=400,
)
) from exception
if task.data:
_update_form_schema_with_task_data_as_needed(form_dict, task.data)
if form_contents:
task.form_schema = form_dict
if form_ui_schema_file_name:
ui_form_contents = _prepare_form_data(
form_ui_schema_file_name,
task.data,
process_model_with_form,
)
if ui_form_contents:
task.form_ui_schema = ui_form_contents
if task.properties and task.data and "instructionsForEndUser" in task.properties:
if task.properties["instructionsForEndUser"]:
task.properties["instructionsForEndUser"] = _render_jinja_template(
task.properties["instructionsForEndUser"], task.data
)
return make_response(jsonify(task), 200)
def process_data_show(
process_instance_id: int,
process_data_identifier: str,
modified_process_model_identifier: str,
) -> flask.wrappers.Response:
"""Process_data_show."""
process_instance = _find_process_instance_by_id_or_raise(process_instance_id)
processor = ProcessInstanceProcessor(process_instance)
all_process_data = processor.get_data()
process_data_value = None
if process_data_identifier in all_process_data:
process_data_value = all_process_data[process_data_identifier]
return make_response(
jsonify(
{
"process_data_identifier": process_data_identifier,
"process_data_value": process_data_value,
}
),
200,
)
def task_submit(
process_instance_id: int,
task_id: str,
body: Dict[str, Any],
terminate_loop: bool = False,
) -> flask.wrappers.Response:
"""Task_submit_user_data."""
principal = _find_principal_or_raise()
process_instance = _find_process_instance_by_id_or_raise(process_instance_id)
if not process_instance.can_submit_task():
raise ApiError(
error_code="process_instance_not_runnable",
message=(
f"Process Instance ({process_instance.id}) has status "
f"{process_instance.status} which does not allow tasks to be submitted."
),
status_code=400,
)
processor = ProcessInstanceProcessor(process_instance)
spiff_task = _get_spiff_task_from_process_instance(
task_id, process_instance, processor=processor
)
AuthorizationService.assert_user_can_complete_spiff_task(
process_instance.id, spiff_task, principal.user
)
if spiff_task.state != TaskState.READY:
raise (
ApiError(
error_code="invalid_state",
message="You may not update a task unless it is in the READY state.",
status_code=400,
)
)
if terminate_loop and spiff_task.is_looping():
spiff_task.terminate_loop()
human_task = HumanTaskModel.query.filter_by(
process_instance_id=process_instance_id, task_id=task_id, completed=False
).first()
if human_task is None:
raise (
ApiError(
error_code="no_human_task",
message=(
f"Cannot find a task to complete for task id '{task_id}' and"
f" process instance {process_instance_id}."
),
status_code=500,
)
)
ProcessInstanceService.complete_form_task(
processor=processor,
spiff_task=spiff_task,
data=body,
user=g.user,
human_task=human_task,
)
# If we need to update all tasks, then get the next ready task and if it a multi-instance with the same
# task spec, complete that form as well.
# if update_all:
# last_index = spiff_task.task_info()["mi_index"]
# next_task = processor.next_task()
# while next_task and next_task.task_info()["mi_index"] > last_index:
# __update_task(processor, next_task, form_data, user)
# last_index = next_task.task_info()["mi_index"]
# next_task = processor.next_task()
next_human_task_assigned_to_me = (
HumanTaskModel.query.filter_by(
process_instance_id=process_instance_id, completed=False
)
.order_by(asc(HumanTaskModel.id)) # type: ignore
.join(HumanTaskUserModel)
.filter_by(user_id=principal.user_id)
.first()
)
if next_human_task_assigned_to_me:
return make_response(
jsonify(HumanTaskModel.to_task(next_human_task_assigned_to_me)), 200
)
return Response(json.dumps({"ok": True}), status=202, mimetype="application/json")
def _get_tasks(
processes_started_by_user: bool = True,
has_lane_assignment_id: bool = True,
page: int = 1,
per_page: int = 100,
user_group_identifier: Optional[str] = None,
) -> flask.wrappers.Response:
"""Get_tasks."""
user_id = g.user.id
# use distinct to ensure we only get one row per human task otherwise
# we can get back multiple for the same human task row which throws off
# pagination later on
# https://stackoverflow.com/q/34582014/6090676
human_tasks_query = (
db.session.query(HumanTaskModel)
.group_by(HumanTaskModel.id) # type: ignore
.outerjoin(GroupModel, GroupModel.id == HumanTaskModel.lane_assignment_id)
.join(ProcessInstanceModel)
.join(UserModel, UserModel.id == ProcessInstanceModel.process_initiator_id)
.filter(HumanTaskModel.completed == False) # noqa: E712
)
assigned_user = aliased(UserModel)
if processes_started_by_user:
human_tasks_query = (
human_tasks_query.filter(
ProcessInstanceModel.process_initiator_id == user_id
)
.outerjoin(
HumanTaskUserModel,
HumanTaskModel.id == HumanTaskUserModel.human_task_id,
)
.outerjoin(assigned_user, assigned_user.id == HumanTaskUserModel.user_id)
)
else:
human_tasks_query = human_tasks_query.filter(
ProcessInstanceModel.process_initiator_id != user_id
).join(
HumanTaskUserModel,
and_(
HumanTaskUserModel.user_id == user_id,
HumanTaskModel.id == HumanTaskUserModel.human_task_id,
),
)
if has_lane_assignment_id:
if user_group_identifier:
human_tasks_query = human_tasks_query.filter(
GroupModel.identifier == user_group_identifier
)
else:
human_tasks_query = human_tasks_query.filter(
HumanTaskModel.lane_assignment_id.is_not(None) # type: ignore
)
else:
human_tasks_query = human_tasks_query.filter(HumanTaskModel.lane_assignment_id.is_(None)) # type: ignore
human_tasks = (
human_tasks_query.add_columns(
ProcessInstanceModel.process_model_identifier,
ProcessInstanceModel.status.label("process_instance_status"), # type: ignore
ProcessInstanceModel.updated_at_in_seconds,
ProcessInstanceModel.created_at_in_seconds,
UserModel.username.label("process_initiator_username"), # type: ignore
GroupModel.identifier.label("assigned_user_group_identifier"),
HumanTaskModel.task_name,
HumanTaskModel.task_title,
HumanTaskModel.process_model_display_name,
HumanTaskModel.process_instance_id,
func.group_concat(assigned_user.username.distinct()).label(
"potential_owner_usernames"
),
)
.order_by(desc(HumanTaskModel.id)) # type: ignore
.paginate(page=page, per_page=per_page, error_out=False)
)
response_json = {
"results": human_tasks.items,
"pagination": {
"count": len(human_tasks.items),
"total": human_tasks.total,
"pages": human_tasks.pages,
},
}
return make_response(jsonify(response_json), 200)
def _prepare_form_data(
form_file: str, task_data: Union[dict, None], process_model: ProcessModelInfo
) -> str:
"""Prepare_form_data."""
if task_data is None:
return ""
file_contents = SpecFileService.get_data(process_model, form_file).decode("utf-8")
return _render_jinja_template(file_contents, task_data)
def _render_jinja_template(unprocessed_template: str, data: dict[str, Any]) -> str:
"""Render_jinja_template."""
jinja_environment = jinja2.Environment(
autoescape=True, lstrip_blocks=True, trim_blocks=True
)
template = jinja_environment.from_string(unprocessed_template)
return template.render(**data)
def _get_spiff_task_from_process_instance(
task_id: str,
process_instance: ProcessInstanceModel,
processor: Union[ProcessInstanceProcessor, None] = None,
) -> SpiffTask:
"""Get_spiff_task_from_process_instance."""
if processor is None:
processor = ProcessInstanceProcessor(process_instance)
task_uuid = uuid.UUID(task_id)
spiff_task = processor.bpmn_process_instance.get_task(task_uuid)
if spiff_task is None:
raise (
ApiError(
error_code="empty_task",
message="Processor failed to obtain task.",
status_code=500,
)
)
return spiff_task
# originally from: https://bitcoden.com/answers/python-nested-dictionary-update-value-where-any-nested-key-matches
def _update_form_schema_with_task_data_as_needed(
in_dict: dict, task_data: dict
) -> None:
"""Update_nested."""
for k, value in in_dict.items():
if "anyOf" == k:
# value will look like the array on the right of "anyOf": ["options_from_task_data_var:awesome_options"]
if isinstance(value, list):
if len(value) == 1:
first_element_in_value_list = value[0]
if isinstance(first_element_in_value_list, str):
if first_element_in_value_list.startswith(
"options_from_task_data_var:"
):
task_data_var = first_element_in_value_list.replace(
"options_from_task_data_var:", ""
)
if task_data_var not in task_data:
raise (
ApiError(
error_code="missing_task_data_var",
message=(
"Task data is missing variable:"
f" {task_data_var}"
),
status_code=500,
)
)
select_options_from_task_data = task_data.get(task_data_var)
if isinstance(select_options_from_task_data, list):
if all(
"value" in d and "label" in d
for d in select_options_from_task_data
):
def map_function(
task_data_select_option: TaskDataSelectOption,
) -> ReactJsonSchemaSelectOption:
"""Map_function."""
return {
"type": "string",
"enum": [task_data_select_option["value"]],
"title": task_data_select_option["label"],
}
options_for_react_json_schema_form = list(
map(map_function, select_options_from_task_data)
)
in_dict[k] = options_for_react_json_schema_form
elif isinstance(value, dict):
_update_form_schema_with_task_data_as_needed(value, task_data)
elif isinstance(value, list):
for o in value:
if isinstance(o, dict):
_update_form_schema_with_task_data_as_needed(o, task_data)

View File

@ -67,16 +67,19 @@ def verify_token(
user_model = get_user_from_decoded_internal_token(decoded_token) user_model = get_user_from_decoded_internal_token(decoded_token)
except Exception as e: except Exception as e:
current_app.logger.error( current_app.logger.error(
f"Exception in verify_token getting user from decoded internal token. {e}" "Exception in verify_token getting user from decoded"
f" internal token. {e}"
) )
elif "iss" in decoded_token.keys(): elif "iss" in decoded_token.keys():
try: try:
if AuthenticationService.validate_id_token(token): if AuthenticationService.validate_id_token(token):
user_info = decoded_token user_info = decoded_token
except ApiError as ae: # API Error is only thrown in the token is outdated. except (
ApiError
) as ae: # API Error is only thrown in the token is outdated.
# Try to refresh the token # Try to refresh the token
user = UserService.get_user_by_service_and_service_id( user = UserService.get_user_by_service_and_service_id(
"open_id", decoded_token["sub"] decoded_token["iss"], decoded_token["sub"]
) )
if user: if user:
refresh_token = AuthenticationService.get_refresh_token(user.id) refresh_token = AuthenticationService.get_refresh_token(user.id)
@ -105,10 +108,12 @@ def verify_token(
) from e ) from e
if ( if (
user_info is not None and "error" not in user_info user_info is not None
and "error" not in user_info
and "iss" in user_info
): # not sure what to test yet ): # not sure what to test yet
user_model = ( user_model = (
UserModel.query.filter(UserModel.service == "open_id") UserModel.query.filter(UserModel.service == user_info["iss"])
.filter(UserModel.service_id == user_info["sub"]) .filter(UserModel.service_id == user_info["sub"])
.first() .first()
) )
@ -293,7 +298,6 @@ def get_decoded_token(token: str) -> Optional[Dict]:
try: try:
decoded_token = jwt.decode(token, options={"verify_signature": False}) decoded_token = jwt.decode(token, options={"verify_signature": False})
except Exception as e: except Exception as e:
print(f"Exception in get_token_type: {e}")
raise ApiError( raise ApiError(
error_code="invalid_token", message="Cannot decode token." error_code="invalid_token", message="Cannot decode token."
) from e ) from e
@ -341,9 +345,5 @@ def get_user_from_decoded_internal_token(decoded_token: dict) -> Optional[UserMo
) )
if user: if user:
return user return user
user = UserModel( user = UserService.create_user(service_id, service, service_id)
username=service_id,
service=service,
service_id=service_id,
)
return user return user

View File

@ -26,6 +26,7 @@ user_blueprint = Blueprint("main", __name__)
# user = UserService.create_user('internal', username) # user = UserService.create_user('internal', username)
# return Response(json.dumps({"id": user.id}), status=201, mimetype=APPLICATION_JSON) # return Response(json.dumps({"id": user.id}), status=201, mimetype=APPLICATION_JSON)
# def _create_user(username): # def _create_user(username):
# user = UserModel.query.filter_by(username=username).first() # user = UserModel.query.filter_by(username=username).first()
# if user is not None: # if user is not None:

View File

@ -0,0 +1,26 @@
"""Users_controller."""
import flask
from flask import g
from flask import jsonify
from flask import make_response
from spiffworkflow_backend.models.user import UserModel
def user_search(username_prefix: str) -> flask.wrappers.Response:
"""User_search."""
found_users = UserModel.query.filter(UserModel.username.like(f"{username_prefix}%")).all() # type: ignore
response_json = {
"users": found_users,
"username_prefix": username_prefix,
}
return make_response(jsonify(response_json), 200)
def user_group_list_for_current_user() -> flask.wrappers.Response:
"""User_group_list_for_current_user."""
groups = g.user.groups
# TODO: filter out the default group and have a way to know what is the default group
group_identifiers = [i.identifier for i in groups if i.identifier != "everybody"]
return make_response(jsonify(sorted(group_identifiers)), 200)

View File

@ -1,43 +0,0 @@
"""Get_env."""
from typing import Any
from spiffworkflow_backend.models.group import GroupModel
from spiffworkflow_backend.models.group import GroupNotFoundError
from spiffworkflow_backend.models.script_attributes_context import (
ScriptAttributesContext,
)
from spiffworkflow_backend.models.user import UserModel
from spiffworkflow_backend.models.user import UserNotFoundError
from spiffworkflow_backend.scripts.script import Script
from spiffworkflow_backend.services.user_service import UserService
class AddUserToGroup(Script):
"""AddUserToGroup."""
def get_description(self) -> str:
"""Get_description."""
return """Add a given user to a given group."""
def run(
self,
script_attributes_context: ScriptAttributesContext,
*args: Any,
**kwargs: Any,
) -> Any:
"""Run."""
username = args[0]
group_identifier = args[1]
user = UserModel.query.filter_by(username=username).first()
if user is None:
raise UserNotFoundError(
f"Script 'add_user_to_group' could not find a user with username: {username}"
)
group = GroupModel.query.filter_by(identifier=group_identifier).first()
if group is None:
raise GroupNotFoundError(
f"Script 'add_user_to_group' could not find group with identifier '{group_identifier}'."
)
UserService.add_user_to_group(user, group)

View File

@ -0,0 +1,63 @@
"""Delete_process_instances_with_criteria."""
from time import time
from typing import Any
from flask_bpmn.models.db import db
from sqlalchemy import or_
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
from spiffworkflow_backend.models.script_attributes_context import (
ScriptAttributesContext,
)
from spiffworkflow_backend.models.spiff_step_details import SpiffStepDetailsModel
from spiffworkflow_backend.scripts.script import Script
class DeleteProcessInstancesWithCriteria(Script):
"""DeleteProcessInstancesWithCriteria."""
def get_description(self) -> str:
"""Get_description."""
return "Delete process instances that match the provided criteria,"
def run(
self,
script_attributes_context: ScriptAttributesContext,
*args: Any,
**kwargs: Any,
) -> Any:
"""Run."""
criteria_list = args[0]
delete_criteria = []
delete_time = time()
for criteria in criteria_list:
delete_criteria.append(
(ProcessInstanceModel.process_model_identifier == criteria["name"])
& ProcessInstanceModel.status.in_(criteria["status"]) # type: ignore
& (
ProcessInstanceModel.updated_at_in_seconds
< (delete_time - criteria["last_updated_delta"])
)
)
results = (
ProcessInstanceModel.query.filter(or_(*delete_criteria)).limit(100).all()
)
rows_affected = len(results)
if rows_affected > 0:
ids_to_delete = list(map(lambda r: r.id, results)) # type: ignore
step_details = SpiffStepDetailsModel.query.filter(
SpiffStepDetailsModel.process_instance_id.in_(ids_to_delete) # type: ignore
).all()
for deletion in step_details:
db.session.delete(deletion)
for deletion in results:
db.session.delete(deletion)
db.session.commit()
return rows_affected

View File

@ -10,6 +10,11 @@ from spiffworkflow_backend.scripts.script import Script
class FactService(Script): class FactService(Script):
"""FactService.""" """FactService."""
@staticmethod
def requires_privileged_permissions() -> bool:
"""We have deemed this function safe to run without elevated permissions."""
return False
def get_description(self) -> str: def get_description(self) -> str:
"""Get_description.""" """Get_description."""
return """Just your basic class that can pull in data from a few api endpoints and return """Just your basic class that can pull in data from a few api endpoints and
@ -30,7 +35,10 @@ class FactService(Script):
if fact == "cat": if fact == "cat":
details = "The cat in the hat" # self.get_cat() details = "The cat in the hat" # self.get_cat()
elif fact == "norris": elif fact == "norris":
details = "Chuck Norris doesnt read books. He stares them down until he gets the information he wants." details = (
"Chuck Norris doesnt read books. He stares them down until he gets the"
" information he wants."
)
elif fact == "buzzword": elif fact == "buzzword":
details = "Move the Needle." # self.get_buzzword() details = "Move the Needle." # self.get_buzzword()
else: else:

View File

@ -0,0 +1,71 @@
"""Get_env."""
from collections import OrderedDict
from typing import Any
from spiffworkflow_backend.models.group import GroupModel
from spiffworkflow_backend.models.permission_assignment import PermissionAssignmentModel
from spiffworkflow_backend.models.permission_target import PermissionTargetModel
from spiffworkflow_backend.models.principal import PrincipalModel
from spiffworkflow_backend.models.script_attributes_context import (
ScriptAttributesContext,
)
from spiffworkflow_backend.scripts.script import Script
class GetAllPermissions(Script):
"""GetAllPermissions."""
def get_description(self) -> str:
"""Get_description."""
return """Get all permissions currently in the system."""
def run(
self,
script_attributes_context: ScriptAttributesContext,
*args: Any,
**kwargs: Any,
) -> Any:
"""Run."""
permission_assignments = (
PermissionAssignmentModel.query.join(
PrincipalModel,
PrincipalModel.id == PermissionAssignmentModel.principal_id,
)
.join(GroupModel, GroupModel.id == PrincipalModel.group_id)
.join(
PermissionTargetModel,
PermissionTargetModel.id
== PermissionAssignmentModel.permission_target_id,
)
.add_columns(
PermissionAssignmentModel.permission,
PermissionTargetModel.uri,
GroupModel.identifier.label("group_identifier"),
)
)
permissions: OrderedDict[tuple[str, str], list[str]] = OrderedDict()
for pa in permission_assignments:
permissions.setdefault((pa.group_identifier, pa.uri), []).append(
pa.permission
)
def replace_suffix(string: str, old: str, new: str) -> str:
"""Replace_suffix."""
if string.endswith(old):
return string[: -len(old)] + new
return string
# sort list of strings based on a specific order
def sort_by_order(string_list: list, order: list) -> list:
"""Sort_by_order."""
return sorted(string_list, key=lambda x: order.index(x))
return [
{
"group_identifier": k[0],
"uri": replace_suffix(k[1], "%", "*"),
"permissions": sort_by_order(v, ["create", "read", "update", "delete"]),
}
for k, v in permissions.items()
]

View File

@ -12,6 +12,11 @@ from spiffworkflow_backend.scripts.script import Script
class GetCurrentUser(Script): class GetCurrentUser(Script):
"""GetCurrentUser.""" """GetCurrentUser."""
@staticmethod
def requires_privileged_permissions() -> bool:
"""We have deemed this function safe to run without elevated permissions."""
return False
def get_description(self) -> str: def get_description(self) -> str:
"""Get_description.""" """Get_description."""
return """Return the current user.""" return """Return the current user."""

View File

@ -10,6 +10,11 @@ from spiffworkflow_backend.scripts.script import Script
class GetEnv(Script): class GetEnv(Script):
"""GetEnv.""" """GetEnv."""
@staticmethod
def requires_privileged_permissions() -> bool:
"""We have deemed this function safe to run without elevated permissions."""
return False
def get_description(self) -> str: def get_description(self) -> str:
"""Get_description.""" """Get_description."""
return """Returns the current environment - ie testing, staging, production.""" return """Returns the current environment - ie testing, staging, production."""

View File

@ -12,6 +12,11 @@ from spiffworkflow_backend.scripts.script import Script
class GetFrontendUrl(Script): class GetFrontendUrl(Script):
"""GetFrontendUrl.""" """GetFrontendUrl."""
@staticmethod
def requires_privileged_permissions() -> bool:
"""We have deemed this function safe to run without elevated permissions."""
return False
def get_description(self) -> str: def get_description(self) -> str:
"""Get_description.""" """Get_description."""
return """Return the url to the frontend.""" return """Return the url to the frontend."""

View File

@ -12,6 +12,11 @@ from spiffworkflow_backend.scripts.script import Script
class GetGroupMembers(Script): class GetGroupMembers(Script):
"""GetGroupMembers.""" """GetGroupMembers."""
@staticmethod
def requires_privileged_permissions() -> bool:
"""We have deemed this function safe to run without elevated permissions."""
return False
def get_description(self) -> str: def get_description(self) -> str:
"""Get_description.""" """Get_description."""
return """Return the list of usernames of the users in the given group.""" return """Return the list of usernames of the users in the given group."""
@ -27,7 +32,8 @@ class GetGroupMembers(Script):
group = GroupModel.query.filter_by(identifier=group_identifier).first() group = GroupModel.query.filter_by(identifier=group_identifier).first()
if group is None: if group is None:
raise GroupNotFoundError( raise GroupNotFoundError(
f"Script 'get_group_members' could not find group with identifier '{group_identifier}'." "Script 'get_group_members' could not find group with identifier"
f" '{group_identifier}'."
) )
usernames = [u.username for u in group.users] usernames = [u.username for u in group.users]

View File

@ -14,6 +14,11 @@ from spiffworkflow_backend.scripts.script import Script
class GetLocaltime(Script): class GetLocaltime(Script):
"""GetLocaltime.""" """GetLocaltime."""
@staticmethod
def requires_privileged_permissions() -> bool:
"""We have deemed this function safe to run without elevated permissions."""
return False
def get_description(self) -> str: def get_description(self) -> str:
"""Get_description.""" """Get_description."""
return """Converts a Datetime object into a Datetime object for a specific timezone. return """Converts a Datetime object into a Datetime object for a specific timezone.

View File

@ -10,6 +10,11 @@ from spiffworkflow_backend.scripts.script import Script
class GetProcessInfo(Script): class GetProcessInfo(Script):
"""GetProcessInfo.""" """GetProcessInfo."""
@staticmethod
def requires_privileged_permissions() -> bool:
"""We have deemed this function safe to run without elevated permissions."""
return False
def get_description(self) -> str: def get_description(self) -> str:
"""Get_description.""" """Get_description."""
return """Returns a dictionary of information about the currently running process.""" return """Returns a dictionary of information about the currently running process."""
@ -23,5 +28,7 @@ class GetProcessInfo(Script):
"""Run.""" """Run."""
return { return {
"process_instance_id": script_attributes_context.process_instance_id, "process_instance_id": script_attributes_context.process_instance_id,
"process_model_identifier": script_attributes_context.process_model_identifier, "process_model_identifier": (
script_attributes_context.process_model_identifier
),
} }

View File

@ -0,0 +1,39 @@
"""Get_env."""
from typing import Any
from spiffworkflow_backend.models.script_attributes_context import (
ScriptAttributesContext,
)
from spiffworkflow_backend.scripts.script import Script
from spiffworkflow_backend.services.authorization_service import AuthorizationService
class RefreshPermissions(Script):
"""RefreshPermissions."""
def get_description(self) -> str:
"""Get_description."""
return """Add permissions using a dict.
group_info: [
{
'name': group_identifier,
'users': array_of_users,
'permissions': [
{
'actions': array_of_actions - create, read, etc,
'uri': target_uri
}
]
}
]
"""
def run(
self,
script_attributes_context: ScriptAttributesContext,
*args: Any,
**kwargs: Any,
) -> Any:
"""Run."""
group_info = args[0]
AuthorizationService.refresh_permissions(group_info)

View File

@ -10,9 +10,12 @@ from typing import Callable
from flask_bpmn.api.api_error import ApiError from flask_bpmn.api.api_error import ApiError
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
from spiffworkflow_backend.models.process_instance import ProcessInstanceNotFoundError
from spiffworkflow_backend.models.script_attributes_context import ( from spiffworkflow_backend.models.script_attributes_context import (
ScriptAttributesContext, ScriptAttributesContext,
) )
from spiffworkflow_backend.services.authorization_service import AuthorizationService
# Generally speaking, having some global in a flask app is TERRIBLE. # Generally speaking, having some global in a flask app is TERRIBLE.
# This is here, because after loading the application this will never change under # This is here, because after loading the application this will never change under
@ -20,6 +23,10 @@ from spiffworkflow_backend.models.script_attributes_context import (
SCRIPT_SUB_CLASSES = None SCRIPT_SUB_CLASSES = None
class ScriptUnauthorizedForUserError(Exception):
"""ScriptUnauthorizedForUserError."""
class Script: class Script:
"""Provides an abstract class that defines how scripts should work, this must be extended in all Script Tasks.""" """Provides an abstract class that defines how scripts should work, this must be extended in all Script Tasks."""
@ -43,6 +50,15 @@ class Script:
+ "does not properly implement the run function.", + "does not properly implement the run function.",
) )
@staticmethod
def requires_privileged_permissions() -> bool:
"""It seems safer to default to True and make safe functions opt in for any user to run them.
To give access to script for a given user, add a 'create' permission with following target-uri:
'/can-run-privileged-script/{script_name}'
"""
return True
@staticmethod @staticmethod
def generate_augmented_list( def generate_augmented_list(
script_attributes_context: ScriptAttributesContext, script_attributes_context: ScriptAttributesContext,
@ -71,18 +87,52 @@ class Script:
that we created. that we created.
""" """
instance = subclass() instance = subclass()
return lambda *ar, **kw: subclass.run(
instance, def check_script_permission() -> None:
script_attributes_context, """Check_script_permission."""
*ar, if subclass.requires_privileged_permissions():
**kw, script_function_name = get_script_function_name(subclass)
) uri = f"/can-run-privileged-script/{script_function_name}"
process_instance = ProcessInstanceModel.query.filter_by(
id=script_attributes_context.process_instance_id
).first()
if process_instance is None:
raise ProcessInstanceNotFoundError(
"Could not find a process instance with id"
f" '{script_attributes_context.process_instance_id}' when"
f" running script '{script_function_name}'"
)
user = process_instance.process_initiator
has_permission = AuthorizationService.user_has_permission(
user=user, permission="create", target_uri=uri
)
if not has_permission:
raise ScriptUnauthorizedForUserError(
f"User {user.username} does not have access to run"
f" privileged script '{script_function_name}'"
)
def run_script_if_allowed(*ar: Any, **kw: Any) -> Any:
"""Run_script_if_allowed."""
check_script_permission()
return subclass.run(
instance,
script_attributes_context,
*ar,
**kw,
)
return run_script_if_allowed
def get_script_function_name(subclass: type[Script]) -> str:
"""Get_script_function_name."""
return subclass.__module__.split(".")[-1]
execlist = {} execlist = {}
subclasses = Script.get_all_subclasses() subclasses = Script.get_all_subclasses()
for x in range(len(subclasses)): for x in range(len(subclasses)):
subclass = subclasses[x] subclass = subclasses[x]
execlist[subclass.__module__.split(".")[-1]] = make_closure( execlist[get_script_function_name(subclass)] = make_closure(
subclass, script_attributes_context=script_attributes_context subclass, script_attributes_context=script_attributes_context
) )
return execlist return execlist
@ -101,7 +151,7 @@ class Script:
"""_get_all_subclasses.""" """_get_all_subclasses."""
# hackish mess to make sure we have all the modules loaded for the scripts # hackish mess to make sure we have all the modules loaded for the scripts
pkg_dir = os.path.dirname(__file__) pkg_dir = os.path.dirname(__file__)
for (_module_loader, name, _ispkg) in pkgutil.iter_modules([pkg_dir]): for _module_loader, name, _ispkg in pkgutil.iter_modules([pkg_dir]):
importlib.import_module("." + name, __package__) importlib.import_module("." + name, __package__)
"""Returns a list of all classes that extend this class.""" """Returns a list of all classes that extend this class."""

View File

@ -29,7 +29,6 @@ def load_acceptance_test_fixtures() -> list[ProcessInstanceModel]:
# suspended - 6 hours ago # suspended - 6 hours ago
process_instances = [] process_instances = []
for i in range(len(statuses)): for i in range(len(statuses)):
process_instance = ProcessInstanceService.create_process_instance_from_process_model_identifier( process_instance = ProcessInstanceService.create_process_instance_from_process_model_identifier(
test_process_model_id, user test_process_model_id, user
) )

View File

@ -93,7 +93,7 @@ class AuthenticationService:
+ f"?state={state}&" + f"?state={state}&"
+ "response_type=code&" + "response_type=code&"
+ f"client_id={self.client_id()}&" + f"client_id={self.client_id()}&"
+ "scope=openid&" + "scope=openid profile email&"
+ f"redirect_uri={return_redirect_url}" + f"redirect_uri={return_redirect_url}"
) )
return login_redirect_url return login_redirect_url

View File

@ -1,10 +1,14 @@
"""Authorization_service.""" """Authorization_service."""
import inspect import inspect
import re import re
from dataclasses import dataclass
from hashlib import sha256 from hashlib import sha256
from hmac import compare_digest from hmac import compare_digest
from hmac import HMAC from hmac import HMAC
from typing import Any
from typing import Optional from typing import Optional
from typing import Set
from typing import TypedDict
from typing import Union from typing import Union
import jwt import jwt
@ -19,6 +23,7 @@ from SpiffWorkflow.task import Task as SpiffTask # type: ignore
from sqlalchemy import or_ from sqlalchemy import or_
from sqlalchemy import text from sqlalchemy import text
from spiffworkflow_backend.helpers.api_version import V1_API_PATH_PREFIX
from spiffworkflow_backend.models.group import GroupModel from spiffworkflow_backend.models.group import GroupModel
from spiffworkflow_backend.models.human_task import HumanTaskModel from spiffworkflow_backend.models.human_task import HumanTaskModel
from spiffworkflow_backend.models.permission_assignment import PermissionAssignmentModel from spiffworkflow_backend.models.permission_assignment import PermissionAssignmentModel
@ -45,6 +50,40 @@ class UserDoesNotHaveAccessToTaskError(Exception):
"""UserDoesNotHaveAccessToTaskError.""" """UserDoesNotHaveAccessToTaskError."""
class InvalidPermissionError(Exception):
"""InvalidPermissionError."""
@dataclass
class PermissionToAssign:
"""PermissionToAssign."""
permission: str
target_uri: str
# the relevant permissions are the only API methods that are currently available for each path prefix.
# if we add further API methods, we'll need to evaluate whether they should be added here.
PATH_SEGMENTS_FOR_PERMISSION_ALL = [
{"path": "/logs", "relevant_permissions": ["read"]},
{
"path": "/process-instances",
"relevant_permissions": ["create", "read", "delete"],
},
{"path": "/process-instance-suspend", "relevant_permissions": ["create"]},
{"path": "/process-instance-terminate", "relevant_permissions": ["create"]},
{"path": "/task-data", "relevant_permissions": ["read", "update"]},
{"path": "/process-data", "relevant_permissions": ["read"]},
]
class DesiredPermissionDict(TypedDict):
"""DesiredPermissionDict."""
group_identifiers: Set[str]
permission_assignments: list[PermissionAssignmentModel]
class AuthorizationService: class AuthorizationService:
"""Determine whether a user has permission to perform their request.""" """Determine whether a user has permission to perform their request."""
@ -75,6 +114,7 @@ class AuthorizationService:
) -> bool: ) -> bool:
"""Has_permission.""" """Has_permission."""
principal_ids = [p.id for p in principals] principal_ids = [p.id for p in principals]
target_uri_normalized = target_uri.removeprefix(V1_API_PATH_PREFIX)
permission_assignments = ( permission_assignments = (
PermissionAssignmentModel.query.filter( PermissionAssignmentModel.query.filter(
@ -84,10 +124,13 @@ class AuthorizationService:
.join(PermissionTargetModel) .join(PermissionTargetModel)
.filter( .filter(
or_( or_(
text(f"'{target_uri}' LIKE permission_target.uri"), text(f"'{target_uri_normalized}' LIKE permission_target.uri"),
# to check for exact matches as well # to check for exact matches as well
# see test_user_can_access_base_path_when_given_wildcard_permission unit test # see test_user_can_access_base_path_when_given_wildcard_permission unit test
text(f"'{target_uri}' = replace(permission_target.uri, '/%', '')"), text(
f"'{target_uri_normalized}' ="
" replace(replace(permission_target.uri, '/%', ''), ':%', '')"
),
) )
) )
.all() .all()
@ -127,17 +170,15 @@ class AuthorizationService:
return cls.has_permission(principals, permission, target_uri) return cls.has_permission(principals, permission, target_uri)
@classmethod @classmethod
def delete_all_permissions_and_recreate(cls) -> None: def delete_all_permissions(cls) -> None:
"""Delete_all_permissions_and_recreate.""" """Delete_all_permissions_and_recreate. EXCEPT For permissions for the current user?"""
for model in [PermissionAssignmentModel, PermissionTargetModel]: for model in [PermissionAssignmentModel, PermissionTargetModel]:
db.session.query(model).delete() db.session.query(model).delete()
# cascading to principals doesn't seem to work when attempting to delete all so do it like this instead # cascading to principals doesn't seem to work when attempting to delete all so do it like this instead
for group in GroupModel.query.all(): for group in GroupModel.query.all():
db.session.delete(group) db.session.delete(group)
db.session.commit() db.session.commit()
cls.import_permissions_from_yaml_file()
@classmethod @classmethod
def associate_user_with_group(cls, user: UserModel, group: GroupModel) -> None: def associate_user_with_group(cls, user: UserModel, group: GroupModel) -> None:
@ -155,12 +196,13 @@ class AuthorizationService:
@classmethod @classmethod
def import_permissions_from_yaml_file( def import_permissions_from_yaml_file(
cls, raise_if_missing_user: bool = False cls, raise_if_missing_user: bool = False
) -> None: ) -> DesiredPermissionDict:
"""Import_permissions_from_yaml_file.""" """Import_permissions_from_yaml_file."""
if current_app.config["SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME"] is None: if current_app.config["SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME"] is None:
raise ( raise (
PermissionsFileNotSetError( PermissionsFileNotSetError(
"SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME needs to be set in order to import permissions" "SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME needs to be set in"
" order to import permissions"
) )
) )
@ -169,13 +211,16 @@ class AuthorizationService:
permission_configs = yaml.safe_load(file) permission_configs = yaml.safe_load(file)
default_group = None default_group = None
unique_user_group_identifiers: Set[str] = set()
if "default_group" in permission_configs: if "default_group" in permission_configs:
default_group_identifier = permission_configs["default_group"] default_group_identifier = permission_configs["default_group"]
default_group = GroupService.find_or_create_group(default_group_identifier) default_group = GroupService.find_or_create_group(default_group_identifier)
unique_user_group_identifiers.add(default_group_identifier)
if "groups" in permission_configs: if "groups" in permission_configs:
for group_identifier, group_config in permission_configs["groups"].items(): for group_identifier, group_config in permission_configs["groups"].items():
group = GroupService.find_or_create_group(group_identifier) group = GroupService.find_or_create_group(group_identifier)
unique_user_group_identifiers.add(group_identifier)
for username in group_config["users"]: for username in group_config["users"]:
user = UserModel.query.filter_by(username=username).first() user = UserModel.query.filter_by(username=username).first()
if user is None: if user is None:
@ -188,26 +233,25 @@ class AuthorizationService:
continue continue
cls.associate_user_with_group(user, group) cls.associate_user_with_group(user, group)
permission_assignments = []
if "permissions" in permission_configs: if "permissions" in permission_configs:
for _permission_identifier, permission_config in permission_configs[ for _permission_identifier, permission_config in permission_configs[
"permissions" "permissions"
].items(): ].items():
uri = permission_config["uri"] uri = permission_config["uri"]
uri_with_percent = re.sub(r"\*", "%", uri) permission_target = cls.find_or_create_permission_target(uri)
permission_target = PermissionTargetModel.query.filter_by(
uri=uri_with_percent
).first()
if permission_target is None:
permission_target = PermissionTargetModel(uri=uri_with_percent)
db.session.add(permission_target)
db.session.commit()
for allowed_permission in permission_config["allowed_permissions"]: for allowed_permission in permission_config["allowed_permissions"]:
if "groups" in permission_config: if "groups" in permission_config:
for group_identifier in permission_config["groups"]: for group_identifier in permission_config["groups"]:
group = GroupService.find_or_create_group(group_identifier) group = GroupService.find_or_create_group(group_identifier)
cls.create_permission_for_principal( unique_user_group_identifiers.add(group_identifier)
group.principal, permission_target, allowed_permission permission_assignments.append(
cls.create_permission_for_principal(
group.principal,
permission_target,
allowed_permission,
)
) )
if "users" in permission_config: if "users" in permission_config:
for username in permission_config["users"]: for username in permission_config["users"]:
@ -218,14 +262,35 @@ class AuthorizationService:
.filter(UserModel.username == username) .filter(UserModel.username == username)
.first() .first()
) )
cls.create_permission_for_principal( permission_assignments.append(
principal, permission_target, allowed_permission cls.create_permission_for_principal(
principal, permission_target, allowed_permission
)
) )
if default_group is not None: if default_group is not None:
for user in UserModel.query.all(): for user in UserModel.query.all():
cls.associate_user_with_group(user, default_group) cls.associate_user_with_group(user, default_group)
return {
"group_identifiers": unique_user_group_identifiers,
"permission_assignments": permission_assignments,
}
@classmethod
def find_or_create_permission_target(cls, uri: str) -> PermissionTargetModel:
"""Find_or_create_permission_target."""
uri_with_percent = re.sub(r"\*", "%", uri)
target_uri_normalized = uri_with_percent.removeprefix(V1_API_PATH_PREFIX)
permission_target: Optional[PermissionTargetModel] = (
PermissionTargetModel.query.filter_by(uri=target_uri_normalized).first()
)
if permission_target is None:
permission_target = PermissionTargetModel(uri=target_uri_normalized)
db.session.add(permission_target)
db.session.commit()
return permission_target
@classmethod @classmethod
def create_permission_for_principal( def create_permission_for_principal(
cls, cls,
@ -234,13 +299,13 @@ class AuthorizationService:
permission: str, permission: str,
) -> PermissionAssignmentModel: ) -> PermissionAssignmentModel:
"""Create_permission_for_principal.""" """Create_permission_for_principal."""
permission_assignment: Optional[ permission_assignment: Optional[PermissionAssignmentModel] = (
PermissionAssignmentModel PermissionAssignmentModel.query.filter_by(
] = PermissionAssignmentModel.query.filter_by( principal_id=principal.id,
principal_id=principal.id, permission_target_id=permission_target.id,
permission_target_id=permission_target.id, permission=permission,
permission=permission, ).first()
).first() )
if permission_assignment is None: if permission_assignment is None:
permission_assignment = PermissionAssignmentModel( permission_assignment = PermissionAssignmentModel(
principal_id=principal.id, principal_id=principal.id,
@ -340,7 +405,10 @@ class AuthorizationService:
raise ApiError( raise ApiError(
error_code="unauthorized", error_code="unauthorized",
message=f"User {g.user.username} is not authorized to perform requested action: {permission_string} - {request.path}", message=(
f"User {g.user.username} is not authorized to perform requested action:"
f" {permission_string} - {request.path}"
),
status_code=403, status_code=403,
) )
@ -419,7 +487,10 @@ class AuthorizationService:
except jwt.InvalidTokenError as exception: except jwt.InvalidTokenError as exception:
raise ApiError( raise ApiError(
"token_invalid", "token_invalid",
"The Authentication token you provided is invalid. You need a new token. ", (
"The Authentication token you provided is invalid. You need a new"
" token. "
),
) from exception ) from exception
@staticmethod @staticmethod
@ -441,41 +512,57 @@ class AuthorizationService:
if user not in human_task.potential_owners: if user not in human_task.potential_owners:
raise UserDoesNotHaveAccessToTaskError( raise UserDoesNotHaveAccessToTaskError(
f"User {user.username} does not have access to update task'{spiff_task.task_spec.name}'" f"User {user.username} does not have access to update"
f" for process instance '{process_instance_id}'" f" task'{spiff_task.task_spec.name}' for process instance"
f" '{process_instance_id}'"
) )
return True return True
@classmethod @classmethod
def create_user_from_sign_in(cls, user_info: dict) -> UserModel: def create_user_from_sign_in(cls, user_info: dict) -> UserModel:
"""Create_user_from_sign_in.""" """Create_user_from_sign_in."""
"""Name, family_name, given_name, middle_name, nickname, preferred_username,"""
"""Profile, picture, website, gender, birthdate, zoneinfo, locale, and updated_at. """
"""Email."""
is_new_user = False is_new_user = False
user_model = ( user_model = (
UserModel.query.filter(UserModel.service == "open_id") UserModel.query.filter(UserModel.service == user_info["iss"])
.filter(UserModel.service_id == user_info["sub"]) .filter(UserModel.service_id == user_info["sub"])
.first() .first()
) )
email = display_name = username = ""
if "email" in user_info:
username = user_info["email"]
email = user_info["email"]
else: # we fall back to the sub, which may be very ugly.
username = user_info["sub"] + "@" + user_info["iss"]
if "preferred_username" in user_info:
display_name = user_info["preferred_username"]
elif "nickname" in user_info:
display_name = user_info["nickname"]
elif "name" in user_info:
display_name = user_info["name"]
if user_model is None: if user_model is None:
current_app.logger.debug("create_user in login_return") current_app.logger.debug("create_user in login_return")
is_new_user = True is_new_user = True
name = username = email = ""
if "name" in user_info:
name = user_info["name"]
if "username" in user_info:
username = user_info["username"]
elif "preferred_username" in user_info:
username = user_info["preferred_username"]
if "email" in user_info:
email = user_info["email"]
user_model = UserService().create_user( user_model = UserService().create_user(
service="open_id",
service_id=user_info["sub"],
name=name,
username=username, username=username,
service=user_info["iss"],
service_id=user_info["sub"],
email=email, email=email,
display_name=display_name,
) )
else:
# Update with the latest information
user_model.username = username
user_model.email = email
user_model.display_name = display_name
user_model.service = user_info["iss"]
user_model.service_id = user_info["sub"]
# this may eventually get too slow. # this may eventually get too slow.
# when it does, be careful about backgrounding, because # when it does, be careful about backgrounding, because
# the user will immediately need permissions to use the site. # the user will immediately need permissions to use the site.
@ -490,6 +577,224 @@ class AuthorizationService:
# this cannot be None so ignore mypy # this cannot be None so ignore mypy
return user_model # type: ignore return user_model # type: ignore
@classmethod
def get_permissions_to_assign(
cls,
permission_set: str,
process_related_path_segment: str,
target_uris: list[str],
) -> list[PermissionToAssign]:
"""Get_permissions_to_assign."""
permissions = permission_set.split(",")
if permission_set == "all":
permissions = ["create", "read", "update", "delete"]
permissions_to_assign: list[PermissionToAssign] = []
# we were thinking that if you can start an instance, you ought to be able to view your own instances.
if permission_set == "start":
target_uri = f"/process-instances/{process_related_path_segment}"
permissions_to_assign.append(
PermissionToAssign(permission="create", target_uri=target_uri)
)
target_uri = f"/process-instances/for-me/{process_related_path_segment}"
permissions_to_assign.append(
PermissionToAssign(permission="read", target_uri=target_uri)
)
else:
if permission_set == "all":
for path_segment_dict in PATH_SEGMENTS_FOR_PERMISSION_ALL:
target_uri = (
f"{path_segment_dict['path']}/{process_related_path_segment}"
)
relevant_permissions = path_segment_dict["relevant_permissions"]
for permission in relevant_permissions:
permissions_to_assign.append(
PermissionToAssign(
permission=permission, target_uri=target_uri
)
)
for target_uri in target_uris:
for permission in permissions:
permissions_to_assign.append(
PermissionToAssign(permission=permission, target_uri=target_uri)
)
return permissions_to_assign
@classmethod
def explode_permissions(
cls, permission_set: str, target: str
) -> list[PermissionToAssign]:
"""Explodes given permissions to and returns list of PermissionToAssign objects.
These can be used to then iterate through and inserted into the database.
Target Macros:
ALL
* gives access to ALL api endpoints - useful to give admin-like permissions
PG:[process_group_identifier]
* affects given process-group and all sub process-groups and process-models
PM:[process_model_identifier]
* affects given process-model
BASIC
* Basic access to complete tasks and use the site
Permission Macros:
all
* create, read, update, delete
start
* create process-instances (aka instantiate or start a process-model)
* only works with PG and PM target macros
"""
permissions_to_assign: list[PermissionToAssign] = []
permissions = permission_set.split(",")
if permission_set == "all":
permissions = ["create", "read", "update", "delete"]
if target.startswith("PG:"):
process_group_identifier = (
target.removeprefix("PG:").replace("/", ":").removeprefix(":")
)
process_related_path_segment = f"{process_group_identifier}:*"
if process_group_identifier == "ALL":
process_related_path_segment = "*"
target_uris = [
f"/process-groups/{process_related_path_segment}",
f"/process-models/{process_related_path_segment}",
]
permissions_to_assign = (
permissions_to_assign
+ cls.get_permissions_to_assign(
permission_set, process_related_path_segment, target_uris
)
)
elif target.startswith("PM:"):
process_model_identifier = (
target.removeprefix("PM:").replace("/", ":").removeprefix(":")
)
process_related_path_segment = f"{process_model_identifier}/*"
if process_model_identifier == "ALL":
process_related_path_segment = "*"
target_uris = [f"/process-models/{process_related_path_segment}"]
permissions_to_assign = (
permissions_to_assign
+ cls.get_permissions_to_assign(
permission_set, process_related_path_segment, target_uris
)
)
elif permission_set == "start":
raise InvalidPermissionError(
"Permission 'start' is only available for macros PM and PG."
)
elif target.startswith("BASIC"):
permissions_to_assign.append(
PermissionToAssign(
permission="read", target_uri="/process-instances/for-me"
)
)
permissions_to_assign.append(
PermissionToAssign(permission="read", target_uri="/processes")
)
permissions_to_assign.append(
PermissionToAssign(permission="read", target_uri="/service-tasks")
)
permissions_to_assign.append(
PermissionToAssign(
permission="read", target_uri="/user-groups/for-current-user"
)
)
for permission in ["create", "read", "update", "delete"]:
permissions_to_assign.append(
PermissionToAssign(
permission=permission, target_uri="/process-instances/reports/*"
)
)
permissions_to_assign.append(
PermissionToAssign(permission=permission, target_uri="/tasks/*")
)
elif target == "ALL":
for permission in permissions:
permissions_to_assign.append(
PermissionToAssign(permission=permission, target_uri="/*")
)
elif target.startswith("/"):
for permission in permissions:
permissions_to_assign.append(
PermissionToAssign(permission=permission, target_uri=target)
)
else:
raise InvalidPermissionError(
f"Target uri '{target}' with permission set '{permission_set}' is"
" invalid. The target uri must either be a macro of PG, PM, BASIC, or"
" ALL or an api uri."
)
return permissions_to_assign
@classmethod
def add_permission_from_uri_or_macro(
cls, group_identifier: str, permission: str, target: str
) -> list[PermissionAssignmentModel]:
"""Add_permission_from_uri_or_macro."""
group = GroupService.find_or_create_group(group_identifier)
permissions_to_assign = cls.explode_permissions(permission, target)
permission_assignments = []
for permission_to_assign in permissions_to_assign:
permission_target = cls.find_or_create_permission_target(
permission_to_assign.target_uri
)
permission_assignments.append(
cls.create_permission_for_principal(
group.principal, permission_target, permission_to_assign.permission
)
)
return permission_assignments
@classmethod
def refresh_permissions(cls, group_info: list[dict[str, Any]]) -> None:
"""Adds new permission assignments and deletes old ones."""
initial_permission_assignments = PermissionAssignmentModel.query.all()
result = cls.import_permissions_from_yaml_file()
desired_permission_assignments = result["permission_assignments"]
desired_group_identifiers = result["group_identifiers"]
for group in group_info:
group_identifier = group["name"]
for username in group["users"]:
GroupService.add_user_to_group_or_add_to_waiting(
username, group_identifier
)
desired_group_identifiers.add(group_identifier)
for permission in group["permissions"]:
for crud_op in permission["actions"]:
desired_permission_assignments.extend(
cls.add_permission_from_uri_or_macro(
group_identifier=group_identifier,
target=permission["uri"],
permission=crud_op,
)
)
desired_group_identifiers.add(group_identifier)
for ipa in initial_permission_assignments:
if ipa not in desired_permission_assignments:
db.session.delete(ipa)
groups_to_delete = GroupModel.query.filter(
GroupModel.identifier.not_in(desired_group_identifiers)
).all()
for gtd in groups_to_delete:
db.session.delete(gtd)
db.session.commit()
class KeycloakAuthorization: class KeycloakAuthorization:
"""Interface with Keycloak server.""" """Interface with Keycloak server."""

View File

@ -40,10 +40,9 @@ class FileSystemService:
@staticmethod @staticmethod
def root_path() -> str: def root_path() -> str:
"""Root_path.""" """Root_path."""
# fixme: allow absolute files
dir_name = current_app.config["BPMN_SPEC_ABSOLUTE_DIR"] dir_name = current_app.config["BPMN_SPEC_ABSOLUTE_DIR"]
app_root = current_app.root_path # ensure this is a string - thanks mypy...
return os.path.abspath(os.path.join(app_root, "..", dir_name)) return os.path.abspath(os.path.join(dir_name, ""))
@staticmethod @staticmethod
def id_string_to_relative_path(id_string: str) -> str: def id_string_to_relative_path(id_string: str) -> str:

View File

@ -173,13 +173,15 @@ class GitService:
if "repository" not in webhook or "clone_url" not in webhook["repository"]: if "repository" not in webhook or "clone_url" not in webhook["repository"]:
raise InvalidGitWebhookBodyError( raise InvalidGitWebhookBodyError(
f"Cannot find required keys of 'repository:clone_url' from webhook body: {webhook}" "Cannot find required keys of 'repository:clone_url' from webhook"
f" body: {webhook}"
) )
clone_url = webhook["repository"]["clone_url"] clone_url = webhook["repository"]["clone_url"]
if clone_url != current_app.config["GIT_CLONE_URL_FOR_PUBLISHING"]: if clone_url != current_app.config["GIT_CLONE_URL_FOR_PUBLISHING"]:
raise GitCloneUrlMismatchError( raise GitCloneUrlMismatchError(
f"Configured clone url does not match clone url from webhook: {clone_url}" "Configured clone url does not match clone url from webhook:"
f" {clone_url}"
) )
if "ref" not in webhook: if "ref" not in webhook:
@ -189,8 +191,8 @@ class GitService:
if current_app.config["GIT_BRANCH"] is None: if current_app.config["GIT_BRANCH"] is None:
raise MissingGitConfigsError( raise MissingGitConfigsError(
"Missing config for GIT_BRANCH. " "Missing config for GIT_BRANCH. This is required for updating the"
"This is required for updating the repository as a result of the webhook" " repository as a result of the webhook"
) )
ref = webhook["ref"] ref = webhook["ref"]

View File

@ -4,6 +4,7 @@ from typing import Optional
from flask_bpmn.models.db import db from flask_bpmn.models.db import db
from spiffworkflow_backend.models.group import GroupModel from spiffworkflow_backend.models.group import GroupModel
from spiffworkflow_backend.models.user import UserModel
from spiffworkflow_backend.services.user_service import UserService from spiffworkflow_backend.services.user_service import UserService
@ -22,3 +23,15 @@ class GroupService:
db.session.commit() db.session.commit()
UserService.create_principal(group.id, id_column_name="group_id") UserService.create_principal(group.id, id_column_name="group_id")
return group return group
@classmethod
def add_user_to_group_or_add_to_waiting(
cls, username: str, group_identifier: str
) -> None:
"""Add_user_to_group_or_add_to_waiting."""
group = cls.find_or_create_group(group_identifier)
user = UserModel.query.filter_by(username=username).first()
if user:
UserService.add_user_to_group(user, group)
else:
UserService.add_waiting_group_assignment(username, group)

View File

@ -122,7 +122,8 @@ def setup_logger(app: Flask) -> None:
if upper_log_level_string not in log_levels: if upper_log_level_string not in log_levels:
raise InvalidLogLevelError( raise InvalidLogLevelError(
f"Log level given is invalid: '{upper_log_level_string}'. Valid options are {log_levels}" f"Log level given is invalid: '{upper_log_level_string}'. Valid options are"
f" {log_levels}"
) )
log_level = getattr(logging, upper_log_level_string) log_level = getattr(logging, upper_log_level_string)
@ -176,7 +177,8 @@ def setup_logger(app: Flask) -> None:
spiff_logger = logging.getLogger("spiff") spiff_logger = logging.getLogger("spiff")
spiff_logger.setLevel(spiff_log_level) spiff_logger.setLevel(spiff_log_level)
spiff_formatter = logging.Formatter( spiff_formatter = logging.Formatter(
"%(asctime)s | %(levelname)s | %(message)s | %(action)s | %(task_type)s | %(process)s | %(processName)s | %(process_instance_id)s" "%(asctime)s | %(levelname)s | %(message)s | %(action)s | %(task_type)s |"
" %(process)s | %(processName)s | %(process_instance_id)s"
) )
# if you add a handler to spiff, it will be used/inherited by spiff.metrics # if you add a handler to spiff, it will be used/inherited by spiff.metrics

View File

@ -145,8 +145,11 @@ class MessageService:
if process_instance_receive is None: if process_instance_receive is None:
raise MessageServiceError( raise MessageServiceError(
( (
f"Process instance cannot be found for queued message: {message_instance_receive.id}." (
f"Tried with id {message_instance_receive.process_instance_id}", "Process instance cannot be found for queued message:"
f" {message_instance_receive.id}.Tried with id"
f" {message_instance_receive.process_instance_id}"
),
) )
) )
@ -182,7 +185,6 @@ class MessageService:
) )
for message_instance_receive in message_instances_receive: for message_instance_receive in message_instances_receive:
# sqlalchemy supports select / where statements like active record apparantly # sqlalchemy supports select / where statements like active record apparantly
# https://docs.sqlalchemy.org/en/14/core/tutorial.html#conjunctions # https://docs.sqlalchemy.org/en/14/core/tutorial.html#conjunctions
message_correlation_select = ( message_correlation_select = (

View File

@ -17,6 +17,7 @@ from typing import Optional
from typing import Tuple from typing import Tuple
from typing import TypedDict from typing import TypedDict
from typing import Union from typing import Union
from uuid import UUID
import dateparser import dateparser
import pytz import pytz
@ -43,6 +44,9 @@ from SpiffWorkflow.spiff.serializer.task_spec_converters import (
CallActivityTaskConverter, CallActivityTaskConverter,
) )
from SpiffWorkflow.spiff.serializer.task_spec_converters import EndEventConverter from SpiffWorkflow.spiff.serializer.task_spec_converters import EndEventConverter
from SpiffWorkflow.spiff.serializer.task_spec_converters import (
EventBasedGatewayConverter,
)
from SpiffWorkflow.spiff.serializer.task_spec_converters import ( from SpiffWorkflow.spiff.serializer.task_spec_converters import (
IntermediateCatchEventConverter, IntermediateCatchEventConverter,
) )
@ -151,6 +155,9 @@ class CustomBpmnScriptEngine(PythonScriptEngine): # type: ignore
"time": time, "time": time,
"decimal": decimal, "decimal": decimal,
"_strptime": _strptime, "_strptime": _strptime,
"enumerate": enumerate,
"list": list,
"map": map,
} }
# This will overwrite the standard builtins # This will overwrite the standard builtins
@ -209,14 +216,14 @@ class CustomBpmnScriptEngine(PythonScriptEngine): # type: ignore
except Exception as exception: except Exception as exception:
if task is None: if task is None:
raise ProcessInstanceProcessorError( raise ProcessInstanceProcessorError(
"Error evaluating expression: " "Error evaluating expression: '%s', exception: %s"
"'%s', exception: %s" % (expression, str(exception)), % (expression, str(exception)),
) from exception ) from exception
else: else:
raise WorkflowTaskExecException( raise WorkflowTaskExecException(
task, task,
"Error evaluating expression " "Error evaluating expression '%s', %s"
"'%s', %s" % (expression, str(exception)), % (expression, str(exception)),
) from exception ) from exception
def execute( def execute(
@ -263,6 +270,7 @@ class ProcessInstanceProcessor:
EndEventConverter, EndEventConverter,
IntermediateCatchEventConverter, IntermediateCatchEventConverter,
IntermediateThrowEventConverter, IntermediateThrowEventConverter,
EventBasedGatewayConverter,
ManualTaskConverter, ManualTaskConverter,
NoneTaskConverter, NoneTaskConverter,
ReceiveTaskConverter, ReceiveTaskConverter,
@ -276,6 +284,7 @@ class ProcessInstanceProcessor:
] ]
) )
_serializer = BpmnWorkflowSerializer(wf_spec_converter, version=SERIALIZER_VERSION) _serializer = BpmnWorkflowSerializer(wf_spec_converter, version=SERIALIZER_VERSION)
_event_serializer = EventBasedGatewayConverter()
PROCESS_INSTANCE_ID_KEY = "process_instance_id" PROCESS_INSTANCE_ID_KEY = "process_instance_id"
VALIDATION_PROCESS_KEY = "validate_only" VALIDATION_PROCESS_KEY = "validate_only"
@ -292,9 +301,7 @@ class ProcessInstanceProcessor:
tld.spiff_step = process_instance_model.spiff_step tld.spiff_step = process_instance_model.spiff_step
# we want this to be the fully qualified path to the process model including all group subcomponents # we want this to be the fully qualified path to the process model including all group subcomponents
current_app.config[ current_app.config["THREAD_LOCAL_DATA"].process_model_identifier = (
"THREAD_LOCAL_DATA"
].process_model_identifier = (
f"{process_instance_model.process_model_identifier}" f"{process_instance_model.process_model_identifier}"
) )
@ -375,8 +382,10 @@ class ProcessInstanceProcessor:
except MissingSpecError as ke: except MissingSpecError as ke:
raise ApiError( raise ApiError(
error_code="unexpected_process_instance_structure", error_code="unexpected_process_instance_structure",
message="Failed to deserialize process_instance" message=(
" '%s' due to a mis-placed or missing task '%s'" "Failed to deserialize process_instance"
" '%s' due to a mis-placed or missing task '%s'"
)
% (self.process_model_identifier, str(ke)), % (self.process_model_identifier, str(ke)),
) from ke ) from ke
@ -392,7 +401,10 @@ class ProcessInstanceProcessor:
raise ( raise (
ApiError( ApiError(
"process_model_not_found", "process_model_not_found",
f"The given process model was not found: {process_model_identifier}.", (
"The given process model was not found:"
f" {process_model_identifier}."
),
) )
) )
spec_files = SpecFileService.get_files(process_model_info) spec_files = SpecFileService.get_files(process_model_info)
@ -522,8 +534,11 @@ class ProcessInstanceProcessor:
potential_owner_ids.append(lane_owner_user.id) potential_owner_ids.append(lane_owner_user.id)
self.raise_if_no_potential_owners( self.raise_if_no_potential_owners(
potential_owner_ids, potential_owner_ids,
f"No users found in task data lane owner list for lane: {task_lane}. " (
f"The user list used: {task.data['lane_owners'][task_lane]}", "No users found in task data lane owner list for lane:"
f" {task_lane}. The user list used:"
f" {task.data['lane_owners'][task_lane]}"
),
) )
else: else:
group_model = GroupModel.query.filter_by(identifier=task_lane).first() group_model = GroupModel.query.filter_by(identifier=task_lane).first()
@ -573,12 +588,6 @@ class ProcessInstanceProcessor:
) )
return details_model return details_model
def save_spiff_step_details(self) -> None:
"""SaveSpiffStepDetails."""
details_model = self.spiff_step_details()
db.session.add(details_model)
db.session.commit()
def extract_metadata(self, process_model_info: ProcessModelInfo) -> None: def extract_metadata(self, process_model_info: ProcessModelInfo) -> None:
"""Extract_metadata.""" """Extract_metadata."""
metadata_extraction_paths = process_model_info.metadata_extraction_paths metadata_extraction_paths = process_model_info.metadata_extraction_paths
@ -614,7 +623,7 @@ class ProcessInstanceProcessor:
db.session.add(pim) db.session.add(pim)
db.session.commit() db.session.commit()
def save(self) -> None: def _save(self) -> None:
"""Saves the current state of this processor to the database.""" """Saves the current state of this processor to the database."""
self.process_instance_model.bpmn_json = self.serialize() self.process_instance_model.bpmn_json = self.serialize()
@ -636,6 +645,9 @@ class ProcessInstanceProcessor:
db.session.add(self.process_instance_model) db.session.add(self.process_instance_model)
db.session.commit() db.session.commit()
def save(self) -> None:
"""Saves the current state and moves on to the next state."""
self._save()
human_tasks = HumanTaskModel.query.filter_by( human_tasks = HumanTaskModel.query.filter_by(
process_instance_id=self.process_instance_model.id process_instance_id=self.process_instance_model.id
).all() ).all()
@ -704,6 +716,47 @@ class ProcessInstanceProcessor:
db.session.add(at) db.session.add(at)
db.session.commit() db.session.commit()
def serialize_task_spec(self, task_spec: SpiffTask) -> Any:
"""Get a serialized version of a task spec."""
# The task spec is NOT actually a SpiffTask, it is the task spec attached to a SpiffTask
# Not sure why mypy accepts this but whatever.
return self._serializer.spec_converter.convert(task_spec)
def send_bpmn_event(self, event_data: dict[str, Any]) -> None:
"""Send an event to the workflow."""
payload = event_data.pop("payload", None)
event_definition = self._event_serializer.restore(event_data)
if payload is not None:
event_definition.payload = payload
current_app.logger.info(
f"Event of type {event_definition.event_type} sent to process instance"
f" {self.process_instance_model.id}"
)
self.bpmn_process_instance.catch(event_definition)
self.do_engine_steps(save=True)
def manual_complete_task(self, task_id: str, execute: bool) -> None:
"""Mark the task complete optionally executing it."""
spiff_task = self.bpmn_process_instance.get_task(UUID(task_id))
if execute:
current_app.logger.info(
f"Manually executing Task {spiff_task.task_spec.name} of process"
f" instance {self.process_instance_model.id}"
)
spiff_task.complete()
else:
current_app.logger.info(
f"Skipping Task {spiff_task.task_spec.name} of process instance"
f" {self.process_instance_model.id}"
)
spiff_task._set_state(TaskState.COMPLETED)
for child in spiff_task.children:
child.task_spec._update(child)
self.bpmn_process_instance.last_task = spiff_task
self._save()
# Saving the workflow seems to reset the status
self.suspend()
@staticmethod @staticmethod
def get_parser() -> MyCustomParser: def get_parser() -> MyCustomParser:
"""Get_parser.""" """Get_parser."""
@ -738,14 +791,13 @@ class ProcessInstanceProcessor:
"""Bpmn_file_full_path_from_bpmn_process_identifier.""" """Bpmn_file_full_path_from_bpmn_process_identifier."""
if bpmn_process_identifier is None: if bpmn_process_identifier is None:
raise ValueError( raise ValueError(
"bpmn_file_full_path_from_bpmn_process_identifier: bpmn_process_identifier is unexpectedly None" "bpmn_file_full_path_from_bpmn_process_identifier:"
" bpmn_process_identifier is unexpectedly None"
) )
spec_reference = ( spec_reference = SpecReferenceCache.query.filter_by(
SpecReferenceCache.query.filter_by(identifier=bpmn_process_identifier) identifier=bpmn_process_identifier, type="process"
.filter_by(type="process") ).first()
.first()
)
bpmn_file_full_path = None bpmn_file_full_path = None
if spec_reference is None: if spec_reference is None:
bpmn_file_full_path = ( bpmn_file_full_path = (
@ -762,7 +814,10 @@ class ProcessInstanceProcessor:
raise ( raise (
ApiError( ApiError(
error_code="could_not_find_bpmn_process_identifier", error_code="could_not_find_bpmn_process_identifier",
message="Could not find the the given bpmn process identifier from any sources: %s" message=(
"Could not find the the given bpmn process identifier from any"
" sources: %s"
)
% bpmn_process_identifier, % bpmn_process_identifier,
) )
) )
@ -786,7 +841,6 @@ class ProcessInstanceProcessor:
new_bpmn_files = set() new_bpmn_files = set()
for bpmn_process_identifier in processor_dependencies_new: for bpmn_process_identifier in processor_dependencies_new:
# ignore identifiers that spiff already knows about # ignore identifiers that spiff already knows about
if bpmn_process_identifier in bpmn_process_identifiers_in_parser: if bpmn_process_identifier in bpmn_process_identifiers_in_parser:
continue continue
@ -829,7 +883,10 @@ class ProcessInstanceProcessor:
raise ( raise (
ApiError( ApiError(
error_code="no_primary_bpmn_error", error_code="no_primary_bpmn_error",
message="There is no primary BPMN process id defined for process_model %s" message=(
"There is no primary BPMN process id defined for"
" process_model %s"
)
% process_model_info.id, % process_model_info.id,
) )
) )
@ -890,7 +947,10 @@ class ProcessInstanceProcessor:
if not bpmn_message.correlations: if not bpmn_message.correlations:
raise ApiError( raise ApiError(
"message_correlations_missing", "message_correlations_missing",
f"Could not find any message correlations bpmn_message: {bpmn_message.name}", (
"Could not find any message correlations bpmn_message:"
f" {bpmn_message.name}"
),
) )
message_correlations = [] message_correlations = []
@ -910,12 +970,16 @@ class ProcessInstanceProcessor:
if message_correlation_property is None: if message_correlation_property is None:
raise ApiError( raise ApiError(
"message_correlations_missing_from_process", "message_correlations_missing_from_process",
"Could not find a known message correlation with identifier:" (
f"{message_correlation_property_identifier}", "Could not find a known message correlation with"
f" identifier:{message_correlation_property_identifier}"
),
) )
message_correlations.append( message_correlations.append(
{ {
"message_correlation_property": message_correlation_property, "message_correlation_property": (
message_correlation_property
),
"name": message_correlation_key, "name": message_correlation_key,
"value": message_correlation_property_value, "value": message_correlation_property_value,
} }
@ -972,7 +1036,10 @@ class ProcessInstanceProcessor:
if message_model is None: if message_model is None:
raise ApiError( raise ApiError(
"invalid_message_name", "invalid_message_name",
f"Invalid message name: {waiting_task.task_spec.event_definition.name}.", (
"Invalid message name:"
f" {waiting_task.task_spec.event_definition.name}."
),
) )
# Ensure we are only creating one message instance for each waiting message # Ensure we are only creating one message instance for each waiting message
@ -1186,9 +1253,13 @@ class ProcessInstanceProcessor:
self.increment_spiff_step() self.increment_spiff_step()
self.bpmn_process_instance.complete_task_from_id(task.id) self.bpmn_process_instance.complete_task_from_id(task.id)
human_task.completed_by_user_id = user.id human_task.completed_by_user_id = user.id
human_task.completed = True
db.session.add(human_task) db.session.add(human_task)
db.session.commit() details_model = self.spiff_step_details()
self.save_spiff_step_details() db.session.add(details_model)
# this is the thing that actually commits the db transaction (on behalf of the other updates above as well)
self.save()
def get_data(self) -> dict[str, Any]: def get_data(self) -> dict[str, Any]:
"""Get_data.""" """Get_data."""

View File

@ -1,6 +1,7 @@
"""Process_instance_report_service.""" """Process_instance_report_service."""
import re import re
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any
from typing import Optional from typing import Optional
import sqlalchemy import sqlalchemy
@ -84,29 +85,8 @@ class ProcessInstanceReportService:
"""ProcessInstanceReportService.""" """ProcessInstanceReportService."""
@classmethod @classmethod
def report_with_identifier( def system_metadata_map(cls, metadata_key: str) -> dict[str, Any]:
cls, """System_metadata_map."""
user: UserModel,
report_id: Optional[int] = None,
report_identifier: Optional[str] = None,
) -> ProcessInstanceReportModel:
"""Report_with_filter."""
if report_id is not None:
process_instance_report = ProcessInstanceReportModel.query.filter_by(
id=report_id, created_by_id=user.id
).first()
if process_instance_report is not None:
return process_instance_report # type: ignore
if report_identifier is None:
report_identifier = "default"
process_instance_report = ProcessInstanceReportModel.query.filter_by(
identifier=report_identifier, created_by_id=user.id
).first()
if process_instance_report is not None:
return process_instance_report # type: ignore
# TODO replace with system reports that are loaded on launch (or similar) # TODO replace with system reports that are loaded on launch (or similar)
temp_system_metadata_map = { temp_system_metadata_map = {
"default": { "default": {
@ -151,10 +131,36 @@ class ProcessInstanceReportService:
"order_by": ["-start_in_seconds", "-id"], "order_by": ["-start_in_seconds", "-id"],
}, },
} }
return temp_system_metadata_map[metadata_key]
@classmethod
def report_with_identifier(
cls,
user: UserModel,
report_id: Optional[int] = None,
report_identifier: Optional[str] = None,
) -> ProcessInstanceReportModel:
"""Report_with_filter."""
if report_id is not None:
process_instance_report = ProcessInstanceReportModel.query.filter_by(
id=report_id, created_by_id=user.id
).first()
if process_instance_report is not None:
return process_instance_report # type: ignore
if report_identifier is None:
report_identifier = "default"
process_instance_report = ProcessInstanceReportModel.query.filter_by(
identifier=report_identifier, created_by_id=user.id
).first()
if process_instance_report is not None:
return process_instance_report # type: ignore
process_instance_report = ProcessInstanceReportModel( process_instance_report = ProcessInstanceReportModel(
identifier=report_identifier, identifier=report_identifier,
created_by_id=user.id, created_by_id=user.id,
report_metadata=temp_system_metadata_map[report_identifier], report_metadata=cls.system_metadata_map(report_identifier),
) )
return process_instance_report # type: ignore return process_instance_report # type: ignore
@ -283,9 +289,9 @@ class ProcessInstanceReportService:
process_instance_dict = process_instance["ProcessInstanceModel"].serialized process_instance_dict = process_instance["ProcessInstanceModel"].serialized
for metadata_column in metadata_columns: for metadata_column in metadata_columns:
if metadata_column["accessor"] not in process_instance_dict: if metadata_column["accessor"] not in process_instance_dict:
process_instance_dict[ process_instance_dict[metadata_column["accessor"]] = (
metadata_column["accessor"] process_instance[metadata_column["accessor"]]
] = process_instance[metadata_column["accessor"]] )
results.append(process_instance_dict) results.append(process_instance_dict)
return results return results
@ -414,13 +420,16 @@ class ProcessInstanceReportService:
) )
if report_filter.with_tasks_assigned_to_my_group is True: if report_filter.with_tasks_assigned_to_my_group is True:
group_model_join_conditions = [GroupModel.id == HumanTaskModel.lane_assignment_id] group_model_join_conditions = [
GroupModel.id == HumanTaskModel.lane_assignment_id
]
if report_filter.user_group_identifier: if report_filter.user_group_identifier:
group_model_join_conditions.append(GroupModel.identifier == report_filter.user_group_identifier) group_model_join_conditions.append(
GroupModel.identifier == report_filter.user_group_identifier
)
process_instance_query = process_instance_query.join(HumanTaskModel) process_instance_query = process_instance_query.join(HumanTaskModel)
process_instance_query = process_instance_query.join( process_instance_query = process_instance_query.join(
GroupModel, GroupModel, and_(*group_model_join_conditions)
and_(*group_model_join_conditions)
) )
process_instance_query = process_instance_query.join( process_instance_query = process_instance_query.join(
UserGroupAssignmentModel, UserGroupAssignmentModel,

View File

@ -17,6 +17,7 @@ from spiffworkflow_backend.models.task import MultiInstanceType
from spiffworkflow_backend.models.task import Task from spiffworkflow_backend.models.task import Task
from spiffworkflow_backend.models.user import UserModel from spiffworkflow_backend.models.user import UserModel
from spiffworkflow_backend.services.authorization_service import AuthorizationService from spiffworkflow_backend.services.authorization_service import AuthorizationService
from spiffworkflow_backend.services.git_service import GitCommandError
from spiffworkflow_backend.services.git_service import GitService from spiffworkflow_backend.services.git_service import GitService
from spiffworkflow_backend.services.process_instance_processor import ( from spiffworkflow_backend.services.process_instance_processor import (
ProcessInstanceProcessor, ProcessInstanceProcessor,
@ -36,7 +37,10 @@ class ProcessInstanceService:
user: UserModel, user: UserModel,
) -> ProcessInstanceModel: ) -> ProcessInstanceModel:
"""Get_process_instance_from_spec.""" """Get_process_instance_from_spec."""
current_git_revision = GitService.get_current_revision() try:
current_git_revision = GitService.get_current_revision()
except GitCommandError:
current_git_revision = ""
process_instance_model = ProcessInstanceModel( process_instance_model = ProcessInstanceModel(
status=ProcessInstanceStatus.not_started.value, status=ProcessInstanceStatus.not_started.value,
process_initiator=user, process_initiator=user,
@ -81,7 +85,8 @@ class ProcessInstanceService:
db.session.add(process_instance) db.session.add(process_instance)
db.session.commit() db.session.commit()
error_message = ( error_message = (
f"Error running waiting task for process_instance {process_instance.id}" "Error running waiting task for process_instance"
f" {process_instance.id}"
+ f"({process_instance.process_model_identifier}). {str(e)}" + f"({process_instance.process_model_identifier}). {str(e)}"
) )
current_app.logger.error(error_message) current_app.logger.error(error_message)
@ -121,7 +126,7 @@ class ProcessInstanceService:
if next_task_trying_again is not None: if next_task_trying_again is not None:
process_instance_api.next_task = ( process_instance_api.next_task = (
ProcessInstanceService.spiff_task_to_api_task( ProcessInstanceService.spiff_task_to_api_task(
next_task_trying_again, add_docs_and_forms=True processor, next_task_trying_again, add_docs_and_forms=True
) )
) )
@ -174,7 +179,10 @@ class ProcessInstanceService:
else: else:
raise ApiError.from_task( raise ApiError.from_task(
error_code="task_lane_user_error", error_code="task_lane_user_error",
message="Spiff Task %s lane user dict must have a key called 'value' with the user's uid in it." message=(
"Spiff Task %s lane user dict must have a key called"
" 'value' with the user's uid in it."
)
% spiff_task.task_spec.name, % spiff_task.task_spec.name,
task=spiff_task, task=spiff_task,
) )
@ -277,7 +285,9 @@ class ProcessInstanceService:
@staticmethod @staticmethod
def spiff_task_to_api_task( def spiff_task_to_api_task(
spiff_task: SpiffTask, add_docs_and_forms: bool = False processor: ProcessInstanceProcessor,
spiff_task: SpiffTask,
add_docs_and_forms: bool = False,
) -> Task: ) -> Task:
"""Spiff_task_to_api_task.""" """Spiff_task_to_api_task."""
task_type = spiff_task.task_spec.spec_type task_type = spiff_task.task_spec.spec_type
@ -311,6 +321,8 @@ class ProcessInstanceService:
if spiff_task.parent: if spiff_task.parent:
parent_id = spiff_task.parent.id parent_id = spiff_task.parent.id
serialized_task_spec = processor.serialize_task_spec(spiff_task.task_spec)
task = Task( task = Task(
spiff_task.id, spiff_task.id,
spiff_task.task_spec.name, spiff_task.task_spec.name,
@ -324,6 +336,7 @@ class ProcessInstanceService:
process_identifier=spiff_task.task_spec._wf_spec.name, process_identifier=spiff_task.task_spec._wf_spec.name,
properties=props, properties=props,
parent=parent_id, parent=parent_id,
event_definition=serialized_task_spec.get("event_definition"),
call_activity_process_identifier=call_activity_process_identifier, call_activity_process_identifier=call_activity_process_identifier,
) )

View File

@ -13,6 +13,8 @@ from flask_bpmn.api.api_error import ApiError
from spiffworkflow_backend.exceptions.process_entity_not_found_error import ( from spiffworkflow_backend.exceptions.process_entity_not_found_error import (
ProcessEntityNotFoundError, ProcessEntityNotFoundError,
) )
from spiffworkflow_backend.interfaces import ProcessGroupLite
from spiffworkflow_backend.interfaces import ProcessGroupLitesWithCache
from spiffworkflow_backend.models.process_group import ProcessGroup from spiffworkflow_backend.models.process_group import ProcessGroup
from spiffworkflow_backend.models.process_group import ProcessGroupSchema from spiffworkflow_backend.models.process_group import ProcessGroupSchema
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
@ -146,7 +148,10 @@ class ProcessModelService(FileSystemService):
if len(instances) > 0: if len(instances) > 0:
raise ApiError( raise ApiError(
error_code="existing_instances", error_code="existing_instances",
message=f"We cannot delete the model `{process_model_id}`, there are existing instances that depend on it.", message=(
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) process_model = self.get_process_model(process_model_id)
path = self.workflow_path(process_model) path = self.workflow_path(process_model)
@ -224,31 +229,46 @@ class ProcessModelService(FileSystemService):
new_process_model_list = [] new_process_model_list = []
for process_model in process_models: for process_model in process_models:
uri = f"/v1.0/process-instances/{process_model.id.replace('/', ':')}" uri = f"/v1.0/process-instances/{process_model.id.replace('/', ':')}"
result = AuthorizationService.user_has_permission( has_permission = AuthorizationService.user_has_permission(
user=user, permission="create", target_uri=uri user=user, permission="create", target_uri=uri
) )
if result: if has_permission:
new_process_model_list.append(process_model) new_process_model_list.append(process_model)
return new_process_model_list return new_process_model_list
return process_models return process_models
@classmethod @classmethod
def get_parent_group_array(cls, process_identifier: str) -> list[dict]: def get_parent_group_array_and_cache_it(
cls, process_identifier: str, process_group_cache: dict[str, ProcessGroup]
) -> ProcessGroupLitesWithCache:
"""Get_parent_group_array.""" """Get_parent_group_array."""
full_group_id_path = None full_group_id_path = None
parent_group_array = [] parent_group_array: list[ProcessGroupLite] = []
for process_group_id_segment in process_identifier.split("/")[0:-1]: for process_group_id_segment in process_identifier.split("/")[0:-1]:
if full_group_id_path is None: if full_group_id_path is None:
full_group_id_path = process_group_id_segment full_group_id_path = process_group_id_segment
else: else:
full_group_id_path = os.path.join(full_group_id_path, process_group_id_segment) # type: ignore full_group_id_path = os.path.join(full_group_id_path, process_group_id_segment) # type: ignore
parent_group = ProcessModelService.get_process_group(full_group_id_path) parent_group = process_group_cache.get(full_group_id_path, None)
if parent_group is None:
parent_group = ProcessModelService.get_process_group(full_group_id_path)
if parent_group: if parent_group:
if full_group_id_path not in process_group_cache:
process_group_cache[full_group_id_path] = parent_group
parent_group_array.append( parent_group_array.append(
{"id": parent_group.id, "display_name": parent_group.display_name} {"id": parent_group.id, "display_name": parent_group.display_name}
) )
return parent_group_array return {"cache": process_group_cache, "process_groups": parent_group_array}
@classmethod
def get_parent_group_array(cls, process_identifier: str) -> list[ProcessGroupLite]:
"""Get_parent_group_array."""
parent_group_lites_with_cache = cls.get_parent_group_array_and_cache_it(
process_identifier, {}
)
return parent_group_lites_with_cache["process_groups"]
@classmethod @classmethod
def get_process_groups( def get_process_groups(
@ -339,8 +359,11 @@ class ProcessModelService(FileSystemService):
if len(problem_models) > 0: if len(problem_models) > 0:
raise ApiError( raise ApiError(
error_code="existing_instances", error_code="existing_instances",
message=f"We cannot delete the group `{process_group_id}`, " message=(
f"there are models with existing instances inside the group. {problem_models}", f"We cannot delete the group `{process_group_id}`, there are"
" models with existing instances inside the group."
f" {problem_models}"
),
) )
shutil.rmtree(path) shutil.rmtree(path)
self.cleanup_process_group_display_order() self.cleanup_process_group_display_order()
@ -392,7 +415,10 @@ class ProcessModelService(FileSystemService):
if process_group is None: if process_group is None:
raise ApiError( raise ApiError(
error_code="process_group_could_not_be_loaded_from_disk", error_code="process_group_could_not_be_loaded_from_disk",
message=f"We could not load the process_group from disk from: {dir_path}", message=(
"We could not load the process_group from disk from:"
f" {dir_path}"
),
) )
else: else:
process_group_id = dir_path.replace(FileSystemService.root_path(), "") process_group_id = dir_path.replace(FileSystemService.root_path(), "")
@ -457,7 +483,10 @@ class ProcessModelService(FileSystemService):
if process_model_info is None: if process_model_info is None:
raise ApiError( raise ApiError(
error_code="process_model_could_not_be_loaded_from_disk", error_code="process_model_could_not_be_loaded_from_disk",
message=f"We could not load the process_model from disk with data: {data}", message=(
"We could not load the process_model from disk with data:"
f" {data}"
),
) )
else: else:
if name is None: if name is None:

View File

@ -112,7 +112,10 @@ class ScriptUnitTestRunner:
except json.decoder.JSONDecodeError as ex: except json.decoder.JSONDecodeError as ex:
return ScriptUnitTestResult( return ScriptUnitTestResult(
result=False, result=False,
error=f"Failed to parse expectedOutputJson: {unit_test['expectedOutputJson']}: {str(ex)}", error=(
"Failed to parse expectedOutputJson:"
f" {unit_test['expectedOutputJson']}: {str(ex)}"
),
) )
script = task.task_spec.script script = task.task_spec.script

View File

@ -44,8 +44,10 @@ class SecretService:
except Exception as e: except Exception as e:
raise ApiError( raise ApiError(
error_code="create_secret_error", error_code="create_secret_error",
message=f"There was an error creating a secret with key: {key} and value ending with: {value[:-4]}. " message=(
f"Original error is {e}", f"There was an error creating a secret with key: {key} and value"
f" ending with: {value[:-4]}. Original error is {e}"
),
) from e ) from e
return secret_model return secret_model
@ -89,7 +91,9 @@ class SecretService:
else: else:
raise ApiError( raise ApiError(
error_code="update_secret_error", error_code="update_secret_error",
message=f"Cannot update secret with key: {key}. Resource does not exist.", message=(
f"Cannot update secret with key: {key}. Resource does not exist."
),
status_code=404, status_code=404,
) )
@ -104,11 +108,16 @@ class SecretService:
except Exception as e: except Exception as e:
raise ApiError( raise ApiError(
error_code="delete_secret_error", error_code="delete_secret_error",
message=f"Could not delete secret with key: {key}. Original error is: {e}", message=(
f"Could not delete secret with key: {key}. Original error"
f" is: {e}"
),
) from e ) from e
else: else:
raise ApiError( raise ApiError(
error_code="delete_secret_error", error_code="delete_secret_error",
message=f"Cannot delete secret with key: {key}. Resource does not exist.", message=(
f"Cannot delete secret with key: {key}. Resource does not exist."
),
status_code=404, status_code=404,
) )

View File

@ -192,7 +192,8 @@ class SpecFileService(FileSystemService):
full_file_path = SpecFileService.full_file_path(process_model_info, file_name) full_file_path = SpecFileService.full_file_path(process_model_info, file_name)
if not os.path.exists(full_file_path): if not os.path.exists(full_file_path):
raise ProcessModelFileNotFoundError( raise ProcessModelFileNotFoundError(
f"No file found with name {file_name} in {process_model_info.display_name}" f"No file found with name {file_name} in"
f" {process_model_info.display_name}"
) )
with open(full_file_path, "rb") as f_handle: with open(full_file_path, "rb") as f_handle:
spec_file_data = f_handle.read() spec_file_data = f_handle.read()
@ -314,8 +315,9 @@ class SpecFileService(FileSystemService):
).first() ).first()
if message_model is None: if message_model is None:
raise ValidationException( raise ValidationException(
f"Could not find message model with identifier '{message_model_identifier}'" "Could not find message model with identifier"
f"Required by a Start Event in : {ref.file_name}" f" '{message_model_identifier}'Required by a Start Event in :"
f" {ref.file_name}"
) )
message_triggerable_process_model = ( message_triggerable_process_model = (
MessageTriggerableProcessModel.query.filter_by( MessageTriggerableProcessModel.query.filter_by(
@ -335,7 +337,8 @@ class SpecFileService(FileSystemService):
!= ref.process_model_id != ref.process_model_id
): ):
raise ValidationException( raise ValidationException(
f"Message model is already used to start process model {ref.process_model_id}" "Message model is already used to start process model"
f" {ref.process_model_id}"
) )
@staticmethod @staticmethod
@ -353,8 +356,9 @@ class SpecFileService(FileSystemService):
).first() ).first()
if message_model is None: if message_model is None:
raise ValidationException( raise ValidationException(
f"Could not find message model with identifier '{message_model_identifier}'" "Could not find message model with identifier"
f"specified by correlation property: {cpre}" f" '{message_model_identifier}'specified by correlation"
f" property: {cpre}"
) )
# fixme: I think we are currently ignoring the correction properties. # fixme: I think we are currently ignoring the correction properties.
message_correlation_property = ( message_correlation_property = (

View File

@ -13,6 +13,9 @@ from spiffworkflow_backend.models.human_task_user import HumanTaskUserModel
from spiffworkflow_backend.models.principal import PrincipalModel from spiffworkflow_backend.models.principal import PrincipalModel
from spiffworkflow_backend.models.user import UserModel from spiffworkflow_backend.models.user import UserModel
from spiffworkflow_backend.models.user_group_assignment import UserGroupAssignmentModel from spiffworkflow_backend.models.user_group_assignment import UserGroupAssignmentModel
from spiffworkflow_backend.models.user_group_assignment_waiting import (
UserGroupAssignmentWaitingModel,
)
class UserService: class UserService:
@ -21,11 +24,11 @@ class UserService:
@classmethod @classmethod
def create_user( def create_user(
cls, cls,
username: str,
service: str, service: str,
service_id: str, service_id: str,
name: Optional[str] = "",
username: Optional[str] = "",
email: Optional[str] = "", email: Optional[str] = "",
display_name: Optional[str] = "",
) -> UserModel: ) -> UserModel:
"""Create_user.""" """Create_user."""
user_model: Optional[UserModel] = ( user_model: Optional[UserModel] = (
@ -41,8 +44,8 @@ class UserService:
username=username, username=username,
service=service, service=service,
service_id=service_id, service_id=service_id,
name=name,
email=email, email=email,
display_name=display_name,
) )
db.session.add(user_model) db.session.add(user_model)
@ -55,6 +58,7 @@ class UserService:
message=f"Could not add user {username}", message=f"Could not add user {username}",
) from e ) from e
cls.create_principal(user_model.id) cls.create_principal(user_model.id)
UserService().apply_waiting_group_assignments(user_model)
return user_model return user_model
else: else:
@ -69,45 +73,12 @@ class UserService:
) )
) )
@classmethod
def find_or_create_user(
cls,
service: str,
service_id: str,
name: Optional[str] = None,
username: Optional[str] = None,
email: Optional[str] = None,
) -> UserModel:
"""Find_or_create_user."""
user_model: UserModel
try:
user_model = cls.create_user(
service=service,
service_id=service_id,
name=name,
username=username,
email=email,
)
except ApiError:
user_model = (
UserModel.query.filter(UserModel.service == service)
.filter(UserModel.service_id == service_id)
.first()
)
return user_model
# Returns true if the current user is logged in. # Returns true if the current user is logged in.
@staticmethod @staticmethod
def has_user() -> bool: def has_user() -> bool:
"""Has_user.""" """Has_user."""
return "token" in g and bool(g.token) and "user" in g and bool(g.user) return "token" in g and bool(g.token) and "user" in g and bool(g.user)
# Returns true if the given user uid is different from the current user's uid.
@staticmethod
def is_different_user(uid: str) -> bool:
"""Is_different_user."""
return UserService.has_user() and uid is not None and uid is not g.user.uid
@staticmethod @staticmethod
def current_user() -> Any: def current_user() -> Any:
"""Current_user.""" """Current_user."""
@ -117,20 +88,6 @@ class UserService:
) )
return g.user return g.user
@staticmethod
def in_list(uids: list[str]) -> bool:
"""Returns true if the current user's id is in the given list of ids.
False if there is no user, or the user is not in the list.
"""
if (
UserService.has_user()
): # If someone is logged in, lock tasks that don't belong to them.
user = UserService.current_user()
if user.uid in uids:
return True
return False
@staticmethod @staticmethod
def get_principal_by_user_id(user_id: int) -> PrincipalModel: def get_principal_by_user_id(user_id: int) -> PrincipalModel:
"""Get_principal_by_user_id.""" """Get_principal_by_user_id."""
@ -173,8 +130,57 @@ class UserService:
@classmethod @classmethod
def add_user_to_group(cls, user: UserModel, group: GroupModel) -> None: def add_user_to_group(cls, user: UserModel, group: GroupModel) -> None:
"""Add_user_to_group.""" """Add_user_to_group."""
ugam = UserGroupAssignmentModel(user_id=user.id, group_id=group.id) exists = (
db.session.add(ugam) UserGroupAssignmentModel()
.query.filter_by(user_id=user.id)
.filter_by(group_id=group.id)
.count()
)
if not exists:
ugam = UserGroupAssignmentModel(user_id=user.id, group_id=group.id)
db.session.add(ugam)
db.session.commit()
@classmethod
def add_waiting_group_assignment(cls, username: str, group: GroupModel) -> None:
"""Add_waiting_group_assignment."""
wugam = (
UserGroupAssignmentWaitingModel()
.query.filter_by(username=username)
.filter_by(group_id=group.id)
.first()
)
if not wugam:
wugam = UserGroupAssignmentWaitingModel(
username=username, group_id=group.id
)
db.session.add(wugam)
db.session.commit()
if wugam.is_match_all():
for user in UserModel.query.all():
cls.add_user_to_group(user, group)
@classmethod
def apply_waiting_group_assignments(cls, user: UserModel) -> None:
"""Apply_waiting_group_assignments."""
waiting = (
UserGroupAssignmentWaitingModel()
.query.filter(UserGroupAssignmentWaitingModel.username == user.username)
.all()
)
for assignment in waiting:
cls.add_user_to_group(user, assignment.group)
db.session.delete(assignment)
wildcard = (
UserGroupAssignmentWaitingModel()
.query.filter(
UserGroupAssignmentWaitingModel.username
== UserGroupAssignmentWaitingModel.MATCH_ALL_USERS
)
.all()
)
for assignment in wildcard:
cls.add_user_to_group(user, assignment.group)
db.session.commit() db.session.commit()
@staticmethod @staticmethod

View File

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:spiffworkflow="http://spiffworkflow.org/bpmn/schema/1.0/core" id="Definitions_96f6665" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.0.0-dev">
<bpmn:process id="natural_language_process_id_template" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>Flow_0gixxkm</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="Flow_0gixxkm" sourceRef="StartEvent_1" targetRef="present_form" />
<bpmn:userTask id="present_form" name="Present Form">
<bpmn:extensionElements>
<spiffworkflow:properties>
<spiffworkflow:property name="formJsonSchemaFilename" value="form-identifier-id-template-schema.json" />
<spiffworkflow:property name="formUiSchemaFilename" value="form-identifier-id-template-uischema.json" />
</spiffworkflow:properties>
</bpmn:extensionElements>
<bpmn:incoming>Flow_0gixxkm</bpmn:incoming>
<bpmn:outgoing>Flow_1oi9nsn</bpmn:outgoing>
</bpmn:userTask>
<bpmn:endEvent id="Event_003bxs1">
<bpmn:incoming>Flow_1oi9nsn</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_1oi9nsn" sourceRef="present_form" targetRef="Event_003bxs1" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="natural_language_process_id_template">
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="179" y="159" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0ajk9gf_di" bpmnElement="present_form">
<dc:Bounds x="270" y="137" width="100" height="80" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_003bxs1_di" bpmnElement="Event_003bxs1">
<dc:Bounds x="432" y="159" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_0gixxkm_di" bpmnElement="Flow_0gixxkm">
<di:waypoint x="215" y="177" />
<di:waypoint x="270" y="177" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1oi9nsn_di" bpmnElement="Flow_1oi9nsn">
<di:waypoint x="370" y="177" />
<di:waypoint x="432" y="177" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -0,0 +1,6 @@
{
"title": "{FORM_IDENTIFIER}",
"description": "",
"properties": {},
"required": []
}

View File

@ -0,0 +1,75 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_96f6665" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.0.0-dev">
<bpmn:process id="Process_hjecbuk" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>Flow_0hnphp9</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="Flow_0hnphp9" sourceRef="StartEvent_1" targetRef="Activity_16lbvwu" />
<bpmn:scriptTask id="Activity_16lbvwu">
<bpmn:incoming>Flow_0hnphp9</bpmn:incoming>
<bpmn:outgoing>Flow_0amajxh</bpmn:outgoing>
<bpmn:dataOutputAssociation id="DataOutputAssociation_15x55ya">
<bpmn:targetRef>DataObjectReference_10g8dit</bpmn:targetRef>
</bpmn:dataOutputAssociation>
<bpmn:script>the_data_object_var = 'hey'</bpmn:script>
</bpmn:scriptTask>
<bpmn:sequenceFlow id="Flow_0amajxh" sourceRef="Activity_16lbvwu" targetRef="manual_task" />
<bpmn:endEvent id="Event_0ik0i72">
<bpmn:incoming>Flow_1ifqo6o</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_1ifqo6o" sourceRef="manual_task" targetRef="Event_0ik0i72" />
<bpmn:manualTask id="manual_task">
<bpmn:incoming>Flow_0amajxh</bpmn:incoming>
<bpmn:outgoing>Flow_1ifqo6o</bpmn:outgoing>
<bpmn:property id="Property_0a8w16m" name="__targetRef_placeholder" />
<bpmn:dataInputAssociation id="DataInputAssociation_0iqtpwy">
<bpmn:sourceRef>DataObjectReference_10g8dit</bpmn:sourceRef>
<bpmn:targetRef>Property_0a8w16m</bpmn:targetRef>
</bpmn:dataInputAssociation>
</bpmn:manualTask>
<bpmn:dataObjectReference id="DataObjectReference_10g8dit" name="The Data Object Var" dataObjectRef="the_data_object_var" />
<bpmn:dataObject id="the_data_object_var" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_hjecbuk">
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="179" y="159" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0wqvy5h_di" bpmnElement="Activity_16lbvwu">
<dc:Bounds x="290" y="137" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_0ik0i72_di" bpmnElement="Event_0ik0i72">
<dc:Bounds x="652" y="159" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0keslpp_di" bpmnElement="manual_task">
<dc:Bounds x="470" y="137" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_0hnphp9_di" bpmnElement="Flow_0hnphp9">
<di:waypoint x="215" y="177" />
<di:waypoint x="290" y="177" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0amajxh_di" bpmnElement="Flow_0amajxh">
<di:waypoint x="390" y="177" />
<di:waypoint x="470" y="177" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1ifqo6o_di" bpmnElement="Flow_1ifqo6o">
<di:waypoint x="570" y="177" />
<di:waypoint x="652" y="177" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="DataObjectReference_10g8dit_di" bpmnElement="DataObjectReference_10g8dit">
<dc:Bounds x="412" y="275" width="36" height="50" />
<bpmndi:BPMNLabel>
<dc:Bounds x="390" y="332" width="81" height="27" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="DataInputAssociation_0iqtpwy_di" bpmnElement="DataInputAssociation_0iqtpwy">
<di:waypoint x="448" y="275" />
<di:waypoint x="491" y="217" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="DataOutputAssociation_15x55ya_di" bpmnElement="DataOutputAssociation_15x55ya">
<di:waypoint x="371" y="217" />
<di:waypoint x="416" y="275" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -0,0 +1,86 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:spiffworkflow="http://spiffworkflow.org/bpmn/schema/1.0/core" xmlns:modeler="http://camunda.org/schema/modeler/1.0" id="Definitions_1ny7jp4" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="5.0.0" modeler:executionPlatform="Camunda Platform" modeler:executionPlatformVersion="7.17.0">
<bpmn:process id="sample" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>Flow_10jwwqy</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="Flow_10jwwqy" sourceRef="StartEvent_1" targetRef="script_task_one" />
<bpmn:endEvent id="Event_1qb1u6a">
<bpmn:incoming>Flow_1axnzv6</bpmn:incoming>
</bpmn:endEvent>
<bpmn:scriptTask id="script_task_one" name="My Script" scriptFormat="python">
<bpmn:extensionElements>
<spiffworkflow:unitTests>
<spiffworkflow:unitTest id="ScriptTest_pass">
<spiffworkflow:inputJson>{
"current_user": {
"id": "2",
"username": "ciadmin1"
},
"num": 0
}</spiffworkflow:inputJson>
<spiffworkflow:expectedOutputJson>{
"Mike": "Awesome",
"i": 2,
"current_user": {
"id": "2",
"username": "ciadmin1"
},
"num": 0,
"my_var": "whatwhat",
"person": "Kevin"
}</spiffworkflow:expectedOutputJson>
</spiffworkflow:unitTest>
<spiffworkflow:unitTest id="ScriptTest_fail">
<spiffworkflow:inputJson>{}</spiffworkflow:inputJson>
<spiffworkflow:expectedOutputJson>{}</spiffworkflow:expectedOutputJson>
</spiffworkflow:unitTest>
<spiffworkflow:unitTest id="unit_test_5T42ZRC">
<spiffworkflow:inputJson>{"current_user": {"id": "1", "username": "kb"}}</spiffworkflow:inputJson>
<spiffworkflow:expectedOutputJson>{"Mike": "Awesome", "current_user": {"id": "1", "username": "kb"}, "heyhey": "https://demo.spiffworkflow.org", "i": 2, "members": [], "my_var": "whatwhat", "person": "Kevin"}</spiffworkflow:expectedOutputJson>
</spiffworkflow:unitTest>
</spiffworkflow:unitTests>
</bpmn:extensionElements>
<bpmn:incoming>Flow_10jwwqy</bpmn:incoming>
<bpmn:outgoing>Flow_1utkzvj</bpmn:outgoing>
<bpmn:script>my_var = 'THE VAR'</bpmn:script>
</bpmn:scriptTask>
<bpmn:sequenceFlow id="Flow_1utkzvj" sourceRef="script_task_one" targetRef="script_task_two" />
<bpmn:sequenceFlow id="Flow_1axnzv6" sourceRef="script_task_two" targetRef="Event_1qb1u6a" />
<bpmn:scriptTask id="script_task_two" name="Bad News Bears">
<bpmn:incoming>Flow_1utkzvj</bpmn:incoming>
<bpmn:outgoing>Flow_1axnzv6</bpmn:outgoing>
<bpmn:script>hey</bpmn:script>
</bpmn:scriptTask>
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="sample">
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="132" y="102" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_1qb1u6a_di" bpmnElement="Event_1qb1u6a">
<dc:Bounds x="612" y="102" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_17ohe7r_di" bpmnElement="script_task_one">
<dc:Bounds x="241" y="80" width="100" height="80" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_03fze1z_di" bpmnElement="script_task_two">
<dc:Bounds x="420" y="80" width="100" height="80" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_10jwwqy_di" bpmnElement="Flow_10jwwqy">
<di:waypoint x="168" y="120" />
<di:waypoint x="241" y="120" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1utkzvj_di" bpmnElement="Flow_1utkzvj">
<di:waypoint x="341" y="120" />
<di:waypoint x="420" y="120" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1axnzv6_di" bpmnElement="Flow_1axnzv6">
<di:waypoint x="520" y="120" />
<di:waypoint x="612" y="120" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -0,0 +1,137 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:spiffworkflow="http://spiffworkflow.org/bpmn/schema/1.0/core" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_96f6665" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.0.0-dev">
<bpmn:collaboration id="Collaboration_1022bxx">
<bpmn:participant id="Participant_1gfxnts" processRef="Process_1oafp0t" />
</bpmn:collaboration>
<bpmn:process id="Process_1oafp0t" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>Flow_1l15rbh</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="Flow_1l15rbh" sourceRef="StartEvent_1" targetRef="Gateway_0n53kj7" />
<bpmn:eventBasedGateway id="Gateway_0n53kj7">
<bpmn:incoming>Flow_1l15rbh</bpmn:incoming>
<bpmn:outgoing>Flow_0d35i06</bpmn:outgoing>
<bpmn:outgoing>Flow_0tzaigt</bpmn:outgoing>
<bpmn:outgoing>Flow_1vld4r2</bpmn:outgoing>
</bpmn:eventBasedGateway>
<bpmn:sequenceFlow id="Flow_0d35i06" sourceRef="Gateway_0n53kj7" targetRef="Event_0xbr8bu" />
<bpmn:intermediateCatchEvent id="Event_0xbr8bu">
<bpmn:incoming>Flow_0d35i06</bpmn:incoming>
<bpmn:outgoing>Flow_1w3n49n</bpmn:outgoing>
<bpmn:messageEventDefinition id="MessageEventDefinition_1aazu62" messageRef="message_1" />
</bpmn:intermediateCatchEvent>
<bpmn:intermediateCatchEvent id="Event_0himdx6">
<bpmn:incoming>Flow_0tzaigt</bpmn:incoming>
<bpmn:outgoing>Flow_1q47ol8</bpmn:outgoing>
<bpmn:messageEventDefinition id="MessageEventDefinition_0oersqt" messageRef="message_2" />
</bpmn:intermediateCatchEvent>
<bpmn:sequenceFlow id="Flow_0tzaigt" sourceRef="Gateway_0n53kj7" targetRef="Event_0himdx6" />
<bpmn:sequenceFlow id="Flow_1vld4r2" sourceRef="Gateway_0n53kj7" targetRef="Event_0e4owa3" />
<bpmn:sequenceFlow id="Flow_13ai5vv" sourceRef="Event_0e4owa3" targetRef="Activity_0uum4kq" />
<bpmn:endEvent id="Event_0vmxgb9">
<bpmn:incoming>Flow_1q47ol8</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_1q47ol8" sourceRef="Event_0himdx6" targetRef="Event_0vmxgb9" />
<bpmn:sequenceFlow id="Flow_1w3n49n" sourceRef="Event_0xbr8bu" targetRef="Event_174a838" />
<bpmn:endEvent id="Event_174a838">
<bpmn:incoming>Flow_1w3n49n</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_1vwnf3n" sourceRef="Activity_0uum4kq" targetRef="Event_1ixib8a" />
<bpmn:intermediateCatchEvent id="Event_0e4owa3">
<bpmn:incoming>Flow_1vld4r2</bpmn:incoming>
<bpmn:outgoing>Flow_13ai5vv</bpmn:outgoing>
<bpmn:timerEventDefinition id="TimerEventDefinition_1fnogr9">
<bpmn:timeDuration xsi:type="bpmn:tFormalExpression">timedelta(hours=1)</bpmn:timeDuration>
</bpmn:timerEventDefinition>
</bpmn:intermediateCatchEvent>
<bpmn:manualTask id="Activity_0uum4kq" name="Any Task">
<bpmn:extensionElements>
<spiffworkflow:instructionsForEndUser>Click the button.</spiffworkflow:instructionsForEndUser>
</bpmn:extensionElements>
<bpmn:incoming>Flow_13ai5vv</bpmn:incoming>
<bpmn:outgoing>Flow_1vwnf3n</bpmn:outgoing>
</bpmn:manualTask>
<bpmn:endEvent id="Event_1ixib8a">
<bpmn:incoming>Flow_1vwnf3n</bpmn:incoming>
</bpmn:endEvent>
</bpmn:process>
<bpmn:message id="message_1" name="Message 1">
<bpmn:extensionElements>
<spiffworkflow:messageVariable>result</spiffworkflow:messageVariable>
</bpmn:extensionElements>
</bpmn:message>
<bpmn:message id="message_2" name="Message 2">
<bpmn:extensionElements>
<spiffworkflow:messageVariable>result</spiffworkflow:messageVariable>
</bpmn:extensionElements>
</bpmn:message>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Collaboration_1022bxx">
<bpmndi:BPMNShape id="Participant_1gfxnts_di" bpmnElement="Participant_1gfxnts" isHorizontal="true">
<dc:Bounds x="120" y="70" width="630" height="310" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="192" y="172" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_0yjcvjd_di" bpmnElement="Gateway_0n53kj7">
<dc:Bounds x="285" y="165" width="50" height="50" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_0xbr8bu_di" bpmnElement="Event_0xbr8bu">
<dc:Bounds x="392" y="102" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_0himdx6_di" bpmnElement="Event_0himdx6">
<dc:Bounds x="392" y="172" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_0vmxgb9_di" bpmnElement="Event_0vmxgb9">
<dc:Bounds x="492" y="172" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_174a838_di" bpmnElement="Event_174a838">
<dc:Bounds x="492" y="102" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_0e4owa3_di" bpmnElement="Event_0e4owa3">
<dc:Bounds x="392" y="272" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_104jmxm_di" bpmnElement="Activity_0uum4kq">
<dc:Bounds x="480" y="250" width="100" height="80" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_1ixib8a_di" bpmnElement="Event_1ixib8a">
<dc:Bounds x="662" y="272" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_1l15rbh_di" bpmnElement="Flow_1l15rbh">
<di:waypoint x="228" y="190" />
<di:waypoint x="285" y="190" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0d35i06_di" bpmnElement="Flow_0d35i06">
<di:waypoint x="310" y="165" />
<di:waypoint x="310" y="120" />
<di:waypoint x="392" y="120" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0tzaigt_di" bpmnElement="Flow_0tzaigt">
<di:waypoint x="335" y="190" />
<di:waypoint x="392" y="190" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1vld4r2_di" bpmnElement="Flow_1vld4r2">
<di:waypoint x="310" y="215" />
<di:waypoint x="310" y="290" />
<di:waypoint x="392" y="290" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_13ai5vv_di" bpmnElement="Flow_13ai5vv">
<di:waypoint x="428" y="290" />
<di:waypoint x="480" y="290" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1q47ol8_di" bpmnElement="Flow_1q47ol8">
<di:waypoint x="428" y="190" />
<di:waypoint x="492" y="190" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1w3n49n_di" bpmnElement="Flow_1w3n49n">
<di:waypoint x="428" y="120" />
<di:waypoint x="492" y="120" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1vwnf3n_di" bpmnElement="Flow_1vwnf3n">
<di:waypoint x="580" y="290" />
<di:waypoint x="662" y="290" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

Some files were not shown because too many files have changed in this diff Show More