diff --git a/.github/workflows/release_builds.yml b/.github/workflows/release_builds.yml new file mode 100644 index 000000000..ef1c3b992 --- /dev/null +++ b/.github/workflows/release_builds.yml @@ -0,0 +1,117 @@ +name: Release Builds + +on: + push: + tags: [ v* ] + +jobs: + create_frontend_docker_container: + runs-on: ubuntu-latest + env: + REGISTRY: ghcr.io + IMAGE_NAME: sartography/spiffworkflow-frontend + permissions: + contents: read + packages: write + steps: + - name: Check out the repository + uses: actions/checkout@v3.0.2 + with: + # Disabling shallow clone is recommended for improving relevancy of reporting in sonarcloud + fetch-depth: 0 + - name: Log in to the Container registry + uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 + with: + context: spiffworkflow-frontend + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + + - name: Build and push Frontend Docker image + uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc + with: + # this action doesn't seem to respect working-directory so set context + context: spiffworkflow-frontend + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + + create_backend_docker_container: + runs-on: ubuntu-latest + env: + REGISTRY: ghcr.io + IMAGE_NAME: sartography/spiffworkflow-backend + permissions: + contents: read + packages: write + steps: + - name: Check out the repository + uses: actions/checkout@v3.0.2 + with: + # Disabling shallow clone is recommended for improving relevancy of reporting in sonarcloud + fetch-depth: 0 + - name: Log in to the Container registry + uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + + - name: Build and push Backend Docker image + uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc + with: + # this action doesn't seem to respect working-directory so set context + context: spiffworkflow-backend + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} +# Is this getting updated, I wonder? + create_demo-proxy: + runs-on: ubuntu-latest + env: + REGISTRY: ghcr.io + IMAGE_NAME: sartography/connector-proxy-demo + + permissions: + contents: read + packages: write + steps: + - name: Check out the repository + uses: actions/checkout@v3.0.2 + with: + # Disabling shallow clone is recommended for improving relevancy of reporting in sonarcloud + fetch-depth: 0 + - name: Log in to the Container registry + uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 + with: + context: connector-proxy-demo + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + + - name: Build and push the connector proxy + uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc + with: + # this action doesn't seem to respect working-directory so set context + context: connector-proxy-demo + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/connector-proxy-demo/Dockerfile b/connector-proxy-demo/Dockerfile new file mode 100644 index 000000000..2e1a76b7f --- /dev/null +++ b/connector-proxy-demo/Dockerfile @@ -0,0 +1,27 @@ +FROM ghcr.io/sartography/python:3.11 + +RUN pip install poetry +RUN useradd _gunicorn --no-create-home --user-group + +RUN apt-get update && \ + apt-get install -y -q \ + gcc libssl-dev \ + curl gunicorn3 + +WORKDIR /app +COPY pyproject.toml poetry.lock /app/ +RUN poetry install --without dev + +RUN set -xe \ + && apt-get remove -y gcc python3-dev libssl-dev \ + && apt-get autoremove -y \ + && apt-get clean -y \ + && rm -rf /var/lib/apt/lists/* + +COPY . /app/ + +# run poetry install again AFTER copying the app into the image +# otherwise it does not know what the main app module is +RUN poetry install --without dev + +CMD ./bin/boot_server_in_docker diff --git a/connector-proxy-demo/bin/boot_server_in_docker b/connector-proxy-demo/bin/boot_server_in_docker new file mode 100755 index 000000000..1179bf5bb --- /dev/null +++ b/connector-proxy-demo/bin/boot_server_in_docker @@ -0,0 +1,19 @@ +#!/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 + +port="${CONNECTOR_PROXY_PORT:-}" +if [[ -z "$port" ]]; then + port=7004 +fi + +workers=3 + +# THIS MUST BE THE LAST COMMAND! +# default --limit-request-line is 4094. see https://stackoverflow.com/a/66688382/6090676 +exec poetry run gunicorn --bind "0.0.0.0:$port" --workers="$workers" --limit-request-line 8192 --timeout 90 --capture-output --access-logfile '-' --log-level debug app:app diff --git a/connector-proxy-demo/poetry.lock b/connector-proxy-demo/poetry.lock index 9147d0315..d7798e2dc 100644 --- a/connector-proxy-demo/poetry.lock +++ b/connector-proxy-demo/poetry.lock @@ -55,7 +55,7 @@ optional = false python-versions = ">=3.6.0" [package.extras] -unicode_backport = ["unicodedata2"] +unicode-backport = ["unicodedata2"] [[package]] name = "click" @@ -127,6 +127,23 @@ Flask = "*" oauthlib = ">=1.1.2,<2.0.3 || >2.0.3,<2.0.4 || >2.0.4,<2.0.5 || >2.0.5,<3.0.0" requests-oauthlib = ">=0.6.2,<1.2.0" +[[package]] +name = "gunicorn" +version = "20.1.0" +description = "WSGI HTTP Server for UNIX" +category = "main" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +setuptools = ">=3.0" + +[package.extras] +eventlet = ["eventlet (>=0.24.1)"] +gevent = ["gevent (>=1.4.0)"] +setproctitle = ["setproctitle"] +tornado = ["tornado (>=0.2)"] + [[package]] name = "idna" version = "3.4" @@ -214,7 +231,7 @@ urllib3 = ">=1.21.1,<1.27" [package.extras] socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "requests-oauthlib" @@ -245,6 +262,19 @@ botocore = ">=1.12.36,<2.0a.0" [package.extras] crt = ["botocore[crt] (>=1.20.29,<2.0a.0)"] +[[package]] +name = "setuptools" +version = "65.6.0" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] + [[package]] name = "simplejson" version = "3.17.6" @@ -310,7 +340,7 @@ watchdog = ["watchdog"] [metadata] lock-version = "1.1" python-versions = "^3.10" -content-hash = "86cf682d49dc495c8cf6dc60a8aedc31ad32a293e6ceaf7b1428e0c232f8319e" +content-hash = "cc395c0c1ce2b0b7ca063a17617981b2d55db39802265b36f0bc3c4383c89919" [metadata.files] boto3 = [ @@ -350,6 +380,10 @@ Flask-OAuthlib = [ {file = "Flask-OAuthlib-0.9.6.tar.gz", hash = "sha256:5bb79c8a8e670c2eb4cb553dfc3283b6c8d1202f674934676dc173cee94fe39c"}, {file = "Flask_OAuthlib-0.9.6-py3-none-any.whl", hash = "sha256:a5c3b62959aa1922470a62b6ebf4273b75f1c29561a7eb4a69cde85d45a1d669"}, ] +gunicorn = [ + {file = "gunicorn-20.1.0-py3-none-any.whl", hash = "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e"}, + {file = "gunicorn-20.1.0.tar.gz", hash = "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8"}, +] idna = [ {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, @@ -428,6 +462,10 @@ s3transfer = [ {file = "s3transfer-0.6.0-py3-none-any.whl", hash = "sha256:06176b74f3a15f61f1b4f25a1fc29a4429040b7647133a463da8fa5bd28d5ecd"}, {file = "s3transfer-0.6.0.tar.gz", hash = "sha256:2ed07d3866f523cc561bf4a00fc5535827981b117dd7876f036b0c1aca42c947"}, ] +setuptools = [ + {file = "setuptools-65.6.0-py3-none-any.whl", hash = "sha256:6211d2f5eddad8757bd0484923ca7c0a6302ebc4ab32ea5e94357176e0ca0840"}, + {file = "setuptools-65.6.0.tar.gz", hash = "sha256:d1eebf881c6114e51df1664bc2c9133d022f78d12d5f4f665b9191f084e2862d"}, +] simplejson = [ {file = "simplejson-3.17.6-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a89acae02b2975b1f8e4974cb8cdf9bf9f6c91162fb8dec50c259ce700f2770a"}, {file = "simplejson-3.17.6-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:82ff356ff91be0ab2293fc6d8d262451eb6ac4fd999244c4b5f863e049ba219c"}, diff --git a/connector-proxy-demo/pyproject.toml b/connector-proxy-demo/pyproject.toml index 9a6f51f88..8acd820e6 100644 --- a/connector-proxy-demo/pyproject.toml +++ b/connector-proxy-demo/pyproject.toml @@ -5,14 +5,14 @@ description = "An example showing how to use the Spiffworkflow-proxy's Flask Blu authors = ["Dan "] license = "LGPL" readme = "README.md" -packages = [{include = "connector_proxy_demo", from = "src"}] +#packages = [{include = "connector_proxy_demo", from = "."}] [tool.poetry.dependencies] python = "^3.10" Flask = "^2.2.2" spiffworkflow-proxy = {git = "https://github.com/sartography/spiffworkflow-proxy"} connector-aws = { git = "https://github.com/sartography/connector-aws.git"} - +gunicorn = "^20.1.0" [build-system] requires = ["poetry-core"] @@ -20,5 +20,5 @@ build-backend = "poetry.core.masonry.api" [tool.pytest.ini_options] pythonpath = [ - ".", "src", + "." ] \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..1cf550248 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,83 @@ +version: "3.8" +services: + spiffworkflow-db: + container_name: spiffworkflow-db + image: mysql:8.0.29 + platform: linux/amd64 + cap_add: + - SYS_NICE + restart: "no" + environment: + - MYSQL_DATABASE=spiffworkflow_backend_development + - MYSQL_ROOT_PASSWORD=my-secret-pw + - MYSQL_TCP_PORT=7003 + ports: + - "7003" + healthcheck: + test: mysql --user=root --password=my-secret-pw -e 'select 1' spiffworkflow_backend_development + interval: 10s + timeout: 5s + retries: 10 + + spiffworkflow-backend: + container_name: spiffworkflow-backend + image: ghcr.io/sartography/spiffworkflow-backend:latest + depends_on: + spiffworkflow-db: + condition: service_healthy + environment: + - APPLICATION_ROOT=/ + - SPIFFWORKFLOW_BACKEND_ENV=development + - FLASK_DEBUG=0 + - FLASK_SESSION_SECRET_KEY=super_secret_key + - OPEN_ID_SERVER_URL=http://localhost:7000/openid + - SPIFFWORKFLOW_FRONTEND_URL=http://localhost:7001 + - SPIFFWORKFLOW_BACKEND_URL=http://localhost:7000 + - SPIFFWORKFLOW_BACKEND_PORT=7000 + - SPIFFWORKFLOW_BACKEND_UPGRADE_DB=true + - SPIFFWORKFLOW_BACKEND_DATABASE_URI=mysql+mysqlconnector://root:my-secret-pw@spiffworkflow-db:7003/spiffworkflow_backend_development + - BPMN_SPEC_ABSOLUTE_DIR=/app/process_models + - SPIFFWORKFLOW_BACKEND_LOAD_FIXTURE_DATA=false + - SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME=example.yml + - RUN_BACKGROUND_SCHEDULER=true + - OPEN_ID_CLIENT_ID=spiffworkflow-backend + - OPEN_ID_CLIENT_SECRET_KEY=my_open_id_secret_key + ports: + - "7000:7000" + volumes: + - ./process_models:/app/process_models + - ./log:/app/log + healthcheck: + test: curl localhost:7000/v1.0/status --fail + interval: 10s + timeout: 5s + retries: 20 + + spiffworkflow-frontend: + container_name: spiffworkflow-frontend + image: ghcr.io/sartography/spiffworkflow-frontend + environment: + - APPLICATION_ROOT=/ + - PORT0=7001 + ports: + - "7001:7001" + + spiffworkflow-connector: + container_name: spiffworkflow-connector + image: ghcr.io/sartography/connector-proxy-demo + environment: + - FLASK_ENV=${FLASK_ENV:-development} + - FLASK_DEBUG=0 + - FLASK_SESSION_SECRET_KEY=${FLASK_SESSION_SECRET_KEY:-super_secret_key} + ports: + - "7004:7004" + healthcheck: + test: curl localhost:7004/liveness --fail + interval: 10s + timeout: 5s + retries: 20 + + +volumes: + spiffworkflow_backend: + driver: local diff --git a/flask-bpmn/pyproject.toml b/flask-bpmn/pyproject.toml index 3cb3217a5..105fa15d6 100644 --- a/flask-bpmn/pyproject.toml +++ b/flask-bpmn/pyproject.toml @@ -64,7 +64,6 @@ sphinx-click = "^4.3.0" Pygments = "^2.13.0" pyupgrade = "^3.2.2" furo = ">=2021.11.12" -MonkeyType = "^22.2.0" [tool.poetry.scripts] flask-bpmn = "flask_bpmn.__main__:main" diff --git a/flask-bpmn/src/flask_bpmn/models/db.py b/flask-bpmn/src/flask_bpmn/models/db.py index 643e1a85d..a288e6fda 100644 --- a/flask-bpmn/src/flask_bpmn/models/db.py +++ b/flask-bpmn/src/flask_bpmn/models/db.py @@ -8,8 +8,8 @@ from typing import Any from flask_migrate import Migrate # type: ignore from flask_sqlalchemy import SQLAlchemy # type: ignore from sqlalchemy import event # type: ignore -from sqlalchemy.engine.base import Connection # type: ignore -from sqlalchemy.orm.mapper import Mapper # type: ignore +from sqlalchemy.engine.base import Connection +from sqlalchemy.orm.mapper import Mapper db = SQLAlchemy() migrate = Migrate() diff --git a/poetry.lock b/poetry.lock index 2a8d7b0d5..e5c9c4c04 100644 --- a/poetry.lock +++ b/poetry.lock @@ -614,7 +614,7 @@ werkzeug = "*" type = "git" url = "https://github.com/sartography/flask-bpmn" reference = "main" -resolved_reference = "5e40777f4013f71f2c1237f13f7dba1bdd5c0de3" +resolved_reference = "860f2387bebdaa9220e9fbf6f8fa7f74e805d0d4" [[package]] name = "flask-cors" @@ -884,22 +884,6 @@ category = "main" optional = false python-versions = ">=3.7" -[[package]] -name = "libcst" -version = "0.4.7" -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]] name = "livereload" version = "2.6.3" @@ -1005,18 +989,6 @@ category = "dev" optional = false 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]] name = "mypy" version = "0.982" @@ -1788,7 +1760,7 @@ lxml = "*" type = "git" url = "https://github.com/sartography/SpiffWorkflow" reference = "main" -resolved_reference = "580939cc8cb0b7ade1571483bd1e28f554434ac4" +resolved_reference = "bba7ddf5478af579b891ca63c50babbfccf6b7a4" [[package]] name = "sqlalchemy" @@ -1998,18 +1970,6 @@ category = "main" optional = false python-versions = ">=3.7" -[[package]] -name = "typing-inspect" -version = "0.8.0" -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]] name = "tzdata" version = "2022.5" @@ -2151,7 +2111,7 @@ tests-strict = ["cmake (==3.21.2)", "codecov (==2.0.15)", "ninja (==1.10.2)", "p [metadata] lock-version = "1.1" python-versions = ">=3.11,<3.12" -content-hash = "8c37333988fdd68bc6868faf474e628a690582acd17ee3b31b18e005a864fecf" +content-hash = "17e037a3784758eb23a5ed9889fd774913ebde97225692dcd9df159f03da8a22" [metadata.files] alabaster = [ @@ -2484,6 +2444,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_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be9e0fb2ada7e5124f5282d6381903183ecc73ea019568d6d63d33f25b2a9000"}, {file = "greenlet-2.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b493db84d124805865adc587532ebad30efa68f79ad68f11b336e0a51ec86c2"}, + {file = "greenlet-2.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0459d94f73265744fee4c2d5ec44c6f34aa8a31017e6e9de770f7bcf29710be9"}, {file = "greenlet-2.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a20d33124935d27b80e6fdacbd34205732660e0a1d35d8b10b3328179a2b51a1"}, {file = "greenlet-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:ea688d11707d30e212e0110a1aac7f7f3f542a259235d396f88be68b649e47d1"}, {file = "greenlet-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:afe07421c969e259e9403c3bb658968702bc3b78ec0b6fde3ae1e73440529c23"}, @@ -2492,6 +2453,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_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:356e4519d4dfa766d50ecc498544b44c0249b6de66426041d7f8b751de4d6b48"}, {file = "greenlet-2.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:811e1d37d60b47cb8126e0a929b58c046251f28117cb16fcd371eed61f66b764"}, + {file = "greenlet-2.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d38ffd0e81ba8ef347d2be0772e899c289b59ff150ebbbbe05dc61b1246eb4e0"}, {file = "greenlet-2.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0109af1138afbfb8ae647e31a2b1ab030f58b21dd8528c27beaeb0093b7938a9"}, {file = "greenlet-2.0.1-cp38-cp38-win32.whl", hash = "sha256:88c8d517e78acdf7df8a2134a3c4b964415b575d2840a2746ddb1cc6175f8608"}, {file = "greenlet-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:d6ee1aa7ab36475035eb48c01efae87d37936a8173fc4d7b10bb02c2d75dd8f6"}, @@ -2500,6 +2462,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_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cce1e90dd302f45716a7715517c6aa0468af0bf38e814ad4eab58e88fc09f7f7"}, {file = "greenlet-2.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e9744c657d896c7b580455e739899e492a4a452e2dd4d2b3e459f6b244a638d"}, + {file = "greenlet-2.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:662e8f7cad915ba75d8017b3e601afc01ef20deeeabf281bd00369de196d7726"}, {file = "greenlet-2.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:41b825d65f31e394b523c84db84f9383a2f7eefc13d987f308f4663794d2687e"}, {file = "greenlet-2.0.1-cp39-cp39-win32.whl", hash = "sha256:db38f80540083ea33bdab614a9d28bcec4b54daa5aff1668d7827a9fc769ae0a"}, {file = "greenlet-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:b23d2a46d53210b498e5b701a1913697671988f4bf8e10f935433f6e7c332fb6"}, @@ -2566,32 +2529,6 @@ lazy-object-proxy = [ {file = "lazy_object_proxy-1.8.0-pp38-pypy38_pp73-any.whl", hash = "sha256:7e1561626c49cb394268edd00501b289053a652ed762c58e1081224c8d881cec"}, {file = "lazy_object_proxy-1.8.0-pp39-pypy39_pp73-any.whl", hash = "sha256:ce58b2b3734c73e68f0e30e4e725264d4d6be95818ec0a0be4bb6bf9a7e79aa8"}, ] -libcst = [ - {file = "libcst-0.4.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dc6f8965b6ca68d47e11321772887d81fa6fd8ea86e6ef87434ca2147de10747"}, - {file = "libcst-0.4.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a8f47d809df59fcd83058b777b86a300154ee3a1f1b0523a398a67b5f8affd4c"}, - {file = "libcst-0.4.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c0d19de56aa733b4ef024527e3ce4896d4b0e9806889797f409ec24caa651a44"}, - {file = "libcst-0.4.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31da97bc986dc3f7a97f7d431fa911932aaf716d2f8bcda947fc964afd3b57cd"}, - {file = "libcst-0.4.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:71b2e2c5e33e53669c20de0853cecfac1ffb8657ee727ab8527140f39049b820"}, - {file = "libcst-0.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:76fae68bd6b7ce069e267b3322c806b4305341cea78d161ae40e0ed641c8c660"}, - {file = "libcst-0.4.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:bac76d69980bb3254f503f52128c256ef4d1bcbaabe4a17c3a9ebcd1fc0472c0"}, - {file = "libcst-0.4.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26f86535271eaefe84a99736875566a038449f92e1a2a61ea0b588d8359fbefd"}, - {file = "libcst-0.4.7-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:617f7fa2610a8c86cf22d8d03416f25391383d05bd0ad1ca8ef68023ddd6b4f6"}, - {file = "libcst-0.4.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c3637fffe476c5b4ee2225c6474b83382518f2c1b2fe4771039e06bdd7835a4a"}, - {file = "libcst-0.4.7-cp37-cp37m-win_amd64.whl", hash = "sha256:f56565124c2541adee0634e411b2126b3f335306d19e91ed2bfe52efa698b219"}, - {file = "libcst-0.4.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0ca2771ff3cfdf1f148349f89fcae64afa365213ed5c2703a69a89319325d0c8"}, - {file = "libcst-0.4.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:aa438131b7befc7e5a3cbadb5a7b1506305de5d62262ea0556add0152f40925e"}, - {file = "libcst-0.4.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c6bd66a8be2ffad7b968d90dae86c62fd4739c0e011d71f3e76544a891ae743"}, - {file = "libcst-0.4.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:214a9c4f4f90cd5b4bfa18e17877da4dd9a896821d9af9be86fa3effdc289b9b"}, - {file = "libcst-0.4.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27a37f2b459a8b51a41e260bd89c24ae41ab1d658f610c91650c79b1bbf27138"}, - {file = "libcst-0.4.7-cp38-cp38-win_amd64.whl", hash = "sha256:2f6766391d90472f036b88a95251c87d498ab068c377724f212ab0cc20509a68"}, - {file = "libcst-0.4.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:234293aa8681a3d47fef1716c5622797a81cbe85a9381fe023815468cfe20eed"}, - {file = "libcst-0.4.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fa618dc359663a0a097c633452b104c1ca93365da7a811e655c6944f6b323239"}, - {file = "libcst-0.4.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3569d9901c18940632414fb7a0943bffd326db9f726a9c041664926820857815"}, - {file = "libcst-0.4.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:beb5347e46b419f782589da060e9300957e71d561aa5574309883b71f93c1dfe"}, - {file = "libcst-0.4.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e541ccfeebda1ae5f005fc120a5bf3e8ac9ccfda405ec3efd3df54fc4688ac3"}, - {file = "libcst-0.4.7-cp39-cp39-win_amd64.whl", hash = "sha256:3a2b7253cd2e3f0f8a3e23b5c2acb492811d865ef36e0816091c925f32b713d2"}, - {file = "libcst-0.4.7.tar.gz", hash = "sha256:95c52c2130531f6e726a3b077442cfd486975435fecf3db8224d43fba7b85099"}, -] livereload = [ {file = "livereload-2.6.3.tar.gz", hash = "sha256:776f2f865e59fde56490a56bcc6773b6917366bce0c267c60ee8aaf1a0959869"}, ] @@ -2729,10 +2666,6 @@ mccabe = [ {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, {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 = [ {file = "mypy-0.982-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5085e6f442003fa915aeb0a46d4da58128da69325d8213b4b35cc7054090aed5"}, {file = "mypy-0.982-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:41fd1cf9bc0e1c19b9af13a6580ccb66c381a5ee2cf63ee5ebab747a4badeba3"}, @@ -3336,10 +3269,6 @@ typing-extensions = [ {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"}, {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"}, ] -typing-inspect = [ - {file = "typing_inspect-0.8.0-py3-none-any.whl", hash = "sha256:5fbf9c1e65d4fa01e701fe12a5bca6c6e08a4ffd5bc60bfac028253a447c5188"}, - {file = "typing_inspect-0.8.0.tar.gz", hash = "sha256:8b1ff0c400943b6145df8119c41c244ca8207f1f10c9c057aeed1560e4806e3d"}, -] tzdata = [ {file = "tzdata-2022.5-py2.py3-none-any.whl", hash = "sha256:323161b22b7802fdc78f20ca5f6073639c64f1a7227c40cd3e19fd1d0ce6650a"}, {file = "tzdata-2022.5.tar.gz", hash = "sha256:e15b2b3005e2546108af42a0eb4ccab4d9e225e2dfbf4f77aad50c70a4b1f3ab"}, diff --git a/pyproject.toml b/pyproject.toml index 3f74a8a60..371f30f0e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -99,7 +99,6 @@ sphinx-click = "^4.3.0" Pygments = "^2.10.0" pyupgrade = "^3.1.0" furo = ">=2021.11.12" -MonkeyType = "^22.2.0" [tool.poetry.scripts] spiffworkflow-backend = "spiffworkflow_backend.__main__:main" diff --git a/spiffworkflow-backend/bin/import_tickets_for_command_line.py b/spiffworkflow-backend/bin/import_tickets_for_command_line.py index e193b5990..a993a3d30 100644 --- a/spiffworkflow-backend/bin/import_tickets_for_command_line.py +++ b/spiffworkflow-backend/bin/import_tickets_for_command_line.py @@ -1,6 +1,5 @@ """Grabs tickets from csv and makes process instances.""" import csv - from flask_bpmn.models.db import db from spiffworkflow_backend import get_hacked_up_app_for_script diff --git a/spiffworkflow-backend/conftest.py b/spiffworkflow-backend/conftest.py index c3af94332..2ded9053b 100644 --- a/spiffworkflow-backend/conftest.py +++ b/spiffworkflow-backend/conftest.py @@ -1,12 +1,12 @@ """Conftest.""" import os import shutil +from flask_bpmn.models.db import db +from flask_bpmn.models.db import SpiffworkflowBaseDBModel import pytest from flask.app import Flask from flask.testing import FlaskClient -from flask_bpmn.models.db import db -from flask_bpmn.models.db import SpiffworkflowBaseDBModel from tests.spiffworkflow_backend.helpers.base_test import BaseTest from spiffworkflow_backend.models.active_task_user import ActiveTaskUserModel diff --git a/spiffworkflow-backend/migrations/versions/40a2ed63cc5a_.py b/spiffworkflow-backend/migrations/versions/4d75421c0af0_.py similarity index 99% rename from spiffworkflow-backend/migrations/versions/40a2ed63cc5a_.py rename to spiffworkflow-backend/migrations/versions/4d75421c0af0_.py index 6abd6b4ab..34fa1e974 100644 --- a/spiffworkflow-backend/migrations/versions/40a2ed63cc5a_.py +++ b/spiffworkflow-backend/migrations/versions/4d75421c0af0_.py @@ -1,8 +1,8 @@ """empty message -Revision ID: 40a2ed63cc5a +Revision ID: 4d75421c0af0 Revises: -Create Date: 2022-11-29 16:59:02.980181 +Create Date: 2022-12-06 17:42:56.417673 """ from alembic import op @@ -10,7 +10,7 @@ import sqlalchemy as sa # revision identifiers, used by Alembic. -revision = '40a2ed63cc5a' +revision = '4d75421c0af0' down_revision = None branch_labels = None depends_on = None @@ -79,8 +79,7 @@ def upgrade(): sa.Column('email', sa.String(length=255), nullable=True), sa.PrimaryKeyConstraint('id'), sa.UniqueConstraint('service', 'service_id', name='service_key'), - sa.UniqueConstraint('uid'), - sa.UniqueConstraint('username') + sa.UniqueConstraint('uid') ) op.create_table('message_correlation_property', sa.Column('id', sa.Integer(), nullable=False), diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/__init__.py b/spiffworkflow-backend/src/spiffworkflow_backend/__init__.py index de73385f3..e894f2f98 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/__init__.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/__init__.py @@ -1,5 +1,8 @@ """__init__.""" import os +from flask_bpmn.api.api_error import api_error_blueprint +from flask_bpmn.models.db import db +from flask_bpmn.models.db import migrate from typing import Any import connexion # type: ignore @@ -9,9 +12,6 @@ import sqlalchemy from apscheduler.schedulers.background import BackgroundScheduler # type: ignore from apscheduler.schedulers.base import BaseScheduler # type: ignore from flask.json.provider import DefaultJSONProvider -from flask_bpmn.api.api_error import api_error_blueprint -from flask_bpmn.models.db import db -from flask_bpmn.models.db import migrate from flask_cors import CORS # type: ignore from flask_mail import Mail # type: ignore from werkzeug.exceptions import NotFound @@ -19,6 +19,9 @@ from werkzeug.exceptions import NotFound import spiffworkflow_backend.load_database_models # noqa: F401 from spiffworkflow_backend.config import setup_config from spiffworkflow_backend.routes.admin_blueprint.admin_blueprint import admin_blueprint +from spiffworkflow_backend.routes.openid_blueprint.openid_blueprint import ( + 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_blueprint import user_blueprint @@ -103,6 +106,7 @@ def create_app() -> flask.app.Flask: app.register_blueprint(process_api_blueprint) app.register_blueprint(api_error_blueprint) app.register_blueprint(admin_blueprint, url_prefix="/admin") + app.register_blueprint(openid_blueprint, url_prefix="/openid") # preflight options requests will be allowed if they meet the requirements of the url regex. # we will add an Access-Control-Max-Age header to the response to tell the browser it doesn't diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/config/__init__.py b/spiffworkflow-backend/src/spiffworkflow_backend/config/__init__.py index b56683ca3..00f540566 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/config/__init__.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/config/__init__.py @@ -14,13 +14,13 @@ class ConfigurationError(Exception): def setup_database_uri(app: Flask) -> None: """Setup_database_uri.""" - if os.environ.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']}" - if os.environ.get("SPIFF_DATABASE_TYPE") == "sqlite": + if app.config.get("SPIFF_DATABASE_TYPE") == "sqlite": app.config[ "SQLALCHEMY_DATABASE_URI" ] = f"sqlite:///{app.instance_path}/db_{app.config['ENV_IDENTIFIER']}.sqlite3" - elif os.environ.get("SPIFF_DATABASE_TYPE") == "postgres": + elif app.config.get("SPIFF_DATABASE_TYPE") == "postgres": app.config[ "SQLALCHEMY_DATABASE_URI" ] = f"postgresql://spiffworkflow_backend:spiffworkflow_backend@localhost:5432/{database_name}" @@ -33,7 +33,7 @@ def setup_database_uri(app: Flask) -> None: "SQLALCHEMY_DATABASE_URI" ] = f"mysql+mysqlconnector://root:{db_pswd}@localhost/{database_name}" else: - app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get( + app.config["SQLALCHEMY_DATABASE_URI"] = app.config.get( "SPIFFWORKFLOW_BACKEND_DATABASE_URI" ) @@ -52,12 +52,6 @@ def setup_config(app: Flask) -> None: app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False app.config.from_object("spiffworkflow_backend.config.default") - # This allows config/testing.py or instance/config.py to override the default config - if "ENV_IDENTIFIER" in app.config and app.config["ENV_IDENTIFIER"] == "testing": - app.config.from_pyfile("config/testing.py", silent=True) - else: - app.config.from_pyfile(f"{app.instance_path}/config.py", silent=True) - env_config_prefix = "spiffworkflow_backend.config." env_config_module = env_config_prefix + app.config["ENV_IDENTIFIER"] try: @@ -73,8 +67,11 @@ def setup_config(app: Flask) -> None: f"Cannot find config module: {env_config_module}" ) from exception - setup_database_uri(app) - setup_logger(app) + # This allows config/testing.py or instance/config.py to override the default config + if "ENV_IDENTIFIER" in app.config and app.config["ENV_IDENTIFIER"] == "testing": + app.config.from_pyfile("config/testing.py", silent=True) + else: + app.config.from_pyfile(f"{app.instance_path}/config.py", silent=True) app.config["PERMISSIONS_FILE_FULLPATH"] = None if app.config["SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME"]: @@ -92,5 +89,8 @@ def setup_config(app: Flask) -> None: if app.config["BPMN_SPEC_ABSOLUTE_DIR"] is None: raise ConfigurationError("BPMN_SPEC_ABSOLUTE_DIR config must be set") + setup_database_uri(app) + setup_logger(app) + thread_local_data = threading.local() app.config["THREAD_LOCAL_DATA"] = thread_local_data diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/config/default.py b/spiffworkflow-backend/src/spiffworkflow_backend/config/default.py index 53d670c77..8643df768 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/config/default.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/config/default.py @@ -30,9 +30,12 @@ CONNECTOR_PROXY_URL = environ.get( GIT_COMMIT_ON_SAVE = environ.get("GIT_COMMIT_ON_SAVE", default="false") == "true" # Open ID server -OPEN_ID_SERVER_URL = environ.get("OPEN_ID_SERVER_URL", default="http://localhost:7002") +OPEN_ID_SERVER_URL = environ.get( + "OPEN_ID_SERVER_URL", default="http://localhost:7002/realms/spiffworkflow" +) +# Replace above line with this to use the built-in Open ID Server. +# OPEN_ID_SERVER_URL = environ.get("OPEN_ID_SERVER_URL", default="http://localhost:7000/openid") OPEN_ID_CLIENT_ID = environ.get("OPEN_ID_CLIENT_ID", default="spiffworkflow-backend") -OPEN_ID_REALM_NAME = environ.get("OPEN_ID_REALM_NAME", default="spiffworkflow") OPEN_ID_CLIENT_SECRET_KEY = environ.get( "OPEN_ID_CLIENT_SECRET_KEY", default="JXeQExm0JhQPLumgHtIIqf52bDalHz0q" ) # noqa: S105 @@ -57,3 +60,12 @@ SENTRY_TRACES_SAMPLE_RATE = environ.get( SPIFFWORKFLOW_BACKEND_LOG_LEVEL = environ.get( "SPIFFWORKFLOW_BACKEND_LOG_LEVEL", default="info" ) + +# Datbase Configuration +SPIFF_DATABASE_TYPE = environ.get( + "SPIFF_DATABASE_TYPE", default="mysql" +) # can also be sqlite, postgres +# Overide above with specific sqlalchymy connection string. +SPIFFWORKFLOW_BACKEND_DATABASE_URI = environ.get( + "SPIFFWORKFLOW_BACKEND_DATABASE_URI", default=None +) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/development.yml b/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/development.yml index 4c748fd9a..419c925fa 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/development.yml +++ b/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/development.yml @@ -1,5 +1,11 @@ default_group: everybody +users: + admin: + email: admin@spiffworkflow.org + password: admin + preferred_username: Admin + groups: admin: users: diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/example.yml b/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/example.yml new file mode 100644 index 000000000..79bfed81d --- /dev/null +++ b/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/example.yml @@ -0,0 +1,88 @@ +default_group: everybody + +users: + admin: + email: admin@spiffworkflow.org + password: admin + preferred_username: Admin + nelson: + email: nelson@spiffworkflow.org + password: nelson + preferred_username: Nelson + malala: + email: malala@spiffworkflow.org + password: malala + preferred_username: Malala + +groups: + admin: + users: + [ + admin, + ] + Education: + users: + [ + malala + ] + President: + users: + [ + nelson + ] + +permissions: + # Admins have access to everything. + admin: + groups: [admin] + users: [] + allowed_permissions: [create, read, update, delete] + uri: /* + + # Everybody can participate in tasks assigned to them. + tasks-crud: + groups: [everybody] + users: [] + allowed_permissions: [create, read, update, delete] + uri: /v1.0/tasks/* + + # Everyone can see everything (all groups, and processes are visible) + 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-instance: + groups: [ everybody ] + users: [ ] + allowed_permissions: [ read ] + uri: /v1.0/process-instances/* + read-process-instance-reports: + groups: [ everybody ] + users: [ ] + allowed_permissions: [ read ] + uri: /v1.0/process-instances/reports/* + processes-read: + groups: [ everybody ] + users: [ ] + allowed_permissions: [ read ] + uri: /v1.0/processes + + # Members of the Education group can change they processes work. + education-admin: + groups: ["Education", "President"] + users: [] + allowed_permissions: [create, read, update, delete] + uri: /v1.0/process-groups/education:* + + # Anyone can start an education process. + education-everybody: + groups: [everybody] + users: [] + allowed_permissions: [create, read] + uri: /v1.0/process-instances/misc:category_number_one:process-model-with-form/* diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/helpers/db_helper.py b/spiffworkflow-backend/src/spiffworkflow_backend/helpers/db_helper.py index 45cd38f7a..2ceea1f44 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/helpers/db_helper.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/helpers/db_helper.py @@ -1,8 +1,8 @@ """Db_helper.""" import time +from flask_bpmn.models.db import db import sqlalchemy -from flask_bpmn.models.db import db def try_to_connect(start_time: float) -> None: diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/active_task.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/active_task.py index ea9e10552..cf08394f4 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/models/active_task.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/active_task.py @@ -2,10 +2,10 @@ from __future__ import annotations from dataclasses import dataclass -from typing import TYPE_CHECKING - from flask_bpmn.models.db import db from flask_bpmn.models.db import SpiffworkflowBaseDBModel +from typing import TYPE_CHECKING + from sqlalchemy import ForeignKey from sqlalchemy.orm import relationship from sqlalchemy.orm import RelationshipProperty diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/active_task_user.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/active_task_user.py index f194c38e4..a14c08ce4 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/models/active_task_user.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/active_task_user.py @@ -2,9 +2,9 @@ from __future__ import annotations from dataclasses import dataclass - from flask_bpmn.models.db import db from flask_bpmn.models.db import SpiffworkflowBaseDBModel + from sqlalchemy import ForeignKey from spiffworkflow_backend.models.active_task import ActiveTaskModel diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/group.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/group.py index 3b7edd6ce..b6db3cf34 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/models/group.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/group.py @@ -1,10 +1,10 @@ """Group.""" from __future__ import annotations -from typing import TYPE_CHECKING - from flask_bpmn.models.db import db from flask_bpmn.models.group import FlaskBpmnGroupModel +from typing import TYPE_CHECKING + from sqlalchemy.orm import relationship if TYPE_CHECKING: diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/message_correlation.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/message_correlation.py index 08bc1cb12..65937a4fc 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/models/message_correlation.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/message_correlation.py @@ -1,9 +1,9 @@ """Message_correlation.""" from dataclasses import dataclass -from typing import TYPE_CHECKING - from flask_bpmn.models.db import db from flask_bpmn.models.db import SpiffworkflowBaseDBModel +from typing import TYPE_CHECKING + from sqlalchemy import ForeignKey from sqlalchemy.orm import relationship diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/message_correlation_message_instance.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/message_correlation_message_instance.py index 320dfba3e..dbbddb44f 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/models/message_correlation_message_instance.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/message_correlation_message_instance.py @@ -1,8 +1,8 @@ """Message_correlation_message_instance.""" from dataclasses import dataclass - from flask_bpmn.models.db import db from flask_bpmn.models.db import SpiffworkflowBaseDBModel + from sqlalchemy import ForeignKey from spiffworkflow_backend.models.message_correlation import MessageCorrelationModel diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/message_correlation_property.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/message_correlation_property.py index b84b7140c..c0c06925f 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/models/message_correlation_property.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/message_correlation_property.py @@ -1,6 +1,7 @@ """Message_correlation_property.""" from flask_bpmn.models.db import db from flask_bpmn.models.db import SpiffworkflowBaseDBModel + from sqlalchemy import ForeignKey from spiffworkflow_backend.models.message_model import MessageModel diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/message_instance.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/message_instance.py index 2559a6352..745d2c2d6 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/models/message_instance.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/message_instance.py @@ -1,12 +1,12 @@ """Message_instance.""" import enum from dataclasses import dataclass +from flask_bpmn.models.db import db +from flask_bpmn.models.db import SpiffworkflowBaseDBModel from typing import Any from typing import Optional from typing import TYPE_CHECKING -from flask_bpmn.models.db import db -from flask_bpmn.models.db import SpiffworkflowBaseDBModel from sqlalchemy import ForeignKey from sqlalchemy.event import listens_for from sqlalchemy.orm import relationship diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/message_triggerable_process_model.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/message_triggerable_process_model.py index cc8834654..29b742f7d 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/models/message_triggerable_process_model.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/message_triggerable_process_model.py @@ -1,6 +1,7 @@ """Message_correlation_property.""" from flask_bpmn.models.db import db from flask_bpmn.models.db import SpiffworkflowBaseDBModel + from sqlalchemy import ForeignKey from spiffworkflow_backend.models.message_model import MessageModel diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/permission_assignment.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/permission_assignment.py index 63295f74e..8aa2b5ade 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/models/permission_assignment.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/permission_assignment.py @@ -1,9 +1,9 @@ """PermissionAssignment.""" import enum -from typing import Any - from flask_bpmn.models.db import db from flask_bpmn.models.db import SpiffworkflowBaseDBModel +from typing import Any + from sqlalchemy import ForeignKey from sqlalchemy.orm import validates diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/permission_target.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/permission_target.py index 53334baf0..3b4a10553 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/models/permission_target.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/permission_target.py @@ -1,10 +1,10 @@ """PermissionTarget.""" import re from dataclasses import dataclass -from typing import Optional - from flask_bpmn.models.db import db from flask_bpmn.models.db import SpiffworkflowBaseDBModel +from typing import Optional + from sqlalchemy.orm import validates diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/principal.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/principal.py index c7efa8609..1c267052d 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/models/principal.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/principal.py @@ -1,8 +1,8 @@ """Principal.""" from dataclasses import dataclass - from flask_bpmn.models.db import db from flask_bpmn.models.db import SpiffworkflowBaseDBModel + from sqlalchemy import ForeignKey from sqlalchemy.orm import relationship from sqlalchemy.schema import CheckConstraint diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance.py index e6a5f6849..c2b003104 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance.py @@ -1,12 +1,12 @@ """Process_instance.""" from __future__ import annotations +from flask_bpmn.models.db import db +from flask_bpmn.models.db import SpiffworkflowBaseDBModel from typing import Any from typing import cast import marshmallow -from flask_bpmn.models.db import db -from flask_bpmn.models.db import SpiffworkflowBaseDBModel from marshmallow import INCLUDE from marshmallow import Schema from marshmallow_enum import EnumField # type: ignore diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance_metadata.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance_metadata.py index c9003594b..e34a56491 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance_metadata.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance_metadata.py @@ -1,8 +1,8 @@ """Spiff_step_details.""" from dataclasses import dataclass - from flask_bpmn.models.db import db from flask_bpmn.models.db import SpiffworkflowBaseDBModel + from sqlalchemy import ForeignKey from spiffworkflow_backend.models.process_instance import ProcessInstanceModel diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance_report.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance_report.py index 1f22a3830..c6da687e0 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance_report.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance_report.py @@ -2,13 +2,13 @@ from __future__ import annotations from dataclasses import dataclass +from flask_bpmn.models.db import db +from flask_bpmn.models.db import SpiffworkflowBaseDBModel from typing import Any from typing import cast from typing import Optional from typing import TypedDict -from flask_bpmn.models.db import db -from flask_bpmn.models.db import SpiffworkflowBaseDBModel from sqlalchemy import ForeignKey from sqlalchemy.orm import deferred from sqlalchemy.orm import relationship diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/refresh_token.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/refresh_token.py index 2e96b7f05..c5684f25a 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/models/refresh_token.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/refresh_token.py @@ -1,8 +1,8 @@ """Refresh_token.""" from dataclasses import dataclass - from flask_bpmn.models.db import db from flask_bpmn.models.db import SpiffworkflowBaseDBModel + from sqlalchemy import ForeignKey # from sqlalchemy.orm import relationship diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/secret_model.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/secret_model.py index 92fd470a3..1bb634cec 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/models/secret_model.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/secret_model.py @@ -1,8 +1,8 @@ """Secret_model.""" from dataclasses import dataclass - from flask_bpmn.models.db import db from flask_bpmn.models.db import SpiffworkflowBaseDBModel + from marshmallow import Schema from sqlalchemy import ForeignKey diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/spec_reference.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/spec_reference.py index 1e85f7229..ce083a483 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/models/spec_reference.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/spec_reference.py @@ -1,8 +1,8 @@ """Message_model.""" from dataclasses import dataclass - from flask_bpmn.models.db import db from flask_bpmn.models.db import SpiffworkflowBaseDBModel + from flask_marshmallow import Schema # type: ignore from marshmallow import INCLUDE from sqlalchemy import UniqueConstraint diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/spiff_logging.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/spiff_logging.py index b0b908877..e27daeb64 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/models/spiff_logging.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/spiff_logging.py @@ -1,9 +1,8 @@ """Spiff_logging.""" from dataclasses import dataclass -from typing import Optional - from flask_bpmn.models.db import db from flask_bpmn.models.db import SpiffworkflowBaseDBModel +from typing import Optional @dataclass diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/spiff_step_details.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/spiff_step_details.py index 91d70116a..62dcd5957 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/models/spiff_step_details.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/spiff_step_details.py @@ -1,9 +1,9 @@ """Spiff_step_details.""" from dataclasses import dataclass -from typing import Optional - from flask_bpmn.models.db import db from flask_bpmn.models.db import SpiffworkflowBaseDBModel +from typing import Optional + from sqlalchemy import ForeignKey from sqlalchemy.orm import deferred diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/user.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/user.py index c33a72e7a..cdd2379b7 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/models/user.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/user.py @@ -1,14 +1,14 @@ """User.""" from __future__ import annotations +from flask_bpmn.api.api_error import ApiError +from flask_bpmn.models.db import db +from flask_bpmn.models.db import SpiffworkflowBaseDBModel from typing import Any import jwt import marshmallow 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 SpiffworkflowBaseDBModel from marshmallow import Schema from sqlalchemy.orm import relationship from sqlalchemy.orm import validates @@ -30,7 +30,8 @@ class UserModel(SpiffworkflowBaseDBModel): __table_args__ = (db.UniqueConstraint("service", "service_id", name="service_key"),) id = db.Column(db.Integer, primary_key=True) - username = db.Column(db.String(255), nullable=False, unique=True) + # server and service id must be unique, not username. + username = db.Column(db.String(255), nullable=False, unique=False) uid = db.Column(db.String(50), unique=True) service = db.Column(db.String(50), nullable=False, unique=False) service_id = db.Column(db.String(255), nullable=False, unique=False) @@ -83,10 +84,6 @@ class UserModel(SpiffworkflowBaseDBModel): algorithm="HS256", ) - def is_admin(self) -> bool: - """Is_admin.""" - return True - # @classmethod # def from_open_id_user_info(cls, user_info: dict) -> Any: # """From_open_id_user_info.""" diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/user_group_assignment.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/user_group_assignment.py index fa5b620c8..548a280b9 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/models/user_group_assignment.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/user_group_assignment.py @@ -1,6 +1,7 @@ """UserGroupAssignment.""" from flask_bpmn.models.db import db from flask_bpmn.models.db import SpiffworkflowBaseDBModel + from sqlalchemy import ForeignKey from sqlalchemy.orm import relationship diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/openid_blueprint/__init__.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/openid_blueprint/__init__.py new file mode 100644 index 000000000..f520b09de --- /dev/null +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/openid_blueprint/__init__.py @@ -0,0 +1 @@ +"""__init__.""" diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/openid_blueprint/openid_blueprint.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/openid_blueprint/openid_blueprint.py new file mode 100644 index 000000000..f812ab034 --- /dev/null +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/openid_blueprint/openid_blueprint.py @@ -0,0 +1,153 @@ +"""OpenID Implementation for demos and local development. + +A very insecure and partial OpenID implementation for use in demos and testing. +Provides the bare minimum endpoints required by SpiffWorkflow to +handle openid authentication -- definitely not a production ready system. +This is just here to make local development, testing, and demonstration easier. +""" +import base64 +import time +from typing import Any +from urllib.parse import urlencode + +import jwt +import yaml +from flask import Blueprint +from flask import current_app +from flask import redirect +from flask import render_template +from flask import request +from flask import url_for +from werkzeug.wrappers import Response + +openid_blueprint = Blueprint( + "openid", __name__, template_folder="templates", static_folder="static" +) + +OPEN_ID_CODE = ":this_is_not_secure_do_not_use_in_production" + + +@openid_blueprint.route("/.well-known/openid-configuration", methods=["GET"]) +def well_known() -> dict: + """Open ID Discovery endpoint. + + These urls can be very different from one openid impl to the next, this is just a small subset. + """ + host_url = request.host_url.strip("/") + return { + "issuer": f"{host_url}/openid", + "authorization_endpoint": f"{host_url}{url_for('openid.auth')}", + "token_endpoint": f"{host_url}{url_for('openid.token')}", + "end_session_endpoint": f"{host_url}{url_for('openid.end_session')}", + } + + +@openid_blueprint.route("/auth", methods=["GET"]) +def auth() -> str: + """Accepts a series of parameters.""" + return render_template( + "login.html", + state=request.args.get("state"), + response_type=request.args.get("response_type"), + client_id=request.args.get("client_id"), + scope=request.args.get("scope"), + redirect_uri=request.args.get("redirect_uri"), + error_message=request.args.get("error_message", ""), + ) + + +@openid_blueprint.route("/form_submit", methods=["POST"]) +def form_submit() -> Any: + """Handles the login form submission.""" + users = get_users() + if ( + request.values["Uname"] in users + and request.values["Pass"] == users[request.values["Uname"]]["password"] + ): + # Redirect back to the end user with some detailed information + state = request.values.get("state") + data = { + "state": state, + "code": request.values["Uname"] + OPEN_ID_CODE, + "session_state": "", + } + url = request.values.get("redirect_uri") + "?" + urlencode(data) + return redirect(url) + else: + return render_template( + "login.html", + state=request.values.get("state"), + response_type=request.values.get("response_type"), + client_id=request.values.get("client_id"), + scope=request.values.get("scope"), + redirect_uri=request.values.get("redirect_uri"), + error_message="Login failed. Please try again.", + ) + + +@openid_blueprint.route("/token", methods=["POST"]) +def token() -> dict: + """Url that will return a valid token, given the super secret sauce.""" + request.values.get("grant_type") + code = request.values.get("code") + request.values.get("redirect_uri") + + """We just stuffed the user name on the front of the code, so grab it.""" + user_name, secret_hash = code.split(":") + user_details = get_users()[user_name] + + """Get authentication from headers.""" + authorization = request.headers.get("Authorization", "Basic ") + authorization = authorization[6:] # Remove "Basic" + authorization = base64.b64decode(authorization).decode("utf-8") + client_id, client_secret = authorization.split(":") + + base_url = request.host_url + "openid" + + id_token = jwt.encode( + { + "iss": base_url, + "aud": [client_id, "account"], + "iat": time.time(), + "exp": time.time() + 86400, # Expire after a day. + "sub": user_name, + "preferred_username": user_details.get("preferred_username", user_name), + }, + client_secret, + algorithm="HS256", + ) + response = { + "access_token": id_token, + "id_token": id_token, + "refresh_token": id_token, + } + return response + + +@openid_blueprint.route("/end_session", methods=["GET"]) +def end_session() -> Response: + """Logout.""" + redirect_url = request.args.get("post_logout_redirect_uri", "http://localhost") + request.args.get("id_token_hint") + return redirect(redirect_url) + + +@openid_blueprint.route("/refresh", methods=["POST"]) +def refresh() -> str: + """Refresh.""" + return "" + + +permission_cache = None + + +def get_users() -> Any: + """Load users from a local configuration file.""" + global permission_cache + if not permission_cache: + with open(current_app.config["PERMISSIONS_FILE_FULLPATH"]) as file: + permission_cache = yaml.safe_load(file) + if "users" in permission_cache: + return permission_cache["users"] + else: + return {} diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/openid_blueprint/static/login.css b/spiffworkflow-backend/src/spiffworkflow_backend/routes/openid_blueprint/static/login.css new file mode 100644 index 000000000..15b093f67 --- /dev/null +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/openid_blueprint/static/login.css @@ -0,0 +1,112 @@ + body{ + margin: 0; + padding: 0; + background-color:white; + font-family: 'Arial'; + } + header { + width: 100%; + background-color: black; + } + .logo_small { + padding: 5px 20px; + } + .error { + margin: 20px auto; + color: red; + font-weight: bold; + text-align: center; + } + .login{ + width: 400px; + overflow: hidden; + margin: 20px auto; + padding: 50px; + background: #fff; + border-radius: 15px ; + } + h2{ + text-align: center; + color: #277582; + padding: 20px; + } + label{ + color: #fff; + width: 200px; + display: inline-block; + } + #log { + width: 100px; + height: 50px; + border: none; + padding-left: 7px; + background-color:#202020; + color: #DDD; + text-align: left; + } + .cds--btn--primary { + background-color: #0f62fe; + border: 1px solid #0000; + color: #fff; + } + .cds--btn { + align-items: center; + border: 0; + border-radius: 0; + box-sizing: border-box; + cursor: pointer; + display: inline-flex; + flex-shrink: 0; + font-family: inherit; + font-size: 100%; + font-size: .875rem; + font-weight: 400; + justify-content: space-between; + letter-spacing: .16px; + line-height: 1.28572; + margin: 0; + max-width: 20rem; + min-height: 3rem; + outline: none; + padding: calc(0.875rem - 3px) 63px calc(0.875rem - 3px) 15px; + position: relative; + text-align: left; + text-decoration: none; + transition: background 70ms cubic-bezier(0, 0, .38, .9), box-shadow 70ms cubic-bezier(0, 0, .38, .9), border-color 70ms cubic-bezier(0, 0, .38, .9), outline 70ms cubic-bezier(0, 0, .38, .9); + vertical-align: initial; + vertical-align: top; + width: max-content; + } + .cds--btn:hover { + background-color: #0145c5; + } + .cds--btn:focus { + background-color: #01369a; + } + + .cds--text-input { + background-color: #eee; + border: none; + border-bottom: 1px solid #8d8d8d; + color: #161616; + font-family: inherit; + font-size: .875rem; + font-weight: 400; + height: 2.5rem; + letter-spacing: .16px; + line-height: 1.28572; + outline: 2px solid #0000; + outline-offset: -2px; + padding: 0 1rem; + transition: background-color 70ms cubic-bezier(.2,0,.38,.9),outline 70ms cubic-bezier(.2,0,.38,.9); + width: 100%; + } + + span{ + color: white; + font-size: 17px; + } + a{ + float: right; + background-color: grey; + } diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/openid_blueprint/static/logo.png b/spiffworkflow-backend/src/spiffworkflow_backend/routes/openid_blueprint/static/logo.png new file mode 100644 index 000000000..4cffb07fd Binary files /dev/null and b/spiffworkflow-backend/src/spiffworkflow_backend/routes/openid_blueprint/static/logo.png differ diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/openid_blueprint/static/logo_small.png b/spiffworkflow-backend/src/spiffworkflow_backend/routes/openid_blueprint/static/logo_small.png new file mode 100644 index 000000000..d0ad4499a Binary files /dev/null and b/spiffworkflow-backend/src/spiffworkflow_backend/routes/openid_blueprint/static/logo_small.png differ diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/openid_blueprint/templates/login.html b/spiffworkflow-backend/src/spiffworkflow_backend/routes/openid_blueprint/templates/login.html new file mode 100644 index 000000000..d9b8b901a --- /dev/null +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/openid_blueprint/templates/login.html @@ -0,0 +1,36 @@ + + + + Login Form + + + +
+ +
+ +

