Merge pull request #66 from sartography/feature/create_containers

A Demo Docker-Compose for All
This commit is contained in:
Dan Funk 2022-12-06 18:12:45 -05:00 committed by GitHub
commit b27a27d86d
79 changed files with 1007 additions and 369 deletions

117
.github/workflows/release_builds.yml vendored Normal file
View File

@ -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 }}

View File

@ -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

View File

@ -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

View File

@ -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"},

View File

@ -5,14 +5,14 @@ description = "An example showing how to use the Spiffworkflow-proxy's Flask Blu
authors = ["Dan <dan@sartography.com>"]
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",
"."
]

83
docker-compose.yml Normal file
View File

@ -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

View File

@ -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"

View File

@ -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()

83
poetry.lock generated
View File

@ -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"},

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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),

View File

@ -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

View File

@ -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

View File

@ -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
)

View File

@ -1,5 +1,11 @@
default_group: everybody
users:
admin:
email: admin@spiffworkflow.org
password: admin
preferred_username: Admin
groups:
admin:
users:

View File

@ -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/*

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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."""

View File

@ -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

View File

@ -0,0 +1 @@
"""__init__."""

View File

@ -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 {}

View File

@ -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;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@ -0,0 +1,36 @@
<!DOCTYPE html>
<html>
<head>
<title>Login Form</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('openid.static', filename='login.css') }}">
</head>
<body>
<header>
<img class="logo_small" src="{{ url_for('openid.static', filename='logo_small.png') }}"/>
</header>
<h2>Login</h2>
<div class="error">{{error_message}}</div>
<div class="login">
<form id="login" method="post" action="{{ url_for('openid.form_submit') }}">
<input type="text" class="cds--text-input" name="Uname" id="Uname" placeholder="Username">
<br><br>
<input type="Password" class="cds--text-input" name="Pass" id="Pass" placeholder="Password">
<br><br>
<input type="hidden" name="state" value="{{state}}"/>
<input type="hidden" name="response_type" value="{{response_type}}"/>
<input type="hidden" name="client_id" value="{{client_id}}"/>
<input type="hidden" name="scope" value="{{scope}}"/>
<input type="hidden" name="redirect_uri" value="{{redirect_uri}}"/>
<input type="submit" name="log" class="cds--btn cds--btn--primary" value="Log In">
<br><br>
<!-- should maybe add this stuff in eventually, but this is just for testing.
<input type="checkbox" id="check">
<span>Remember me</span>
<br><br>
Forgot <a href="#">Password</a>
-->
</form>
</div>
</body>
</html>

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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,

View File

@ -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,
)

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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,
)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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"<h2>Login</h2>" 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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 (

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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