Login

+
{{error_message}}
+
+
+ +

+ +

+ + + + + + +

+ +
+
+ + diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py index 549c76f05..a241661ab 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py @@ -4,6 +4,8 @@ import random import re import string import uuid +from flask_bpmn.api.api_error import ApiError +from flask_bpmn.models.db import db from typing import Any from typing import Dict from typing import Optional @@ -22,8 +24,6 @@ from flask import make_response from flask import redirect 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 lxml import etree # type: ignore from lxml.builder import ElementMaker # type: ignore from SpiffWorkflow.task import Task as SpiffTask # type: ignore @@ -34,6 +34,7 @@ from sqlalchemy import desc from sqlalchemy import func from sqlalchemy.orm import aliased from sqlalchemy.orm import joinedload +from sqlalchemy.orm import selectinload from spiffworkflow_backend.exceptions.process_entity_not_found_error import ( ProcessEntityNotFoundError, @@ -841,7 +842,7 @@ def process_instance_list( process_instance_query = ProcessInstanceModel.query # Always join that hot user table for good performance at serialization time. process_instance_query = process_instance_query.options( - joinedload(ProcessInstanceModel.process_initiator) + selectinload(ProcessInstanceModel.process_initiator) ) if report_filter.process_model_identifier is not None: diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/user.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/user.py index 5fe10e0af..f18dff3b3 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/user.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/user.py @@ -1,6 +1,8 @@ """User.""" import ast import base64 +import json +from flask_bpmn.api.api_error import ApiError from typing import Any from typing import Dict from typing import Optional @@ -11,7 +13,6 @@ from flask import current_app from flask import g from flask import redirect from flask import request -from flask_bpmn.api.api_error import ApiError from werkzeug.wrappers import Response from spiffworkflow_backend.models.user import UserModel @@ -58,7 +59,6 @@ def verify_token( decoded_token = get_decoded_token(token) if decoded_token is not None: - if "token_type" in decoded_token: token_type = decoded_token["token_type"] if token_type == "internal": # noqa: S105 @@ -68,11 +68,11 @@ def verify_token( current_app.logger.error( f"Exception in verify_token getting user from decoded internal token. {e}" ) - elif "iss" in decoded_token.keys(): try: - user_info = AuthenticationService.get_user_info_from_open_id(token) - except ApiError as ae: + if AuthenticationService.validate_id_token(token): + user_info = decoded_token + except ApiError as ae: # API Error is only thrown in the token is outdated. # Try to refresh the token user = UserService.get_user_by_service_and_service_id( "open_id", decoded_token["sub"] @@ -86,14 +86,9 @@ def verify_token( ) ) if auth_token and "error" not in auth_token: - # redirect to original url, with auth_token? - user_info = ( - AuthenticationService.get_user_info_from_open_id( - auth_token["access_token"] - ) - ) - if not user_info: - raise ae + # We have the user, but this code is a bit convoluted, and will later demand + # a user_info object so it can look up the user. Sorry to leave this crap here. + user_info = {"sub": user.service_id} else: raise ae else: @@ -203,6 +198,18 @@ def login(redirect_url: str = "/") -> Response: return redirect(login_redirect_url) +def parse_id_token(token: str) -> Any: + """Parse the id token.""" + parts = token.split(".") + if len(parts) != 3: + raise Exception("Incorrect id token format") + + payload = parts[1] + padded = payload + "=" * (4 - len(payload) % 4) + decoded = base64.b64decode(padded) + return json.loads(decoded) + + def login_return(code: str, state: str, session_state: str) -> Optional[Response]: """Login_return.""" state_dict = ast.literal_eval(base64.b64decode(state).decode("utf-8")) @@ -211,10 +218,9 @@ def login_return(code: str, state: str, session_state: str) -> Optional[Response if "id_token" in auth_token_object: id_token = auth_token_object["id_token"] + user_info = parse_id_token(id_token) + if AuthenticationService.validate_id_token(id_token): - user_info = AuthenticationService.get_user_info_from_open_id( - auth_token_object["access_token"] - ) if user_info and "error" not in user_info: user_model = AuthorizationService.create_user_from_sign_in(user_info) g.user = user_model.id @@ -332,15 +338,11 @@ def get_user_from_decoded_internal_token(decoded_token: dict) -> Optional[UserMo .filter(UserModel.service_id == service_id) .first() ) - # user: UserModel = UserModel.query.filter() if user: return user user = UserModel( username=service_id, - uid=service_id, service=service, service_id=service_id, - name="API User", ) - return user diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/user_blueprint.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/user_blueprint.py index 29bbddcd1..d1dea8c8e 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/user_blueprint.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/user_blueprint.py @@ -1,5 +1,7 @@ """Main.""" import json +from flask_bpmn.api.api_error import ApiError +from flask_bpmn.models.db import db from typing import Any from typing import Final @@ -7,8 +9,6 @@ import flask.wrappers from flask import Blueprint from flask import request from flask import Response -from flask_bpmn.api.api_error import ApiError -from flask_bpmn.models.db import db from sqlalchemy.exc import IntegrityError from spiffworkflow_backend.models.group import GroupModel diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/scripts/get_localtime.py b/spiffworkflow-backend/src/spiffworkflow_backend/scripts/get_localtime.py index 689b86d8c..c6680c9dd 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/scripts/get_localtime.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/scripts/get_localtime.py @@ -1,9 +1,9 @@ """Get_localtime.""" from datetime import datetime +from flask_bpmn.api.api_error import ApiError from typing import Any import pytz -from flask_bpmn.api.api_error import ApiError from spiffworkflow_backend.models.script_attributes_context import ( ScriptAttributesContext, diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/scripts/save_process_instance_metadata.py b/spiffworkflow-backend/src/spiffworkflow_backend/scripts/save_process_instance_metadata.py index d9c1959aa..2f3f5eac7 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/scripts/save_process_instance_metadata.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/scripts/save_process_instance_metadata.py @@ -2,6 +2,7 @@ from typing import Any from flask_bpmn.models.db import db +from typing import Any from spiffworkflow_backend.models.process_instance_metadata import ( ProcessInstanceMetadataModel, diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/scripts/script.py b/spiffworkflow-backend/src/spiffworkflow_backend/scripts/script.py index b744694a2..00f8de792 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/scripts/script.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/scripts/script.py @@ -5,11 +5,10 @@ import importlib import os import pkgutil from abc import abstractmethod +from flask_bpmn.api.api_error import ApiError from typing import Any from typing import Callable -from flask_bpmn.api.api_error import ApiError - from spiffworkflow_backend.models.script_attributes_context import ( ScriptAttributesContext, ) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/acceptance_test_fixtures.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/acceptance_test_fixtures.py index 81488910e..81cb2b3fa 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/acceptance_test_fixtures.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/acceptance_test_fixtures.py @@ -1,8 +1,8 @@ """Acceptance_test_fixtures.""" import time +from flask_bpmn.models.db import db from flask import current_app -from flask_bpmn.models.db import db from tests.spiffworkflow_backend.helpers.base_test import BaseTest from spiffworkflow_backend.models.process_instance import ProcessInstanceModel diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/authentication_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/authentication_service.py index 3868adf65..4981c3156 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/authentication_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/authentication_service.py @@ -3,14 +3,14 @@ import base64 import enum import json import time +from flask_bpmn.api.api_error import ApiError +from flask_bpmn.models.db import db from typing import Optional import jwt import requests from flask import current_app from flask import redirect -from flask_bpmn.api.api_error import ApiError -from flask_bpmn.models.db import db from werkzeug.wrappers import Response from spiffworkflow_backend.models.refresh_token import RefreshTokenModel @@ -26,58 +26,35 @@ class AuthenticationProviderTypes(enum.Enum): class AuthenticationService: """AuthenticationService.""" + ENDPOINT_CACHE: dict = ( + {} + ) # We only need to find the openid endpoints once, then we can cache them. + @staticmethod - def get_open_id_args() -> tuple: - """Get_open_id_args.""" - open_id_server_url = current_app.config["OPEN_ID_SERVER_URL"] - open_id_client_id = current_app.config["OPEN_ID_CLIENT_ID"] - open_id_realm_name = current_app.config["OPEN_ID_REALM_NAME"] - open_id_client_secret_key = current_app.config[ - "OPEN_ID_CLIENT_SECRET_KEY" - ] # noqa: S105 - return ( - open_id_server_url, - open_id_client_id, - open_id_realm_name, - open_id_client_secret_key, - ) + def client_id() -> str: + """Returns the client id from the config.""" + return current_app.config.get("OPEN_ID_CLIENT_ID", "") + + @staticmethod + def server_url() -> str: + """Returns the server url from the config.""" + return current_app.config.get("OPEN_ID_SERVER_URL", "") + + @staticmethod + def secret_key() -> str: + """Returns the secret key from the config.""" + return current_app.config.get("OPEN_ID_CLIENT_SECRET_KEY", "") @classmethod - def get_user_info_from_open_id(cls, token: str) -> dict: - """The token is an auth_token.""" - ( - open_id_server_url, - open_id_client_id, - open_id_realm_name, - open_id_client_secret_key, - ) = cls.get_open_id_args() - - headers = {"Authorization": f"Bearer {token}"} - - request_url = f"{open_id_server_url}/realms/{open_id_realm_name}/protocol/openid-connect/userinfo" - try: - request_response = requests.get(request_url, headers=headers) - except Exception as e: - current_app.logger.error(f"Exception in get_user_info_from_id_token: {e}") - raise ApiError( - error_code="token_error", - message=f"Exception in get_user_info_from_id_token: {e}", - status_code=401, - ) from e - - if request_response.status_code == 401: - raise ApiError( - error_code="invalid_token", message="Please login", status_code=401 - ) - elif request_response.status_code == 200: - user_info: dict = json.loads(request_response.text) - return user_info - - raise ApiError( - error_code="user_info_error", - message="Cannot get user info in get_user_info_from_id_token", - status_code=401, - ) + def open_id_endpoint_for_name(cls, name: str) -> str: + """All openid systems provide a mapping of static names to the full path of that endpoint.""" + if name not in AuthenticationService.ENDPOINT_CACHE: + request_url = f"{cls.server_url()}/.well-known/openid-configuration" + response = requests.get(request_url) + AuthenticationService.ENDPOINT_CACHE = response.json() + if name not in AuthenticationService.ENDPOINT_CACHE: + raise Exception(f"Unknown OpenID Endpoint: {name}") + return AuthenticationService.ENDPOINT_CACHE.get(name, "") @staticmethod def get_backend_url() -> str: @@ -87,17 +64,10 @@ class AuthenticationService: def logout(self, id_token: str, redirect_url: Optional[str] = None) -> Response: """Logout.""" if redirect_url is None: - redirect_url = "/" - return_redirect_url = f"{self.get_backend_url()}/v1.0/logout_return" - ( - open_id_server_url, - open_id_client_id, - open_id_realm_name, - open_id_client_secret_key, - ) = AuthenticationService.get_open_id_args() + redirect_url = f"{self.get_backend_url()}/v1.0/logout_return" request_url = ( - f"{open_id_server_url}/realms/{open_id_realm_name}/protocol/openid-connect/logout?" - + f"post_logout_redirect_uri={return_redirect_url}&" + self.open_id_endpoint_for_name("end_session_endpoint") + + f"?post_logout_redirect_uri={redirect_url}&" + f"id_token_hint={id_token}" ) @@ -113,18 +83,12 @@ class AuthenticationService: self, state: str, redirect_url: str = "/v1.0/login_return" ) -> str: """Get_login_redirect_url.""" - ( - open_id_server_url, - open_id_client_id, - open_id_realm_name, - open_id_client_secret_key, - ) = AuthenticationService.get_open_id_args() return_redirect_url = f"{self.get_backend_url()}{redirect_url}" login_redirect_url = ( - f"{open_id_server_url}/realms/{open_id_realm_name}/protocol/openid-connect/auth?" - + f"state={state}&" + self.open_id_endpoint_for_name("authorization_endpoint") + + f"?state={state}&" + "response_type=code&" - + f"client_id={open_id_client_id}&" + + f"client_id={self.client_id()}&" + "scope=openid&" + f"redirect_uri={return_redirect_url}" ) @@ -134,14 +98,7 @@ class AuthenticationService: self, code: str, redirect_url: str = "/v1.0/login_return" ) -> dict: """Get_auth_token_object.""" - ( - open_id_server_url, - open_id_client_id, - open_id_realm_name, - open_id_client_secret_key, - ) = AuthenticationService.get_open_id_args() - - backend_basic_auth_string = f"{open_id_client_id}:{open_id_client_secret_key}" + backend_basic_auth_string = f"{self.client_id()}:{self.secret_key()}" backend_basic_auth_bytes = bytes(backend_basic_auth_string, encoding="ascii") backend_basic_auth = base64.b64encode(backend_basic_auth_bytes) headers = { @@ -154,7 +111,7 @@ class AuthenticationService: "redirect_uri": f"{self.get_backend_url()}{redirect_url}", } - request_url = f"{open_id_server_url}/realms/{open_id_realm_name}/protocol/openid-connect/token" + request_url = self.open_id_endpoint_for_name("token_endpoint") response = requests.post(request_url, data=data, headers=headers) auth_token_object: dict = json.loads(response.text) @@ -165,12 +122,6 @@ class AuthenticationService: """Https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation.""" valid = True now = time.time() - ( - open_id_server_url, - open_id_client_id, - open_id_realm_name, - open_id_client_secret_key, - ) = cls.get_open_id_args() try: decoded_token = jwt.decode(id_token, options={"verify_signature": False}) except Exception as e: @@ -179,15 +130,15 @@ class AuthenticationService: message="Cannot decode id_token", status_code=401, ) from e - if decoded_token["iss"] != f"{open_id_server_url}/realms/{open_id_realm_name}": + if decoded_token["iss"] != cls.server_url(): valid = False elif ( - open_id_client_id not in decoded_token["aud"] + cls.client_id() not in decoded_token["aud"] and "account" not in decoded_token["aud"] ): valid = False elif "azp" in decoded_token and decoded_token["azp"] not in ( - open_id_client_id, + cls.client_id(), "account", ): valid = False @@ -241,15 +192,8 @@ class AuthenticationService: @classmethod def get_auth_token_from_refresh_token(cls, refresh_token: str) -> dict: - """Get a new auth_token from a refresh_token.""" - ( - open_id_server_url, - open_id_client_id, - open_id_realm_name, - open_id_client_secret_key, - ) = cls.get_open_id_args() - - backend_basic_auth_string = f"{open_id_client_id}:{open_id_client_secret_key}" + """Converts a refresh token to an Auth Token by calling the openid's auth endpoint.""" + backend_basic_auth_string = f"{cls.client_id()}:{cls.secret_key()}" backend_basic_auth_bytes = bytes(backend_basic_auth_string, encoding="ascii") backend_basic_auth = base64.b64encode(backend_basic_auth_bytes) headers = { @@ -260,11 +204,11 @@ class AuthenticationService: data = { "grant_type": "refresh_token", "refresh_token": refresh_token, - "client_id": open_id_client_id, - "client_secret": open_id_client_secret_key, + "client_id": cls.client_id(), + "client_secret": cls.secret_key(), } - request_url = f"{open_id_server_url}/realms/{open_id_realm_name}/protocol/openid-connect/token" + request_url = cls.open_id_endpoint_for_name("token_endpoint") response = requests.post(request_url, data=data, headers=headers) auth_token_object: dict = json.loads(response.text) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py index ea488f7a9..5b0d0573b 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py @@ -1,5 +1,8 @@ """Authorization_service.""" +import inspect import re +from flask_bpmn.api.api_error import ApiError +from flask_bpmn.models.db import db from typing import Optional from typing import Union @@ -8,8 +11,7 @@ import yaml from flask import current_app from flask import g from flask import request -from flask_bpmn.api.api_error import ApiError -from flask_bpmn.models.db import db +from flask import scaffold from SpiffWorkflow.task import Task as SpiffTask # type: ignore from sqlalchemy import or_ from sqlalchemy import text @@ -23,6 +25,7 @@ from spiffworkflow_backend.models.principal import PrincipalModel from spiffworkflow_backend.models.user import UserModel from spiffworkflow_backend.models.user import UserNotFoundError from spiffworkflow_backend.models.user_group_assignment import UserGroupAssignmentModel +from spiffworkflow_backend.routes.openid_blueprint import openid_blueprint from spiffworkflow_backend.services.group_service import GroupService from spiffworkflow_backend.services.user_service import UserService @@ -241,6 +244,7 @@ class AuthorizationService: return True api_view_function = current_app.view_functions[request.endpoint] + module = inspect.getmodule(api_view_function) if ( api_view_function and api_view_function.__name__.startswith("login") @@ -248,6 +252,8 @@ class AuthorizationService: or api_view_function.__name__.startswith("console_ui_") or api_view_function.__name__ in authentication_exclusion_list or api_view_function.__name__ in swagger_functions + or module == openid_blueprint + or module == scaffold # don't check permissions for static assets ): return True diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/data_setup_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/data_setup_service.py index c9c0647ed..b0179079e 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/data_setup_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/data_setup_service.py @@ -1,7 +1,8 @@ """Data_setup_service.""" -from flask import current_app from flask_bpmn.models.db import db +from flask import current_app + from spiffworkflow_backend.services.process_model_service import ProcessModelService from spiffworkflow_backend.services.spec_file_service import SpecFileService diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/error_handling_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/error_handling_service.py index 1e8b38f2d..a5be1b8e3 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/error_handling_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/error_handling_service.py @@ -1,11 +1,10 @@ """Error_handling_service.""" +from flask_bpmn.api.api_error import ApiError +from flask_bpmn.models.db import db from typing import Any from typing import List from typing import Union -from flask_bpmn.api.api_error import ApiError -from flask_bpmn.models.db import db - from spiffworkflow_backend.models.process_instance import ProcessInstanceModel from spiffworkflow_backend.models.process_instance import ProcessInstanceStatus from spiffworkflow_backend.services.email_service import EmailService diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/file_system_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/file_system_service.py index 5812748c7..29f118b63 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/file_system_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/file_system_service.py @@ -1,12 +1,12 @@ """File_system_service.""" import os from datetime import datetime +from flask_bpmn.api.api_error import ApiError from typing import List from typing import Optional import pytz from flask import current_app -from flask_bpmn.api.api_error import ApiError from spiffworkflow_backend.models.file import CONTENT_TYPES from spiffworkflow_backend.models.file import File diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/group_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/group_service.py index aa560009e..edc0e69a4 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/group_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/group_service.py @@ -1,7 +1,6 @@ """Group_service.""" -from typing import Optional - from flask_bpmn.models.db import db +from typing import Optional from spiffworkflow_backend.models.group import GroupModel from spiffworkflow_backend.services.user_service import UserService diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/logging_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/logging_service.py index dd34cb3fd..fe56e104f 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/logging_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/logging_service.py @@ -2,12 +2,12 @@ import json import logging import re +from flask_bpmn.models.db import db from typing import Any from typing import Optional from flask import g from flask.app import Flask -from flask_bpmn.models.db import db from spiffworkflow_backend.models.spiff_logging import SpiffLoggingModel diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/message_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/message_service.py index cfb42c836..8e8f7a728 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/message_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/message_service.py @@ -1,8 +1,8 @@ """Message_service.""" +from flask_bpmn.models.db import db from typing import Any from typing import Optional -from flask_bpmn.models.db import db from sqlalchemy import and_ from sqlalchemy import or_ from sqlalchemy import select diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py index bdf717406..1be46f1b8 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py @@ -8,6 +8,8 @@ import re import time from datetime import datetime from datetime import timedelta +from flask_bpmn.api.api_error import ApiError +from flask_bpmn.models.db import db from typing import Any from typing import Callable from typing import Dict @@ -21,8 +23,6 @@ from typing import Union import dateparser import pytz from flask import current_app -from flask_bpmn.api.api_error import ApiError -from flask_bpmn.models.db import db from lxml import etree # type: ignore from RestrictedPython import safe_globals # type: ignore from SpiffWorkflow.bpmn.exceptions import WorkflowTaskExecException # type: ignore @@ -100,6 +100,7 @@ from spiffworkflow_backend.services.service_task_service import ServiceTaskDeleg from spiffworkflow_backend.services.spec_file_service import SpecFileService from spiffworkflow_backend.services.user_service import UserService + # Sorry about all this crap. I wanted to move this thing to another file, but # importing a bunch of types causes circular imports. @@ -191,11 +192,11 @@ class CustomBpmnScriptEngine(PythonScriptEngine): # type: ignore return self._evaluate(expression, task.data, task, external_methods) def _evaluate( - self, - expression: str, - context: Dict[str, Union[Box, str]], - task: Optional[SpiffTask] = None, - external_methods: Optional[Dict[str, Any]] = None, + self, + expression: str, + context: Dict[str, Union[Box, str]], + task: Optional[SpiffTask] = None, + external_methods: Optional[Dict[str, Any]] = None, ) -> Any: """_evaluate.""" methods = self.__get_augment_methods(task) @@ -219,7 +220,7 @@ class CustomBpmnScriptEngine(PythonScriptEngine): # type: ignore ) from exception def execute( - self, task: SpiffTask, script: str, external_methods: Any = None + self, task: SpiffTask, script: str, external_methods: Any = None ) -> None: """Execute.""" try: @@ -233,10 +234,10 @@ class CustomBpmnScriptEngine(PythonScriptEngine): # type: ignore raise WorkflowTaskExecException(task, f" {script}, {e}", e) from e def call_service( - self, - operation_name: str, - operation_params: Dict[str, Any], - task_data: Dict[str, Any], + self, + operation_name: str, + operation_params: Dict[str, Any], + task_data: Dict[str, Any], ) -> Any: """CallService.""" return ServiceTaskDelegate.call_connector( @@ -283,7 +284,7 @@ class ProcessInstanceProcessor: # * get_spec, which returns a spec and any subprocesses (as IdToBpmnProcessSpecMapping dict) # * __get_bpmn_process_instance, which takes spec and subprocesses and instantiates and returns a BpmnWorkflow def __init__( - self, process_instance_model: ProcessInstanceModel, validate_only: bool = False + self, process_instance_model: ProcessInstanceModel, validate_only: bool = False ) -> None: """Create a Workflow Processor based on the serialized information available in the process_instance model.""" tld = current_app.config["THREAD_LOCAL_DATA"] @@ -310,7 +311,7 @@ class ProcessInstanceProcessor: ) else: bpmn_json_length = len(process_instance_model.bpmn_json.encode("utf-8")) - megabyte = float(1024**2) + megabyte = float(1024 ** 2) json_size = bpmn_json_length / megabyte if json_size > 1: wf_json = json.loads(process_instance_model.bpmn_json) @@ -324,22 +325,22 @@ class ProcessInstanceProcessor: len(json.dumps(test_spec).encode("utf-8")) / megabyte ) message = ( - "Workflow " - + process_instance_model.process_model_identifier - + f" JSON Size is over 1MB:{json_size:.2f} MB" + "Workflow " + + process_instance_model.process_model_identifier + + f" JSON Size is over 1MB:{json_size:.2f} MB" ) message += f"\n Task Size: {task_size}" message += f"\n Spec Size: {spec_size}" current_app.logger.warning(message) def check_sub_specs( - test_spec: dict, indent: int = 0, show_all: bool = False + test_spec: dict, indent: int = 0, show_all: bool = False ) -> None: """Check_sub_specs.""" for my_spec_name in test_spec["task_specs"]: my_spec = test_spec["task_specs"][my_spec_name] my_spec_size = ( - len(json.dumps(my_spec).encode("utf-8")) / megabyte + len(json.dumps(my_spec).encode("utf-8")) / megabyte ) if my_spec_size > 0.1 or show_all: current_app.logger.warning( @@ -375,13 +376,13 @@ class ProcessInstanceProcessor: raise ApiError( error_code="unexpected_process_instance_structure", message="Failed to deserialize process_instance" - " '%s' due to a mis-placed or missing task '%s'" - % (self.process_model_identifier, str(ke)), + " '%s' due to a mis-placed or missing task '%s'" + % (self.process_model_identifier, str(ke)), ) from ke @classmethod def get_process_model_and_subprocesses( - cls, process_model_identifier: str + cls, process_model_identifier: str ) -> Tuple[BpmnProcessSpec, IdToBpmnProcessSpecMapping]: """Get_process_model_and_subprocesses.""" process_model_info = ProcessModelService.get_process_model( @@ -399,7 +400,7 @@ class ProcessInstanceProcessor: @classmethod def get_bpmn_process_instance_from_process_model( - cls, process_model_identifier: str + cls, process_model_identifier: str ) -> BpmnWorkflow: """Get_all_bpmn_process_identifiers_for_process_model.""" (bpmn_process_spec, subprocesses) = cls.get_process_model_and_subprocesses( @@ -424,7 +425,7 @@ class ProcessInstanceProcessor: return current_user def add_user_info_to_process_instance( - self, bpmn_process_instance: BpmnWorkflow + self, bpmn_process_instance: BpmnWorkflow ) -> None: """Add_user_info_to_process_instance.""" current_user = self.current_user() @@ -437,8 +438,8 @@ class ProcessInstanceProcessor: @staticmethod def get_bpmn_process_instance_from_workflow_spec( - spec: BpmnProcessSpec, - subprocesses: Optional[IdToBpmnProcessSpecMapping] = None, + spec: BpmnProcessSpec, + subprocesses: Optional[IdToBpmnProcessSpecMapping] = None, ) -> BpmnWorkflow: """Get_bpmn_process_instance_from_workflow_spec.""" return BpmnWorkflow( @@ -449,10 +450,10 @@ class ProcessInstanceProcessor: @staticmethod def __get_bpmn_process_instance( - process_instance_model: ProcessInstanceModel, - spec: Optional[BpmnProcessSpec] = None, - validate_only: bool = False, - subprocesses: Optional[IdToBpmnProcessSpecMapping] = None, + process_instance_model: ProcessInstanceModel, + spec: Optional[BpmnProcessSpec] = None, + validate_only: bool = False, + subprocesses: Optional[IdToBpmnProcessSpecMapping] = None, ) -> BpmnWorkflow: """__get_bpmn_process_instance.""" if process_instance_model.bpmn_json: @@ -495,14 +496,14 @@ class ProcessInstanceProcessor: self.save() def raise_if_no_potential_owners( - self, potential_owner_ids: list[int], message: str + self, potential_owner_ids: list[int], message: str ) -> None: """Raise_if_no_potential_owners.""" if not potential_owner_ids: raise (NoPotentialOwnersForTaskError(message)) def get_potential_owner_ids_from_task( - self, task: SpiffTask + self, task: SpiffTask ) -> PotentialOwnerIdList: """Get_potential_owner_ids_from_task.""" task_spec = task.task_spec @@ -706,7 +707,7 @@ class ProcessInstanceProcessor: @staticmethod def backfill_missing_spec_reference_records( - bpmn_process_identifier: str, + bpmn_process_identifier: str, ) -> Optional[str]: """Backfill_missing_spec_reference_records.""" process_models = ProcessModelService.get_process_models(recursive=True) @@ -727,7 +728,7 @@ class ProcessInstanceProcessor: @staticmethod def bpmn_file_full_path_from_bpmn_process_identifier( - bpmn_process_identifier: str, + bpmn_process_identifier: str, ) -> str: """Bpmn_file_full_path_from_bpmn_process_identifier.""" if bpmn_process_identifier is None: @@ -737,8 +738,8 @@ class ProcessInstanceProcessor: spec_reference = ( SpecReferenceCache.query.filter_by(identifier=bpmn_process_identifier) - .filter_by(type="process") - .first() + .filter_by(type="process") + .first() ) bpmn_file_full_path = None if spec_reference is None: @@ -757,15 +758,15 @@ class ProcessInstanceProcessor: ApiError( error_code="could_not_find_bpmn_process_identifier", message="Could not find the the given bpmn process identifier from any sources: %s" - % bpmn_process_identifier, + % bpmn_process_identifier, ) ) return os.path.abspath(bpmn_file_full_path) @staticmethod def update_spiff_parser_with_all_process_dependency_files( - parser: BpmnDmnParser, - processed_identifiers: Optional[set[str]] = None, + parser: BpmnDmnParser, + processed_identifiers: Optional[set[str]] = None, ) -> None: """Update_spiff_parser_with_all_process_dependency_files.""" if processed_identifiers is None: @@ -803,7 +804,7 @@ class ProcessInstanceProcessor: @staticmethod def get_spec( - files: List[File], process_model_info: ProcessModelInfo + files: List[File], process_model_info: ProcessModelInfo ) -> Tuple[BpmnProcessSpec, IdToBpmnProcessSpecMapping]: """Returns a SpiffWorkflow specification for the given process_instance spec, using the files provided.""" parser = ProcessInstanceProcessor.get_parser() @@ -817,14 +818,14 @@ class ProcessInstanceProcessor: dmn: etree.Element = etree.fromstring(data) parser.add_dmn_xml(dmn, filename=file.name) if ( - process_model_info.primary_process_id is None - or process_model_info.primary_process_id == "" + process_model_info.primary_process_id is None + or process_model_info.primary_process_id == "" ): raise ( ApiError( error_code="no_primary_bpmn_error", message="There is no primary BPMN process id defined for process_model %s" - % process_model_info.id, + % process_model_info.id, ) ) ProcessInstanceProcessor.update_spiff_parser_with_all_process_dependency_files( @@ -842,7 +843,7 @@ class ProcessInstanceProcessor: raise ApiError( error_code="process_instance_validation_error", message="Failed to parse the Workflow Specification. " - + "Error is '%s.'" % str(ve), + + "Error is '%s.'" % str(ve), file_name=ve.filename, task_id=ve.id, tag=ve.tag, @@ -889,12 +890,12 @@ class ProcessInstanceProcessor: message_correlations = [] for ( - message_correlation_key, - message_correlation_properties, + message_correlation_key, + message_correlation_properties, ) in bpmn_message.correlations.items(): for ( - message_correlation_property_identifier, - message_correlation_property_value, + message_correlation_property_identifier, + message_correlation_property_value, ) in message_correlation_properties.items(): message_correlation_property = ( MessageCorrelationPropertyModel.query.filter_by( @@ -986,7 +987,7 @@ class ProcessInstanceProcessor: db.session.add(message_instance) for ( - spiff_correlation_property + spiff_correlation_property ) in waiting_task.task_spec.event_definition.correlation_properties: # NOTE: we may have to cycle through keys here # not sure yet if it's valid for a property to be associated with multiple keys @@ -996,9 +997,9 @@ class ProcessInstanceProcessor: process_instance_id=self.process_instance_model.id, name=correlation_key_name, ) - .join(MessageCorrelationPropertyModel) - .filter_by(identifier=spiff_correlation_property.name) - .first() + .join(MessageCorrelationPropertyModel) + .filter_by(identifier=spiff_correlation_property.name) + .first() ) message_correlation_message_instance = ( MessageCorrelationMessageInstanceModel( @@ -1102,12 +1103,12 @@ class ProcessInstanceProcessor: endtasks = [] if self.bpmn_process_instance.is_completed(): for task in SpiffTask.Iterator( - self.bpmn_process_instance.task_tree, TaskState.ANY_MASK + self.bpmn_process_instance.task_tree, TaskState.ANY_MASK ): # Assure that we find the end event for this process_instance, and not for any sub-process_instances. if ( - isinstance(task.task_spec, EndEvent) - and task.workflow == self.bpmn_process_instance + isinstance(task.task_spec, EndEvent) + and task.workflow == self.bpmn_process_instance ): endtasks.append(task) if len(endtasks) > 0: @@ -1143,8 +1144,8 @@ class ProcessInstanceProcessor: return task for task in ready_tasks: if ( - self.bpmn_process_instance.last_task - and task.parent == last_user_task.parent + self.bpmn_process_instance.last_task + and task.parent == last_user_task.parent ): return task @@ -1154,7 +1155,7 @@ class ProcessInstanceProcessor: # and return that next_task = None for task in SpiffTask.Iterator( - self.bpmn_process_instance.task_tree, TaskState.NOT_FINISHED_MASK + self.bpmn_process_instance.task_tree, TaskState.NOT_FINISHED_MASK ): next_task = task return next_task @@ -1238,7 +1239,7 @@ class ProcessInstanceProcessor: t for t in all_tasks if not self.bpmn_process_instance._is_engine_task(t.task_spec) - and t.state in [TaskState.COMPLETED, TaskState.CANCELLED] + and t.state in [TaskState.COMPLETED, TaskState.CANCELLED] ] def get_all_waiting_tasks(self) -> list[SpiffTask]: @@ -1253,7 +1254,7 @@ class ProcessInstanceProcessor: @classmethod def get_task_by_bpmn_identifier( - cls, bpmn_task_identifier: str, bpmn_process_instance: BpmnWorkflow + cls, bpmn_task_identifier: str, bpmn_process_instance: BpmnWorkflow ) -> Optional[SpiffTask]: """Get_task_by_id.""" all_tasks = bpmn_process_instance.get_tasks(TaskState.ANY_MASK) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_service.py index 46bd252b9..826019a97 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_service.py @@ -1,11 +1,11 @@ """Process_instance_service.""" import time +from flask_bpmn.api.api_error import ApiError +from flask_bpmn.models.db import db from typing import Any from typing import List from flask import current_app -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_backend.models.active_task import ActiveTaskModel diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_service.py index f009af688..9a88cb56a 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_service.py @@ -2,14 +2,13 @@ import json import os import shutil +from flask_bpmn.api.api_error import ApiError from glob import glob from typing import Any from typing import List from typing import Optional from typing import TypeVar -from flask_bpmn.api.api_error import ApiError - from spiffworkflow_backend.exceptions.process_entity_not_found_error import ( ProcessEntityNotFoundError, ) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/secret_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/secret_service.py index e4dee4913..2362a1a5d 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/secret_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/secret_service.py @@ -1,8 +1,7 @@ """Secret_service.""" -from typing import Optional - from flask_bpmn.api.api_error import ApiError from flask_bpmn.models.db import db +from typing import Optional from spiffworkflow_backend.models.secret_model import SecretModel diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/spec_file_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/spec_file_service.py index c69f41c30..8057a7e14 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/spec_file_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/spec_file_service.py @@ -2,10 +2,10 @@ import os import shutil from datetime import datetime +from flask_bpmn.models.db import db from typing import List from typing import Optional -from flask_bpmn.models.db import db from SpiffWorkflow.bpmn.parser.ValidationException import ValidationException # type: ignore from spiffworkflow_backend.models.file import File diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/user_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/user_service.py index 0e8e65c2c..009d0531b 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/user_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/user_service.py @@ -1,11 +1,11 @@ """User_service.""" +from flask_bpmn.api.api_error import ApiError +from flask_bpmn.models.db import db from typing import Any from typing import Optional from flask import current_app from flask import g -from flask_bpmn.api.api_error import ApiError -from flask_bpmn.models.db import db from spiffworkflow_backend.models.active_task import ActiveTaskModel from spiffworkflow_backend.models.active_task_user import ActiveTaskUserModel diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/helpers/base_test.py b/spiffworkflow-backend/tests/spiffworkflow_backend/helpers/base_test.py index 48982fc60..f3746e1fc 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/helpers/base_test.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/helpers/base_test.py @@ -3,14 +3,14 @@ import io import json import os import time +from flask_bpmn.api.api_error import ApiError +from flask_bpmn.models.db import db from typing import Any from typing import Dict from typing import Optional from flask import current_app from flask.testing import FlaskClient -from flask_bpmn.api.api_error import ApiError -from flask_bpmn.models.db import db from tests.spiffworkflow_backend.helpers.test_data import load_test_spec from werkzeug.test import TestResponse # type: ignore diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_openid_blueprint.py b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_openid_blueprint.py new file mode 100644 index 000000000..20a0bb67b --- /dev/null +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_openid_blueprint.py @@ -0,0 +1,61 @@ +"""Test_authentication.""" +from flask import Flask +from flask.testing import FlaskClient +from tests.spiffworkflow_backend.helpers.base_test import BaseTest + + +class TestFlaskOpenId(BaseTest): + """An integrated Open ID that responds to openID requests. + + By referencing a build in YAML file. Useful for + local development, testing, demos etc... + """ + + def test_discovery_of_endpoints( + self, + app: Flask, + client: FlaskClient, + with_db_and_bpmn_file_cleanup: None, + ) -> None: + """Test discovery endpoints.""" + response = client.get("/openid/.well-known/openid-configuration") + discovered_urls = response.json + assert "http://localhost/openid" == discovered_urls["issuer"] + assert ( + "http://localhost/openid/auth" == discovered_urls["authorization_endpoint"] + ) + assert "http://localhost/openid/token" == discovered_urls["token_endpoint"] + + def test_get_login_page( + self, + app: Flask, + client: FlaskClient, + with_db_and_bpmn_file_cleanup: None, + ) -> None: + """It should be possible to get to a login page.""" + data = {"state": {"bubblegum": 1, "daydream": 2}} + response = client.get("/openid/auth", query_string=data) + assert b"

Login

" in response.data + assert b"bubblegum" in response.data + + def test_get_token( + self, + app: Flask, + client: FlaskClient, + with_db_and_bpmn_file_cleanup: None, + ) -> None: + """It should be possible to get a token.""" + code = ( + "c3BpZmZ3b3JrZmxvdy1iYWNrZW5kOkpYZVFFeG0wSmhRUEx1bWdIdElJcWY1MmJEYWxIejBx" + ) + headers = { + "Content-Type": "application/x-www-form-urlencoded", + "Authorization": f"Basic {code}", + } + data = { + "grant_type": "authorization_code", + "code": code, + "redirect_url": "http://localhost:7000/v1.0/login_return", + } + response = client.post("/openid/token", data=data, headers=headers) + assert response diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py index 9d719a233..9a87dea88 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py @@ -3,12 +3,12 @@ import io import json import os import time +from flask_bpmn.models.db import db from typing import Any import pytest from flask.app import Flask from flask.testing import FlaskClient -from flask_bpmn.models.db import db from tests.spiffworkflow_backend.helpers.base_test import BaseTest from tests.spiffworkflow_backend.helpers.test_data import load_test_spec diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_secret_service.py b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_secret_service.py index c71f67f23..9d064173f 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_secret_service.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_secret_service.py @@ -1,11 +1,11 @@ """Test_secret_service.""" import json +from flask_bpmn.api.api_error import ApiError from typing import Optional import pytest from flask.app import Flask from flask.testing import FlaskClient -from flask_bpmn.api.api_error import ApiError from tests.spiffworkflow_backend.helpers.base_test import BaseTest from werkzeug.test import TestResponse # type: ignore diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/scripts/test_get_group_members.py b/spiffworkflow-backend/tests/spiffworkflow_backend/scripts/test_get_group_members.py index 8a6046b5b..a2e9f5ef8 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/scripts/test_get_group_members.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/scripts/test_get_group_members.py @@ -1,7 +1,8 @@ """Test_get_localtime.""" +from flask_bpmn.models.db import db + from flask.app import Flask from flask.testing import FlaskClient -from flask_bpmn.models.db import db from tests.spiffworkflow_backend.helpers.base_test import BaseTest from tests.spiffworkflow_backend.helpers.test_data import load_test_spec diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_message_instance.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_message_instance.py index 2c091eeb1..a28034144 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_message_instance.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_message_instance.py @@ -1,8 +1,9 @@ """Test_message_instance.""" +from flask_bpmn.models.db import db + import pytest from flask import Flask from flask.testing import FlaskClient -from flask_bpmn.models.db import db from tests.spiffworkflow_backend.helpers.base_test import BaseTest from spiffworkflow_backend.models.message_instance import MessageInstanceModel diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_permission_target.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_permission_target.py index 567681428..f0f693f09 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_permission_target.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_permission_target.py @@ -1,7 +1,8 @@ """Process Model.""" +from flask_bpmn.models.db import db + import pytest from flask.app import Flask -from flask_bpmn.models.db import db from tests.spiffworkflow_backend.helpers.base_test import BaseTest from spiffworkflow_backend.models.permission_target import ( diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_permissions.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_permissions.py index b66f32370..92f5c5002 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_permissions.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_permissions.py @@ -1,7 +1,8 @@ """Test Permissions.""" +from flask_bpmn.models.db import db + from flask.app import Flask from flask.testing import FlaskClient -from flask_bpmn.models.db import db from tests.spiffworkflow_backend.helpers.base_test import BaseTest from tests.spiffworkflow_backend.helpers.test_data import load_test_spec diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model.py index 9eb6901b9..087eee63f 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model.py @@ -1,7 +1,8 @@ """Process Model.""" +from flask_bpmn.models.db import db + from flask.app import Flask from flask.testing import FlaskClient -from flask_bpmn.models.db import db from tests.spiffworkflow_backend.helpers.base_test import BaseTest from tests.spiffworkflow_backend.helpers.test_data import load_test_spec diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_restricted_script_engine.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_restricted_script_engine.py index d31ea424f..ae77a1fc6 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_restricted_script_engine.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_restricted_script_engine.py @@ -1,8 +1,9 @@ """Test_various_bpmn_constructs.""" +from flask_bpmn.api.api_error import ApiError + import pytest from flask.app import Flask from flask.testing import FlaskClient -from flask_bpmn.api.api_error import ApiError from tests.spiffworkflow_backend.helpers.base_test import BaseTest from tests.spiffworkflow_backend.helpers.test_data import load_test_spec diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_spec_file_service.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_spec_file_service.py index 3cc353b52..5d67d2b22 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_spec_file_service.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_spec_file_service.py @@ -1,10 +1,10 @@ """Test_message_service.""" import os +from flask_bpmn.models.db import db import pytest from flask import Flask from flask.testing import FlaskClient -from flask_bpmn.models.db import db from SpiffWorkflow.bpmn.parser.ValidationException import ValidationException # type: ignore from tests.spiffworkflow_backend.helpers.base_test import BaseTest from tests.spiffworkflow_backend.helpers.test_data import load_test_spec diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_spiff_logging.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_spiff_logging.py index d8680b719..15a892c06 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_spiff_logging.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_spiff_logging.py @@ -1,8 +1,8 @@ """Process Model.""" from decimal import Decimal +from flask_bpmn.models.db import db from flask.app import Flask -from flask_bpmn.models.db import db from tests.spiffworkflow_backend.helpers.base_test import BaseTest from tests.spiffworkflow_backend.helpers.test_data import load_test_spec