Merge branch 'main' into feature/edit-task-data

This commit is contained in:
mike cullerton 2022-10-25 11:52:17 -04:00
commit 02fe909691
33 changed files with 941 additions and 468 deletions

View File

@ -217,7 +217,7 @@ jobs:
nox --version
- name: Download coverage data
uses: actions/download-artifact@v3.0.0
uses: actions/download-artifact@v3.0.1
with:
name: coverage-data

2
.gitignore vendored
View File

@ -18,4 +18,4 @@ node_modules
/tests/spiffworkflow_backend/files
/bin/import_secrets.py
/src/spiffworkflow_backend/config/secrets.py
_null-ls_*
*null-ls_*

View File

@ -5,10 +5,10 @@
"defaultSignatureAlgorithm": "RS256",
"revokeRefreshToken": false,
"refreshTokenMaxReuse": 0,
"accessTokenLifespan": 86400,
"accessTokenLifespan": 1800,
"accessTokenLifespanForImplicitFlow": 900,
"ssoSessionIdleTimeout": 1800,
"ssoSessionMaxLifespan": 36000,
"ssoSessionIdleTimeout": 86400,
"ssoSessionMaxLifespan": 864000,
"ssoSessionIdleTimeoutRememberMe": 0,
"ssoSessionMaxLifespanRememberMe": 0,
"offlineSessionIdleTimeout": 2592000,
@ -942,6 +942,7 @@
"saml.force.post.binding": "false",
"saml.multivalued.roles": "false",
"frontchannel.logout.session.required": "false",
"post.logout.redirect.uris": "+",
"oauth2.device.authorization.grant.enabled": "false",
"backchannel.logout.revoke.offline.tokens": "false",
"saml.server.signature.keyinfo.ext": "false",
@ -1007,6 +1008,7 @@
"saml.force.post.binding": "false",
"saml.multivalued.roles": "false",
"frontchannel.logout.session.required": "false",
"post.logout.redirect.uris": "+",
"oauth2.device.authorization.grant.enabled": "false",
"backchannel.logout.revoke.offline.tokens": "false",
"saml.server.signature.keyinfo.ext": "false",
@ -1072,6 +1074,7 @@
"saml.force.post.binding": "false",
"saml.multivalued.roles": "false",
"frontchannel.logout.session.required": "false",
"post.logout.redirect.uris": "+",
"oauth2.device.authorization.grant.enabled": "false",
"backchannel.logout.revoke.offline.tokens": "false",
"saml.server.signature.keyinfo.ext": "false",
@ -1137,6 +1140,7 @@
"saml.force.post.binding": "false",
"saml.multivalued.roles": "false",
"frontchannel.logout.session.required": "false",
"post.logout.redirect.uris": "+",
"oauth2.device.authorization.grant.enabled": "false",
"backchannel.logout.revoke.offline.tokens": "false",
"saml.server.signature.keyinfo.ext": "false",
@ -1204,6 +1208,7 @@
"saml.force.post.binding": "false",
"saml.multivalued.roles": "false",
"frontchannel.logout.session.required": "false",
"post.logout.redirect.uris": "+",
"oauth2.device.authorization.grant.enabled": "false",
"backchannel.logout.revoke.offline.tokens": "false",
"saml.server.signature.keyinfo.ext": "false",
@ -1293,6 +1298,7 @@
"saml.force.post.binding": "false",
"saml.multivalued.roles": "false",
"frontchannel.logout.session.required": "false",
"post.logout.redirect.uris": "+",
"oauth2.device.authorization.grant.enabled": "false",
"backchannel.logout.revoke.offline.tokens": "false",
"saml.server.signature.keyinfo.ext": "false",
@ -1563,6 +1569,7 @@
"saml.force.post.binding": "false",
"saml.multivalued.roles": "false",
"frontchannel.logout.session.required": "false",
"post.logout.redirect.uris": "+",
"oauth2.device.authorization.grant.enabled": "false",
"backchannel.logout.revoke.offline.tokens": "false",
"saml.server.signature.keyinfo.ext": "false",
@ -1634,6 +1641,7 @@
"saml.force.post.binding": "false",
"saml.multivalued.roles": "false",
"frontchannel.logout.session.required": "false",
"post.logout.redirect.uris": "+",
"oauth2.device.authorization.grant.enabled": "false",
"backchannel.logout.revoke.offline.tokens": "false",
"saml.server.signature.keyinfo.ext": "false",
@ -2327,14 +2335,14 @@
"subComponents": {},
"config": {
"allowed-protocol-mapper-types": [
"oidc-usermodel-property-mapper",
"oidc-full-name-mapper",
"oidc-usermodel-attribute-mapper",
"oidc-usermodel-property-mapper",
"oidc-sha256-pairwise-sub-mapper",
"saml-user-property-mapper",
"saml-role-list-mapper",
"saml-user-attribute-mapper",
"oidc-usermodel-attribute-mapper",
"oidc-address-mapper",
"oidc-sha256-pairwise-sub-mapper"
"saml-role-list-mapper",
"saml-user-attribute-mapper"
]
}
},
@ -2356,14 +2364,14 @@
"subComponents": {},
"config": {
"allowed-protocol-mapper-types": [
"saml-role-list-mapper",
"oidc-full-name-mapper",
"oidc-usermodel-attribute-mapper",
"oidc-address-mapper",
"saml-user-attribute-mapper",
"oidc-sha256-pairwise-sub-mapper",
"saml-user-property-mapper",
"oidc-usermodel-property-mapper"
"oidc-usermodel-attribute-mapper",
"saml-role-list-mapper",
"oidc-sha256-pairwise-sub-mapper",
"oidc-usermodel-property-mapper",
"saml-user-attribute-mapper",
"oidc-address-mapper"
]
}
},
@ -2406,7 +2414,7 @@
],
"org.keycloak.userprofile.UserProfileProvider": [
{
"id": "320029d9-7878-445e-8da9-cf418dbbfc73",
"id": "576f8c6a-00e6-45dd-a63d-614100fb2cc4",
"providerId": "declarative-user-profile",
"subComponents": {},
"config": {}
@ -2477,7 +2485,7 @@
"supportedLocales": [],
"authenticationFlows": [
{
"id": "3ec26fff-71d4-4b11-a747-f06f13423195",
"id": "ff21c216-5ea8-4d26-95ca-2b467a9d5059",
"alias": "Account verification options",
"description": "Method with which to verity the existing account",
"providerId": "basic-flow",
@ -2503,7 +2511,7 @@
]
},
{
"id": "639c5cc5-30c2-4d3f-a089-fa64cc5e7107",
"id": "256108f7-b791-4e54-b4cb-a551afdf870a",
"alias": "Authentication Options",
"description": "Authentication options.",
"providerId": "basic-flow",
@ -2537,7 +2545,7 @@
]
},
{
"id": "32e28313-f365-4ebf-a323-2ea44de185ae",
"id": "fa9b2739-d814-4f83-805f-2ab0f5692cc8",
"alias": "Browser - Conditional OTP",
"description": "Flow to determine if the OTP is required for the authentication",
"providerId": "basic-flow",
@ -2563,7 +2571,7 @@
]
},
{
"id": "bd58057b-475e-4ac3-891a-1673f732afcb",
"id": "76819f1b-04b8-412e-933c-3e30b48f350b",
"alias": "Direct Grant - Conditional OTP",
"description": "Flow to determine if the OTP is required for the authentication",
"providerId": "basic-flow",
@ -2589,7 +2597,7 @@
]
},
{
"id": "4e042249-48ca-4634-814b-22c8eb85cb7b",
"id": "54f89ad2-b2b2-4554-8528-04a8b4e73e68",
"alias": "First broker login - Conditional OTP",
"description": "Flow to determine if the OTP is required for the authentication",
"providerId": "basic-flow",
@ -2615,7 +2623,7 @@
]
},
{
"id": "862d0cc1-2c80-4e8b-90ac-32988d4ba8b3",
"id": "08664454-8aa7-4f07-990b-9b59ddd19a26",
"alias": "Handle Existing Account",
"description": "Handle what to do if there is existing account with same email/username like authenticated identity provider",
"providerId": "basic-flow",
@ -2641,7 +2649,7 @@
]
},
{
"id": "efec0d38-6dfd-4f1a-bddc-56a99e772052",
"id": "29af9cfb-11d1-4781-aee3-844b436d4c08",
"alias": "Reset - Conditional OTP",
"description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.",
"providerId": "basic-flow",
@ -2667,7 +2675,7 @@
]
},
{
"id": "fc35195a-7cf8-45ed-a6db-66c862ea55e2",
"id": "2c2d44f6-115e-420e-bc86-1d58914b16ac",
"alias": "User creation or linking",
"description": "Flow for the existing/non-existing user alternatives",
"providerId": "basic-flow",
@ -2694,7 +2702,7 @@
]
},
{
"id": "7be21a14-c03b-45d0-8539-790549d2a620",
"id": "050e3be8-d313-49ec-a891-fa84592c6cc4",
"alias": "Verify Existing Account by Re-authentication",
"description": "Reauthentication of existing account",
"providerId": "basic-flow",
@ -2720,7 +2728,7 @@
]
},
{
"id": "e05cd6b8-cbbb-46ca-a7b7-c3792705da0b",
"id": "d04138a1-dfa4-4854-a59e-b7f4693b56e6",
"alias": "browser",
"description": "browser based authentication",
"providerId": "basic-flow",
@ -2762,7 +2770,7 @@
]
},
{
"id": "c8b4ddcd-fc90-4492-a436-9453765ea05f",
"id": "998cd89f-b1da-4101-9c75-658998ad3503",
"alias": "clients",
"description": "Base authentication for clients",
"providerId": "client-flow",
@ -2804,7 +2812,7 @@
]
},
{
"id": "eb2f7103-73c9-4916-a612-e0aad579e6a7",
"id": "e75753f0-6cd8-4fe5-88d5-55affdbbc5d1",
"alias": "direct grant",
"description": "OpenID Connect Resource Owner Grant",
"providerId": "basic-flow",
@ -2838,7 +2846,7 @@
]
},
{
"id": "773ea3a2-2401-4147-b64b-001bd1f5f6c5",
"id": "3854b6cc-eb08-473b-95f8-71eaab9219de",
"alias": "docker auth",
"description": "Used by Docker clients to authenticate against the IDP",
"providerId": "basic-flow",
@ -2856,7 +2864,7 @@
]
},
{
"id": "2f834413-ed70-40f5-82bd-bcea67a1121d",
"id": "a52f25a7-8509-468c-925c-4bb02e8ccd8e",
"alias": "first broker login",
"description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account",
"providerId": "basic-flow",
@ -2883,7 +2891,7 @@
]
},
{
"id": "593b072d-c66c-41f4-9fe0-37ba45acc6ee",
"id": "cc9b12fa-7f7d-44ef-aa11-d7e374b2ec0d",
"alias": "forms",
"description": "Username, password, otp and other auth forms.",
"providerId": "basic-flow",
@ -2909,7 +2917,7 @@
]
},
{
"id": "8d932a3a-62cd-4aac-94cc-082196eb5a95",
"id": "289ec9b7-c2b8-4222-922a-81be4450ac2e",
"alias": "http challenge",
"description": "An authentication flow based on challenge-response HTTP Authentication Schemes",
"providerId": "basic-flow",
@ -2935,7 +2943,7 @@
]
},
{
"id": "2a34b84c-93e7-466a-986a-e5a7a8cad061",
"id": "295c9bc2-0252-4fd3-b7da-47d4d2f0a09b",
"alias": "registration",
"description": "registration flow",
"providerId": "basic-flow",
@ -2954,7 +2962,7 @@
]
},
{
"id": "b601070a-b986-482d-8649-9df8feff3bf3",
"id": "260f9fad-5f32-4507-9e39-6e46bc26e74e",
"alias": "registration form",
"description": "registration form",
"providerId": "form-flow",
@ -2996,7 +3004,7 @@
]
},
{
"id": "7b1d2327-8429-4584-b6cf-35bfc17bdc8f",
"id": "39ef84e4-b7a0-434d-ba2a-5869b78e7aa0",
"alias": "reset credentials",
"description": "Reset credentials for a user if they forgot their password or something",
"providerId": "basic-flow",
@ -3038,7 +3046,7 @@
]
},
{
"id": "3325ebbb-617c-4917-ab4e-e5f25642536c",
"id": "e47473b7-22e0-4bd0-a253-60300aadd9b9",
"alias": "saml ecp",
"description": "SAML ECP Profile Authentication Flow",
"providerId": "basic-flow",
@ -3058,14 +3066,14 @@
],
"authenticatorConfig": [
{
"id": "33b05ac0-d30b-43d8-9ec4-08b79939a561",
"id": "a85a0c1d-f3a2-4183-862e-394a22f12c28",
"alias": "create unique user config",
"config": {
"require.password.update.after.registration": "false"
}
},
{
"id": "032891cb-dbd8-4035-a3a9-9c24f644247f",
"id": "9167b412-f119-4f29-8b38-211437556f63",
"alias": "review profile config",
"config": {
"update.profile.on.first.login": "missing"
@ -3145,18 +3153,22 @@
"dockerAuthenticationFlow": "docker auth",
"attributes": {
"cibaBackchannelTokenDeliveryMode": "poll",
"cibaExpiresIn": "120",
"cibaAuthRequestedUserHint": "login_hint",
"oauth2DeviceCodeLifespan": "600",
"clientOfflineSessionMaxLifespan": "0",
"oauth2DevicePollingInterval": "5",
"clientSessionIdleTimeout": "0",
"parRequestUriLifespan": "60",
"clientSessionMaxLifespan": "0",
"actionTokenGeneratedByUserLifespan-execute-actions": "",
"actionTokenGeneratedByUserLifespan-verify-email": "",
"clientOfflineSessionIdleTimeout": "0",
"cibaInterval": "5"
"actionTokenGeneratedByUserLifespan-reset-credentials": "",
"cibaInterval": "5",
"cibaExpiresIn": "120",
"oauth2DeviceCodeLifespan": "600",
"actionTokenGeneratedByUserLifespan-idp-verify-account-via-email": "",
"parRequestUriLifespan": "60",
"clientSessionMaxLifespan": "0"
},
"keycloakVersion": "18.0.2",
"keycloakVersion": "19.0.3",
"userManagedAccessAllowed": false,
"clientProfiles": {
"profiles": []

View File

@ -1,10 +1,18 @@
#!/usr/bin/env bash
function setup_traps() {
trap 'error_handler ${LINENO} $?' ERR
}
function remove_traps() {
trap - ERR
}
function error_handler() {
>&2 echo "Exited with BAD EXIT CODE '${2}' in ${0} script at line: ${1}."
exit "$2"
}
trap 'error_handler ${LINENO} $?' ERR
setup_traps
set -o errtrace -o errexit -o nounset -o pipefail
if ! docker network inspect spiffworkflow > /dev/null 2>&1; then
@ -19,14 +27,22 @@ docker run \
-e KEYCLOAK_LOGLEVEL=ALL \
-e ROOT_LOGLEVEL=ALL \
-e KEYCLOAK_ADMIN=admin \
-e KEYCLOAK_ADMIN_PASSWORD=admin quay.io/keycloak/keycloak:18.0.2 start-dev \
-e KEYCLOAK_ADMIN_PASSWORD=admin quay.io/keycloak/keycloak:19.0.3 start-dev \
-Dkeycloak.profile.feature.token_exchange=enabled \
-Dkeycloak.profile.feature.admin_fine_grained_authz=enabled
docker cp bin/spiffworkflow-realm.json keycloak:/tmp
sleep 10
docker exec keycloak /opt/keycloak/bin/kc.sh import --file /tmp/spiffworkflow-realm.json || echo ''
sleep 20
remove_traps
set +e
import_output=$(docker exec keycloak /opt/keycloak/bin/kc.sh import --file /tmp/spiffworkflow-realm.json 2>&1)
setup_traps
set -e
if ! grep -qE "Import finished successfully" <<<"$import_output"; then
echo -e "FAILED: $import_output"
exit 1
fi
echo 'imported realms'

View File

@ -9,6 +9,7 @@ from flask_bpmn.models.db import SpiffworkflowBaseDBModel
from tests.spiffworkflow_backend.helpers.base_test import BaseTest
from tests.spiffworkflow_backend.helpers.test_data import load_test_spec
from spiffworkflow_backend.models.active_task_user import ActiveTaskUserModel
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
from spiffworkflow_backend.models.user import UserModel
from spiffworkflow_backend.services.process_instance_processor import (
@ -56,6 +57,8 @@ def app() -> Flask:
@pytest.fixture()
def with_db_and_bpmn_file_cleanup() -> None:
"""Process_group_resource."""
db.session.query(ActiveTaskUserModel).delete()
for model in SpiffworkflowBaseDBModel._all_subclasses():
db.session.query(model).delete()
db.session.commit()

View File

@ -1,3 +1,3 @@
furo==2022.9.29
sphinx==5.2.3
sphinx==5.3.0
sphinx-click==4.3.0

View File

@ -1,11 +1,11 @@
FROM quay.io/keycloak/keycloak:18.0.2 as builder
FROM quay.io/keycloak/keycloak:19.0.3 as builder
ENV KEYCLOAK_LOGLEVEL="ALL"
ENV ROOT_LOGLEVEL="ALL"
ENV KC_HEALTH_ENABLED="true"
# ENV KC_METRICS_ENABLED=true
ENV PROXY_ADDRESS_FORWARDING="true"
ENV KC_HOSTNAME="keycloak.demo.spiffworkflow.org"
# ENV KC_HOSTNAME="keycloak.demo.spiffworkflow.org"
ENV KC_HOSTNAME_URL="https://keycloak.demo.spiffworkflow.org"
ENV KC_FEATURES="token-exchange,admin-fine-grained-authz"
# ENV KC_DB=postgres
@ -13,7 +13,7 @@ ENV KC_FEATURES="token-exchange,admin-fine-grained-authz"
# RUN curl -sL https://github.com/aerogear/keycloak-metrics-spi/releases/download/2.5.3/keycloak-metrics-spi-2.5.3.jar -o /opt/keycloak/providers/keycloak-metrics-spi-2.5.3.jar
RUN /opt/keycloak/bin/kc.sh build
FROM quay.io/keycloak/keycloak:18.0.2
FROM quay.io/keycloak/keycloak:19.0.3
COPY --from=builder /opt/keycloak/ /opt/keycloak/
WORKDIR /opt/keycloak
# for demonstration purposes only, please make sure to use proper certificates in production instead

View File

@ -1,8 +1,8 @@
"""empty message
Revision ID: e12e98d4e7e4
Revision ID: 4ba2ed52a63a
Revises:
Create Date: 2022-10-21 08:53:52.815491
Create Date: 2022-10-21 09:31:30.520942
"""
from alembic import op
@ -10,7 +10,7 @@ import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'e12e98d4e7e4'
revision = '4ba2ed52a63a'
down_revision = None
branch_labels = None
depends_on = None
@ -165,7 +165,8 @@ def upgrade():
op.create_table('active_task',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('process_instance_id', sa.Integer(), nullable=False),
sa.Column('assigned_principal_id', sa.Integer(), nullable=True),
sa.Column('actual_owner_id', sa.Integer(), nullable=True),
sa.Column('lane_assignment_id', sa.Integer(), nullable=True),
sa.Column('form_file_name', sa.String(length=50), nullable=True),
sa.Column('ui_form_file_name', sa.String(length=50), nullable=True),
sa.Column('updated_at_in_seconds', sa.Integer(), nullable=True),
@ -176,7 +177,8 @@ def upgrade():
sa.Column('task_type', sa.String(length=50), nullable=True),
sa.Column('task_status', sa.String(length=50), nullable=True),
sa.Column('process_model_display_name', sa.String(length=255), nullable=True),
sa.ForeignKeyConstraint(['assigned_principal_id'], ['principal.id'], ),
sa.ForeignKeyConstraint(['actual_owner_id'], ['user.id'], ),
sa.ForeignKeyConstraint(['lane_assignment_id'], ['group.id'], ),
sa.ForeignKeyConstraint(['process_instance_id'], ['process_instance.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('task_id', 'process_instance_id', name='active_task_unique')
@ -279,6 +281,17 @@ def upgrade():
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('active_task_user',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('active_task_id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['active_task_id'], ['active_task.id'], ),
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('active_task_id', 'user_id', name='active_task_user_unique')
)
op.create_index(op.f('ix_active_task_user_active_task_id'), 'active_task_user', ['active_task_id'], unique=False)
op.create_index(op.f('ix_active_task_user_user_id'), 'active_task_user', ['user_id'], unique=False)
op.create_table('data_store',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('updated_at_in_seconds', sa.Integer(), nullable=True),
@ -312,6 +325,9 @@ def downgrade():
op.drop_index(op.f('ix_message_correlation_message_instance_message_correlation_id'), table_name='message_correlation_message_instance')
op.drop_table('message_correlation_message_instance')
op.drop_table('data_store')
op.drop_index(op.f('ix_active_task_user_user_id'), table_name='active_task_user')
op.drop_index(op.f('ix_active_task_user_active_task_id'), table_name='active_task_user')
op.drop_table('active_task_user')
op.drop_table('task_event')
op.drop_table('spiff_logging')
op.drop_table('permission_assignment')

164
poetry.lock generated
View File

@ -95,7 +95,7 @@ python-versions = ">=3.5"
dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"]
docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"]
tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"]
tests_no_zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"]
tests-no-zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"]
[[package]]
name = "Babel"
@ -268,7 +268,7 @@ optional = false
python-versions = ">=3.6.0"
[package.extras]
unicode_backport = ["unicodedata2"]
unicode-backport = ["unicodedata2"]
[[package]]
name = "classify-imports"
@ -515,18 +515,18 @@ pycodestyle = "*"
[[package]]
name = "flake8-bugbear"
version = "22.9.23"
version = "22.10.25"
description = "A plugin for flake8 finding likely bugs and design problems in your program. Contains warnings that don't belong in pyflakes and pycodestyle."
category = "dev"
optional = false
python-versions = ">=3.6"
python-versions = ">=3.7"
[package.dependencies]
attrs = ">=19.2.0"
flake8 = ">=3.0.0"
[package.extras]
dev = ["coverage", "hypothesis", "hypothesmith (>=0.2)", "pre-commit"]
dev = ["coverage", "hypothesis", "hypothesmith (>=0.2)", "pre-commit", "tox"]
[[package]]
name = "flake8-docstrings"
@ -631,7 +631,7 @@ flask-marshmallow = "*"
flask-migrate = "*"
flask-restful = "*"
sentry-sdk = "*"
sphinx-autoapi = "^1.9.0"
sphinx-autoapi = "^2.0.0"
spiffworkflow = "*"
werkzeug = "*"
@ -639,7 +639,7 @@ werkzeug = "*"
type = "git"
url = "https://github.com/sartography/flask-bpmn"
reference = "main"
resolved_reference = "c8fd01df47518749a074772fec383256c482139f"
resolved_reference = "aae17b7b4152fe09d4a365ac5ec85289218c663b"
[[package]]
name = "Flask-Cors"
@ -1293,16 +1293,16 @@ python-versions = ">=3.6"
plugins = ["importlib-metadata"]
[[package]]
name = "PyJWT"
version = "2.5.0"
name = "pyjwt"
version = "2.6.0"
description = "JSON Web Token implementation in Python"
category = "main"
optional = false
python-versions = ">=3.7"
[package.extras]
crypto = ["cryptography (>=3.3.1)", "types-cryptography (>=3.3.21)"]
dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.3.1)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "types-cryptography (>=3.3.21)", "zope.interface"]
crypto = ["cryptography (>=3.4.0)"]
dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"]
docs = ["sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"]
tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"]
@ -1460,14 +1460,14 @@ tzdata = {version = "*", markers = "python_version >= \"3.6\""}
[[package]]
name = "pyupgrade"
version = "2.38.4"
version = "3.1.0"
description = "A tool to automatically upgrade syntax for newer versions."
category = "dev"
optional = false
python-versions = ">=3.7"
[package.dependencies]
tokenize-rt = "<5"
tokenize-rt = ">=3.2.0"
[[package]]
name = "PyYAML"
@ -1487,7 +1487,7 @@ python-versions = ">=3.6"
[[package]]
name = "reorder-python-imports"
version = "3.8.5"
version = "3.9.0"
description = "Tool for reordering python imports"
category = "dev"
optional = false
@ -1512,7 +1512,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-toolbelt"
@ -1604,7 +1604,7 @@ gitlab = ["python-gitlab (>=1.3.0)"]
[[package]]
name = "sentry-sdk"
version = "1.9.10"
version = "1.10.1"
description = "Python client for Sentry (https://sentry.io)"
category = "main"
optional = false
@ -1625,7 +1625,7 @@ falcon = ["falcon (>=1.4)"]
fastapi = ["fastapi (>=0.79.0)"]
flask = ["blinker (>=1.1)", "flask (>=0.11)"]
httpx = ["httpx (>=0.16.0)"]
pure_eval = ["asttokens", "executing", "pure-eval"]
pure-eval = ["asttokens", "executing", "pure-eval"]
pyspark = ["pyspark (>=2.4.4)"]
quart = ["blinker (>=1.1)", "quart (>=0.16.1)"]
rq = ["rq (>=0.6)"]
@ -1647,6 +1647,14 @@ docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-g
testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mock", "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-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"
description = "Simple, fast, extensible JSON encoder/decoder for Python"
category = "main"
optional = false
python-versions = ">=2.5, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]]
name = "six"
version = "1.16.0"
@ -1713,7 +1721,7 @@ test = ["cython", "html5lib", "pytest (>=4.6)", "typed_ast"]
[[package]]
name = "sphinx-autoapi"
version = "1.9.0"
version = "2.0.0"
description = "Sphinx API documentation generator"
category = "main"
optional = false
@ -1723,7 +1731,7 @@ python-versions = ">=3.7"
astroid = ">=2.7"
Jinja2 = "*"
PyYAML = "*"
sphinx = ">=3.0"
sphinx = ">=4.0"
unidecode = "*"
[package.extras]
@ -1883,19 +1891,19 @@ aiomysql = ["aiomysql", "greenlet (!=0.4.17)"]
aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing_extensions (!=3.10.0.1)"]
asyncio = ["greenlet (!=0.4.17)"]
asyncmy = ["asyncmy (>=0.2.3,!=0.2.4)", "greenlet (!=0.4.17)"]
mariadb_connector = ["mariadb (>=1.0.1,!=1.1.2)"]
mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2)"]
mssql = ["pyodbc"]
mssql_pymssql = ["pymssql"]
mssql_pyodbc = ["pyodbc"]
mssql-pymssql = ["pymssql"]
mssql-pyodbc = ["pyodbc"]
mypy = ["mypy (>=0.910)", "sqlalchemy2-stubs"]
mysql = ["mysqlclient (>=1.4.0)", "mysqlclient (>=1.4.0,<2)"]
mysql_connector = ["mysql-connector-python"]
mysql-connector = ["mysql-connector-python"]
oracle = ["cx_oracle (>=7)", "cx_oracle (>=7,<8)"]
postgresql = ["psycopg2 (>=2.7)"]
postgresql_asyncpg = ["asyncpg", "greenlet (!=0.4.17)"]
postgresql_pg8000 = ["pg8000 (>=1.16.6,!=1.29.0)"]
postgresql_psycopg2binary = ["psycopg2-binary"]
postgresql_psycopg2cffi = ["psycopg2cffi"]
postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"]
postgresql-pg8000 = ["pg8000 (>=1.16.6,!=1.29.0)"]
postgresql-psycopg2binary = ["psycopg2-binary"]
postgresql-psycopg2cffi = ["psycopg2cffi"]
pymysql = ["pymysql", "pymysql (<1)"]
sqlcipher = ["sqlcipher3_binary"]
@ -2240,7 +2248,7 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools"
[metadata]
lock-version = "1.1"
python-versions = ">=3.9,<3.11"
content-hash = "29b4ba7f1afdf87ad0a336216ef71d2e2659cd1bd3baecb009efdaac2937737a"
content-hash = "bfb51ebc4ef76d4a74f670f44dc4d7ca7e91874b096f56521c2776f1837f6a63"
[metadata.files]
alabaster = [
@ -2477,8 +2485,8 @@ flake8-bandit = [
{file = "flake8_bandit-2.1.2.tar.gz", hash = "sha256:687fc8da2e4a239b206af2e54a90093572a60d0954f3054e23690739b0b0de3b"},
]
flake8-bugbear = [
{file = "flake8-bugbear-22.9.23.tar.gz", hash = "sha256:17b9623325e6e0dcdcc80ed9e4aa811287fcc81d7e03313b8736ea5733759937"},
{file = "flake8_bugbear-22.9.23-py3-none-any.whl", hash = "sha256:cd2779b2b7ada212d7a322814a1e5651f1868ab0d3f24cc9da66169ab8fda474"},
{file = "flake8-bugbear-22.10.25.tar.gz", hash = "sha256:89e51284eb929fbb7f23fbd428491e7427f7cdc8b45a77248daffe86a039d696"},
{file = "flake8_bugbear-22.10.25-py3-none-any.whl", hash = "sha256:584631b608dc0d7d3f9201046d5840a45502da4732d5e8df6c7ac1694a91cb9e"},
]
flake8-docstrings = [
{file = "flake8-docstrings-1.6.0.tar.gz", hash = "sha256:9fe7c6a306064af8e62a055c2f61e9eb1da55f84bb39caef2b84ce53708ac34b"},
@ -3043,18 +3051,7 @@ py = [
{file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"},
]
pyasn1 = [
{file = "pyasn1-0.4.8-py2.4.egg", hash = "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"},
{file = "pyasn1-0.4.8-py2.5.egg", hash = "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf"},
{file = "pyasn1-0.4.8-py2.6.egg", hash = "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00"},
{file = "pyasn1-0.4.8-py2.7.egg", hash = "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8"},
{file = "pyasn1-0.4.8-py2.py3-none-any.whl", hash = "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d"},
{file = "pyasn1-0.4.8-py3.1.egg", hash = "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86"},
{file = "pyasn1-0.4.8-py3.2.egg", hash = "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7"},
{file = "pyasn1-0.4.8-py3.3.egg", hash = "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576"},
{file = "pyasn1-0.4.8-py3.4.egg", hash = "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12"},
{file = "pyasn1-0.4.8-py3.5.egg", hash = "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2"},
{file = "pyasn1-0.4.8-py3.6.egg", hash = "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359"},
{file = "pyasn1-0.4.8-py3.7.egg", hash = "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776"},
{file = "pyasn1-0.4.8.tar.gz", hash = "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"},
]
pycodestyle = [
@ -3073,9 +3070,9 @@ Pygments = [
{file = "Pygments-2.13.0-py3-none-any.whl", hash = "sha256:f643f331ab57ba3c9d89212ee4a2dabc6e94f117cf4eefde99a0574720d14c42"},
{file = "Pygments-2.13.0.tar.gz", hash = "sha256:56a8508ae95f98e2b9bdf93a6be5ae3f7d8af858b43e02c5a2ff083726be40c1"},
]
PyJWT = [
{file = "PyJWT-2.5.0-py3-none-any.whl", hash = "sha256:8d82e7087868e94dd8d7d418e5088ce64f7daab4b36db654cbaedb46f9d1ca80"},
{file = "PyJWT-2.5.0.tar.gz", hash = "sha256:e77ab89480905d86998442ac5788f35333fa85f65047a534adc38edf3c88fc3b"},
pyjwt = [
{file = "PyJWT-2.6.0-py3-none-any.whl", hash = "sha256:d83c3d892a77bbb74d3e1a2cfa90afaadb60945205d1095d9221f04466f64c14"},
{file = "PyJWT-2.6.0.tar.gz", hash = "sha256:69285c7e31fc44f68a1feb309e948e0df53259d579295e6cfe2b1792329f05fd"},
]
pyparsing = [
{file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"},
@ -3141,8 +3138,8 @@ pytz-deprecation-shim = [
{file = "pytz_deprecation_shim-0.1.0.post0.tar.gz", hash = "sha256:af097bae1b616dde5c5744441e2ddc69e74dfdcb0c263129610d85b87445a59d"},
]
pyupgrade = [
{file = "pyupgrade-2.38.4-py2.py3-none-any.whl", hash = "sha256:944ff993c396ddc2b9012eb3de4cda138eb4c149b22c6c560d4c8bfd0e180982"},
{file = "pyupgrade-2.38.4.tar.gz", hash = "sha256:1eb43a49f416752929741ba4d706bf3f33593d3cac9bdc217fc1ef55c047c1f4"},
{file = "pyupgrade-3.1.0-py2.py3-none-any.whl", hash = "sha256:77c6101a710be3e24804891e43388cedbee617258e93b09c8c5e58de08617758"},
{file = "pyupgrade-3.1.0.tar.gz", hash = "sha256:7a8d393d85e15e0e2753e90b7b2e173b9d29dfd71e61f93d93e985b242627ed3"},
]
PyYAML = [
{file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"},
@ -3263,8 +3260,8 @@ regex = [
{file = "regex-2022.3.2.tar.gz", hash = "sha256:79e5af1ff258bc0fe0bdd6f69bc4ae33935a898e3cbefbbccf22e88a27fa053b"},
]
reorder-python-imports = [
{file = "reorder_python_imports-3.8.5-py2.py3-none-any.whl", hash = "sha256:6c36b84add1a8125479e1de97a21b7797ee6df51530b5340857d65c79d6882ac"},
{file = "reorder_python_imports-3.8.5.tar.gz", hash = "sha256:5e018dceb889688eafd41a1b217420f810e0400f5a26c679a08f7f9de956ca3b"},
{file = "reorder_python_imports-3.9.0-py2.py3-none-any.whl", hash = "sha256:3f9c16e8781f54c944756d0d1eb34a8c863554f7a4eb3693f574fe19b1a29b56"},
{file = "reorder_python_imports-3.9.0.tar.gz", hash = "sha256:49292ed537829a6bece9fb3746fc1bbe98f52643be5de01a4e13680268a5b0ec"},
]
requests = [
{file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"},
@ -3326,13 +3323,76 @@ safety = [
{file = "safety-2.3.1.tar.gz", hash = "sha256:6e6fcb7d4e8321098cf289f59b65051cafd3467f089c6e57c9f894ae32c23b71"},
]
sentry-sdk = [
{file = "sentry-sdk-1.9.10.tar.gz", hash = "sha256:4fbace9a763285b608c06f01a807b51acb35f6059da6a01236654e08b0ee81ff"},
{file = "sentry_sdk-1.9.10-py2.py3-none-any.whl", hash = "sha256:2469240f6190aaebcb453033519eae69cfe8cc602065b4667e18ee14fc1e35dc"},
{file = "sentry-sdk-1.10.1.tar.gz", hash = "sha256:105faf7bd7b7fa25653404619ee261527266b14103fe1389e0ce077bd23a9691"},
{file = "sentry_sdk-1.10.1-py2.py3-none-any.whl", hash = "sha256:06c0fa9ccfdc80d7e3b5d2021978d6eb9351fa49db9b5847cf4d1f2a473414ad"},
]
setuptools = [
{file = "setuptools-65.5.0-py3-none-any.whl", hash = "sha256:f62ea9da9ed6289bfe868cd6845968a2c854d1427f8548d52cae02a42b4f0356"},
{file = "setuptools-65.5.0.tar.gz", hash = "sha256:512e5536220e38146176efb833d4a62aa726b7bbff82cfbc8ba9eaa3996e0b17"},
]
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"},
{file = "simplejson-3.17.6-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:0de783e9c2b87bdd75b57efa2b6260c24b94605b5c9843517577d40ee0c3cc8a"},
{file = "simplejson-3.17.6-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:d24a9e61df7a7787b338a58abfba975414937b609eb6b18973e25f573bc0eeeb"},
{file = "simplejson-3.17.6-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:e8603e691580487f11306ecb066c76f1f4a8b54fb3bdb23fa40643a059509366"},
{file = "simplejson-3.17.6-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:9b01e7b00654115965a206e3015f0166674ec1e575198a62a977355597c0bef5"},
{file = "simplejson-3.17.6-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:37bc0cf0e5599f36072077e56e248f3336917ded1d33d2688624d8ed3cefd7d2"},
{file = "simplejson-3.17.6-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:cf6e7d5fe2aeb54898df18db1baf479863eae581cce05410f61f6b4188c8ada1"},
{file = "simplejson-3.17.6-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:bdfc54b4468ed4cd7415928cbe782f4d782722a81aeb0f81e2ddca9932632211"},
{file = "simplejson-3.17.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:dd16302d39c4d6f4afde80edd0c97d4db643327d355a312762ccd9bd2ca515ed"},
{file = "simplejson-3.17.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:deac4bdafa19bbb89edfb73b19f7f69a52d0b5bd3bb0c4ad404c1bbfd7b4b7fd"},
{file = "simplejson-3.17.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a8bbdb166e2fb816e43ab034c865147edafe28e1b19c72433147789ac83e2dda"},
{file = "simplejson-3.17.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a7854326920d41c3b5d468154318fe6ba4390cb2410480976787c640707e0180"},
{file = "simplejson-3.17.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:04e31fa6ac8e326480703fb6ded1488bfa6f1d3f760d32e29dbf66d0838982ce"},
{file = "simplejson-3.17.6-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f63600ec06982cdf480899026f4fda622776f5fabed9a869fdb32d72bc17e99a"},
{file = "simplejson-3.17.6-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e03c3b8cc7883a54c3f34a6a135c4a17bc9088a33f36796acdb47162791b02f6"},
{file = "simplejson-3.17.6-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a2d30d6c1652140181dc6861f564449ad71a45e4f165a6868c27d36745b65d40"},
{file = "simplejson-3.17.6-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a1aa6e4cae8e3b8d5321be4f51c5ce77188faf7baa9fe1e78611f93a8eed2882"},
{file = "simplejson-3.17.6-cp310-cp310-win32.whl", hash = "sha256:97202f939c3ff341fc3fa84d15db86156b1edc669424ba20b0a1fcd4a796a045"},
{file = "simplejson-3.17.6-cp310-cp310-win_amd64.whl", hash = "sha256:80d3bc9944be1d73e5b1726c3bbfd2628d3d7fe2880711b1eb90b617b9b8ac70"},
{file = "simplejson-3.17.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9fa621b3c0c05d965882c920347b6593751b7ab20d8fa81e426f1735ca1a9fc7"},
{file = "simplejson-3.17.6-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd2fb11922f58df8528adfca123f6a84748ad17d066007e7ac977720063556bd"},
{file = "simplejson-3.17.6-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:724c1fe135aa437d5126138d977004d165a3b5e2ee98fc4eb3e7c0ef645e7e27"},
{file = "simplejson-3.17.6-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4ff4ac6ff3aa8f814ac0f50bf218a2e1a434a17aafad4f0400a57a8cc62ef17f"},
{file = "simplejson-3.17.6-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:67093a526e42981fdd954868062e56c9b67fdd7e712616cc3265ad0c210ecb51"},
{file = "simplejson-3.17.6-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:5d6b4af7ad7e4ac515bc6e602e7b79e2204e25dbd10ab3aa2beef3c5a9cad2c7"},
{file = "simplejson-3.17.6-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:1c9b1ed7ed282b36571638297525f8ef80f34b3e2d600a56f962c6044f24200d"},
{file = "simplejson-3.17.6-cp36-cp36m-win32.whl", hash = "sha256:632ecbbd2228575e6860c9e49ea3cc5423764d5aa70b92acc4e74096fb434044"},
{file = "simplejson-3.17.6-cp36-cp36m-win_amd64.whl", hash = "sha256:4c09868ddb86bf79b1feb4e3e7e4a35cd6e61ddb3452b54e20cf296313622566"},
{file = "simplejson-3.17.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4b6bd8144f15a491c662f06814bd8eaa54b17f26095bb775411f39bacaf66837"},
{file = "simplejson-3.17.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5decdc78849617917c206b01e9fc1d694fd58caa961be816cb37d3150d613d9a"},
{file = "simplejson-3.17.6-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:521877c7bd060470806eb6335926e27453d740ac1958eaf0d8c00911bc5e1802"},
{file = "simplejson-3.17.6-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:65b998193bd7b0c7ecdfffbc825d808eac66279313cb67d8892bb259c9d91494"},
{file = "simplejson-3.17.6-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ac786f6cb7aa10d44e9641c7a7d16d7f6e095b138795cd43503769d4154e0dc2"},
{file = "simplejson-3.17.6-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:3ff5b3464e1ce86a8de8c88e61d4836927d5595c2162cab22e96ff551b916e81"},
{file = "simplejson-3.17.6-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:69bd56b1d257a91e763256d63606937ae4eb890b18a789b66951c00062afec33"},
{file = "simplejson-3.17.6-cp37-cp37m-win32.whl", hash = "sha256:b81076552d34c27e5149a40187a8f7e2abb2d3185576a317aaf14aeeedad862a"},
{file = "simplejson-3.17.6-cp37-cp37m-win_amd64.whl", hash = "sha256:07ecaafc1b1501f275bf5acdee34a4ad33c7c24ede287183ea77a02dc071e0c0"},
{file = "simplejson-3.17.6-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:068670af975247acbb9fc3d5393293368cda17026db467bf7a51548ee8f17ee1"},
{file = "simplejson-3.17.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4d1c135af0c72cb28dd259cf7ba218338f4dc027061262e46fe058b4e6a4c6a3"},
{file = "simplejson-3.17.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:23fe704da910ff45e72543cbba152821685a889cf00fc58d5c8ee96a9bad5f94"},
{file = "simplejson-3.17.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f444762fed1bc1fd75187ef14a20ed900c1fbb245d45be9e834b822a0223bc81"},
{file = "simplejson-3.17.6-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:681eb4d37c9a9a6eb9b3245a5e89d7f7b2b9895590bb08a20aa598c1eb0a1d9d"},
{file = "simplejson-3.17.6-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8e8607d8f6b4f9d46fee11447e334d6ab50e993dd4dbfb22f674616ce20907ab"},
{file = "simplejson-3.17.6-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b10556817f09d46d420edd982dd0653940b90151d0576f09143a8e773459f6fe"},
{file = "simplejson-3.17.6-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:e1ec8a9ee0987d4524ffd6299e778c16cc35fef6d1a2764e609f90962f0b293a"},
{file = "simplejson-3.17.6-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0b4126cac7d69ac06ff22efd3e0b3328a4a70624fcd6bca4fc1b4e6d9e2e12bf"},
{file = "simplejson-3.17.6-cp38-cp38-win32.whl", hash = "sha256:35a49ebef25f1ebdef54262e54ae80904d8692367a9f208cdfbc38dbf649e00a"},
{file = "simplejson-3.17.6-cp38-cp38-win_amd64.whl", hash = "sha256:743cd768affaa508a21499f4858c5b824ffa2e1394ed94eb85caf47ac0732198"},
{file = "simplejson-3.17.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:fb62d517a516128bacf08cb6a86ecd39fb06d08e7c4980251f5d5601d29989ba"},
{file = "simplejson-3.17.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:12133863178a8080a3dccbf5cb2edfab0001bc41e5d6d2446af2a1131105adfe"},
{file = "simplejson-3.17.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5540fba2d437edaf4aa4fbb80f43f42a8334206ad1ad3b27aef577fd989f20d9"},
{file = "simplejson-3.17.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d74ee72b5071818a1a5dab47338e87f08a738cb938a3b0653b9e4d959ddd1fd9"},
{file = "simplejson-3.17.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:28221620f4dcabdeac310846629b976e599a13f59abb21616356a85231ebd6ad"},
{file = "simplejson-3.17.6-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b09bc62e5193e31d7f9876220fb429ec13a6a181a24d897b9edfbbdbcd678851"},
{file = "simplejson-3.17.6-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7255a37ff50593c9b2f1afa8fafd6ef5763213c1ed5a9e2c6f5b9cc925ab979f"},
{file = "simplejson-3.17.6-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:401d40969cee3df7bda211e57b903a534561b77a7ade0dd622a8d1a31eaa8ba7"},
{file = "simplejson-3.17.6-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a649d0f66029c7eb67042b15374bd93a26aae202591d9afd71e111dd0006b198"},
{file = "simplejson-3.17.6-cp39-cp39-win32.whl", hash = "sha256:522fad7be85de57430d6d287c4b635813932946ebf41b913fe7e880d154ade2e"},
{file = "simplejson-3.17.6-cp39-cp39-win_amd64.whl", hash = "sha256:3fe87570168b2ae018391e2b43fbf66e8593a86feccb4b0500d134c998983ccc"},
{file = "simplejson-3.17.6.tar.gz", hash = "sha256:cf98038d2abf63a1ada5730e91e84c642ba6c225b0198c3684151b1f80c5f8a6"},
]
six = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
@ -3354,8 +3414,8 @@ Sphinx = [
{file = "sphinx-5.3.0-py3-none-any.whl", hash = "sha256:060ca5c9f7ba57a08a1219e547b269fadf125ae25b06b9fa7f66768efb652d6d"},
]
sphinx-autoapi = [
{file = "sphinx-autoapi-1.9.0.tar.gz", hash = "sha256:c897ea337df16ad0cde307cbdfe2bece207788dde1587fa4fc8b857d1fc5dcba"},
{file = "sphinx_autoapi-1.9.0-py2.py3-none-any.whl", hash = "sha256:d217953273b359b699d8cb81a5a72985a3e6e15cfe3f703d9a3c201ffc30849b"},
{file = "sphinx-autoapi-2.0.0.tar.gz", hash = "sha256:97dcf1b5b54cd0d8efef867594e4a4f3e2d3a2c0ec1e5a891e0a61bc77046006"},
{file = "sphinx_autoapi-2.0.0-py2.py3-none-any.whl", hash = "sha256:dab2753a38cad907bf4e61473c0da365a26bfbe69fbf5aa6e4f7d48e1cf8a148"},
]
sphinx-autobuild = [
{file = "sphinx-autobuild-2021.3.14.tar.gz", hash = "sha256:de1ca3b66e271d2b5b5140c35034c89e47f263f2cd5db302c9217065f7443f05"},

View File

@ -31,8 +31,8 @@ werkzeug = "*"
SpiffWorkflow = {git = "https://github.com/sartography/SpiffWorkflow", rev = "main"}
# SpiffWorkflow = {develop = true, path = "/Users/kevin/projects/github/sartography/SpiffWorkflow"}
# SpiffWorkflow = {develop = true, path = "/home/jason/projects/github/sartography/SpiffWorkflow"}
sentry-sdk = "^1.9.10"
sphinx-autoapi = "^1.8.4"
sentry-sdk = "^1.10"
sphinx-autoapi = "^2.0"
# flask-bpmn = {develop = true, path = "/home/jason/projects/github/sartography/flask-bpmn"}
# flask-bpmn = {develop = true, path = "/Users/kevin/projects/github/sartography/flask-bpmn"}
flask-bpmn = {git = "https://github.com/sartography/flask-bpmn", rev = "main"}
@ -45,7 +45,7 @@ connexion = {extras = [ "swagger-ui",], version = "^2"}
lxml = "^4.9.1"
marshmallow-enum = "^1.5.1"
marshmallow-sqlalchemy = "^0.28.0"
PyJWT = "^2.4.0"
PyJWT = "^2.6.0"
gunicorn = "^20.1.0"
python-keycloak = "^2.5.0"
APScheduler = "^3.9.1"
@ -71,6 +71,7 @@ types-pytz = "^2022.1.1"
# sqlalchemy-stubs = {develop = true, path = "/Users/kevin/projects/github/sqlalchemy-stubs"}
# for now use my fork
sqlalchemy-stubs = { git = "https://github.com/burnettk/sqlalchemy-stubs.git", rev = "scoped-session-delete" }
simplejson = "^3.17.6"
[tool.poetry.dev-dependencies]
@ -90,17 +91,17 @@ flake8-bandit = "^2.1.2"
# 1.7.3 broke us. https://github.com/PyCQA/bandit/issues/841
bandit = "1.7.2"
flake8-bugbear = "^22.7.1"
flake8-bugbear = "^22.10.25"
flake8-docstrings = "^1.6.0"
flake8-rst-docstrings = "^0.2.7"
# flask-sqlalchemy-stubs = "^0.2"
pep8-naming = "^0.13.2"
darglint = "^1.8.1"
reorder-python-imports = "^3.8.1"
reorder-python-imports = "^3.9.0"
pre-commit-hooks = "^4.0.1"
sphinx-click = "^4.3.0"
Pygments = "^2.10.0"
pyupgrade = "^2.37.1"
pyupgrade = "^3.1.0"
furo = ">=2021.11.12"
MonkeyType = "^22.2.0"

View File

@ -145,7 +145,6 @@ def get_hacked_up_app_for_script() -> flask.app.Flask:
def configure_sentry(app: flask.app.Flask) -> None:
"""Configure_sentry."""
import sentry_sdk
from flask import Flask
from sentry_sdk.integrations.flask import FlaskIntegration
def before_send(event: Any, hint: Any) -> Any:
@ -172,5 +171,3 @@ def configure_sentry(app: flask.app.Flask) -> None:
traces_sample_rate=float(sentry_sample_rate),
before_send=before_send,
)
app = Flask(__name__)

View File

@ -2,7 +2,7 @@
from os import environ
SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME = environ.get(
"SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME", default="staging.yml"
"SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME", default="development.yml"
)
SPIFFWORKFLOW_BACKEND_LOG_LEVEL = environ.get(

View File

@ -1,29 +1,87 @@
default_group: everybody
groups:
admin:
users:
[jakub, kb, alex, dan, mike, jason, amir, jarrad, elizabeth, jon, natalia]
[
jakub,
kb,
alex,
dan,
mike,
jason,
amir,
jarrad,
elizabeth,
jon,
natalia,
harmeet,
sasha,
manuchehr,
]
finance:
users: [harmeet, sasha]
Finance Team:
users:
[
jakub,
alex,
dan,
mike,
jason,
amir,
jarrad,
elizabeth,
jon,
natalia,
sasha,
]
Project Lead:
users:
[
jakub,
alex,
dan,
mike,
jason,
jarrad,
elizabeth,
jon,
natalia,
manuchehr,
]
hr:
users: [manuchehr]
permissions:
tasks-crud:
groups: [everybody]
users: []
allowed_permissions: [create, read, update, delete]
uri: /v1.0/tasks/*
admin:
groups: [admin]
users: []
allowed_permissions: [create, read, update, delete, list, instantiate]
uri: /*
# TODO: all uris should really have the same structure
finance-admin-group:
groups: ["Finance Team"]
users: []
allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-groups/finance/*
finance-admin:
groups: [finance]
groups: ["Finance Team"]
users: []
allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-groups/finance/*
read-all:
groups: [finance, hr, admin]
groups: ["Finance Team", "Project Lead", hr, admin]
users: []
allowed_permissions: [read]
uri: /*

View File

@ -1,8 +1,10 @@
default_group: everybody
groups:
admin:
users: [testadmin1, testadmin2]
finance:
Finance Team:
users: [testuser1, testuser2]
hr:
@ -16,13 +18,26 @@ permissions:
uri: /*
read-all:
groups: [finance, hr, admin]
groups: ["Finance Team", hr, admin]
users: []
allowed_permissions: [read]
uri: /*
finance-admin:
groups: [finance]
tasks-crud:
groups: [everybody]
users: []
allowed_permissions: [create, read, update, delete]
uri: /v1.0/tasks/*
# TODO: all uris should really have the same structure
finance-admin-group:
groups: ["Finance Team"]
users: [testuser4]
allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-groups/finance/*
finance-admin-model:
groups: ["Finance Team"]
users: [testuser4]
allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-models/finance/*

View File

@ -10,10 +10,11 @@ avoid circular imports
from flask_bpmn.models.db import add_listeners
# must load this before UserModel and GroupModel for relationships
# must load these before UserModel and GroupModel for relationships
from spiffworkflow_backend.models.user_group_assignment import (
UserGroupAssignmentModel,
) # noqa: F401
from spiffworkflow_backend.models.principal import PrincipalModel # noqa: F401
from spiffworkflow_backend.models.active_task import ActiveTaskModel # noqa: F401
@ -38,7 +39,6 @@ from spiffworkflow_backend.models.permission_assignment import (
from spiffworkflow_backend.models.permission_target import (
PermissionTargetModel,
) # noqa: F401
from spiffworkflow_backend.models.principal import PrincipalModel # noqa: F401
from spiffworkflow_backend.models.process_instance import (
ProcessInstanceModel,
) # noqa: F401

View File

@ -2,6 +2,7 @@
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
@ -9,9 +10,16 @@ from sqlalchemy import ForeignKey
from sqlalchemy.orm import relationship
from sqlalchemy.orm import RelationshipProperty
from spiffworkflow_backend.models.principal import PrincipalModel
from spiffworkflow_backend.models.group import GroupModel
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
from spiffworkflow_backend.models.task import Task
from spiffworkflow_backend.models.user import UserModel
if TYPE_CHECKING:
from spiffworkflow_backend.models.active_task_user import ( # noqa: F401
ActiveTaskUserModel,
)
@dataclass
@ -25,14 +33,13 @@ class ActiveTaskModel(SpiffworkflowBaseDBModel):
),
)
assigned_principal: RelationshipProperty[PrincipalModel] = relationship(
PrincipalModel
)
actual_owner: RelationshipProperty[UserModel] = relationship(UserModel)
id: int = db.Column(db.Integer, primary_key=True)
process_instance_id: int = db.Column(
ForeignKey(ProcessInstanceModel.id), nullable=False # type: ignore
)
assigned_principal_id: int = db.Column(ForeignKey(PrincipalModel.id))
actual_owner_id: int = db.Column(ForeignKey(UserModel.id))
lane_assignment_id: int | None = db.Column(ForeignKey(GroupModel.id))
form_file_name: str | None = db.Column(db.String(50))
ui_form_file_name: str | None = db.Column(db.String(50))
@ -46,6 +53,14 @@ class ActiveTaskModel(SpiffworkflowBaseDBModel):
task_status = db.Column(db.String(50))
process_model_display_name = db.Column(db.String(255))
active_task_users = relationship("ActiveTaskUserModel", cascade="delete")
potential_owners = relationship( # type: ignore
"UserModel",
viewonly=True,
secondary="active_task_user",
overlaps="active_task_user,users",
)
@classmethod
def to_task(cls, task: ActiveTaskModel) -> Task:
"""To_task."""

View File

@ -0,0 +1,32 @@
"""Active_task_user."""
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
from spiffworkflow_backend.models.user import UserModel
@dataclass
class ActiveTaskUserModel(SpiffworkflowBaseDBModel):
"""ActiveTaskUserModel."""
__tablename__ = "active_task_user"
__table_args__ = (
db.UniqueConstraint(
"active_task_id",
"user_id",
name="active_task_user_unique",
),
)
id = db.Column(db.Integer, primary_key=True)
active_task_id = db.Column(
ForeignKey(ActiveTaskModel.id), nullable=False, index=True # type: ignore
)
user_id = db.Column(ForeignKey(UserModel.id), nullable=False, index=True)

View File

@ -4,6 +4,7 @@ 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
from spiffworkflow_backend.models.group import GroupModel
@ -28,3 +29,6 @@ class PrincipalModel(SpiffworkflowBaseDBModel):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(ForeignKey(UserModel.id), nullable=True, unique=True)
group_id = db.Column(ForeignKey(GroupModel.id), nullable=True, unique=True)
user = relationship("UserModel", viewonly=True)
group = relationship("GroupModel", viewonly=True)

View File

@ -1,4 +1,6 @@
"""User."""
from __future__ import annotations
from typing import Any
import jwt

View File

@ -28,12 +28,14 @@ from lxml import etree # type: ignore
from lxml.builder import ElementMaker # type: ignore
from SpiffWorkflow.task import Task as SpiffTask # type: ignore
from SpiffWorkflow.task import TaskState
from sqlalchemy import asc
from sqlalchemy import desc
from spiffworkflow_backend.exceptions.process_entity_not_found_error import (
ProcessEntityNotFoundError,
)
from spiffworkflow_backend.models.active_task import ActiveTaskModel
from spiffworkflow_backend.models.active_task_user import ActiveTaskUserModel
from spiffworkflow_backend.models.file import FileSchema
from spiffworkflow_backend.models.message_instance import MessageInstanceModel
from spiffworkflow_backend.models.message_model import MessageModel
@ -918,11 +920,11 @@ def process_instance_report_show(
def task_list_my_tasks(page: int = 1, per_page: int = 100) -> flask.wrappers.Response:
"""Task_list_my_tasks."""
principal = find_principal_or_raise()
active_tasks = (
ActiveTaskModel.query.filter_by(assigned_principal_id=principal.id)
.order_by(desc(ActiveTaskModel.id)) # type: ignore
ActiveTaskModel.query.order_by(desc(ActiveTaskModel.id)) # type: ignore
.join(ProcessInstanceModel)
.join(ActiveTaskUserModel)
.filter_by(user_id=principal.user_id)
# just need this add_columns to add the process_model_identifier. Then add everything back that was removed.
.add_columns(
ProcessInstanceModel.process_model_identifier,
@ -1085,18 +1087,15 @@ def task_submit(
) -> flask.wrappers.Response:
"""Task_submit_user_data."""
principal = find_principal_or_raise()
active_task_assigned_to_me = find_active_task_by_id_or_raise(
process_instance_id, task_id, principal.id
)
process_instance = find_process_instance_by_id_or_raise(
active_task_assigned_to_me.process_instance_id
)
process_instance = find_process_instance_by_id_or_raise(process_instance_id)
processor = ProcessInstanceProcessor(process_instance)
spiff_task = get_spiff_task_from_process_instance(
task_id, process_instance, processor=processor
)
AuthorizationService.assert_user_can_complete_spiff_task(
processor, spiff_task, principal.user
)
if spiff_task.state != TaskState.READY:
raise (
@ -1110,10 +1109,6 @@ def task_submit(
if terminate_loop and spiff_task.is_looping():
spiff_task.terminate_loop()
# TODO: support repeating fields
# Extract the details specific to the form submitted
# form_data = WorkflowService().extract_form_data(body, spiff_task)
ProcessInstanceService.complete_form_task(processor, spiff_task, body, g.user)
# If we need to update all tasks, then get the next ready task and if it a multi-instance with the same
@ -1128,9 +1123,13 @@ def task_submit(
ProcessInstanceService.update_task_assignments(processor)
next_active_task_assigned_to_me = ActiveTaskModel.query.filter_by(
assigned_principal_id=principal.id, process_instance_id=process_instance.id
).first()
next_active_task_assigned_to_me = (
ActiveTaskModel.query.filter_by(process_instance_id=process_instance_id)
.order_by(asc(ActiveTaskModel.id)) # type: ignore
.join(ActiveTaskUserModel)
.filter_by(user_id=principal.user_id)
.first()
)
if next_active_task_assigned_to_me:
return make_response(
jsonify(ActiveTaskModel.to_task(next_active_task_assigned_to_me)), 200
@ -1293,30 +1292,6 @@ def find_principal_or_raise() -> PrincipalModel:
return principal # type: ignore
def find_active_task_by_id_or_raise(
process_instance_id: int, task_id: str, principal_id: PrincipalModel
) -> ActiveTaskModel:
"""Find_active_task_by_id_or_raise."""
active_task_assigned_to_me = ActiveTaskModel.query.filter_by(
process_instance_id=process_instance_id,
task_id=task_id,
assigned_principal_id=principal_id,
).first()
if active_task_assigned_to_me is None:
message = (
f"Task not found for principal user {principal_id} "
f"process_instance_id: {process_instance_id}, task_id: {task_id}"
)
raise (
ApiError(
error_code="task_not_found",
message=message,
status_code=400,
)
)
return active_task_assigned_to_me # type: ignore
def find_process_instance_by_id_or_raise(
process_instance_id: int,
) -> ProcessInstanceModel:

View File

@ -203,7 +203,6 @@ 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"))
state_redirect_url = state_dict["redirect_url"]
auth_token_object = AuthenticationService().get_auth_token_object(code)
if "id_token" in auth_token_object:
id_token = auth_token_object["id_token"]
@ -213,46 +212,12 @@ def login_return(code: str, state: str, session_state: str) -> Optional[Response
auth_token_object["access_token"]
)
if user_info and "error" not in user_info:
user_model = (
UserModel.query.filter(UserModel.service == "open_id")
.filter(UserModel.service_id == user_info["sub"])
.first()
user_model = AuthorizationService.create_user_from_sign_in(user_info)
g.user = user_model.id
g.token = auth_token_object["id_token"]
AuthenticationService.store_refresh_token(
user_model.id, auth_token_object["refresh_token"]
)
if user_model is None:
current_app.logger.debug("create_user in login_return")
name = username = email = ""
if "name" in user_info:
name = user_info["name"]
if "username" in user_info:
username = user_info["username"]
elif "preferred_username" in user_info:
username = user_info["preferred_username"]
if "email" in user_info:
email = user_info["email"]
user_model = UserService().create_user(
service="open_id",
service_id=user_info["sub"],
name=name,
username=username,
email=email,
)
if user_model:
g.user = user_model.id
g.token = auth_token_object["id_token"]
AuthenticationService.store_refresh_token(
user_model.id, auth_token_object["refresh_token"]
)
# this may eventually get too slow.
# when it does, be careful about backgrounding, because
# the user will immediately need permissions to use the site.
# we are also a little apprehensive about pre-creating users
# before the user signs in, because we won't know things like
# the external service user identifier.
AuthorizationService.import_permissions_from_yaml_file()
redirect_url = (
f"{state_redirect_url}?"
+ f"access_token={auth_token_object['access_token']}&"

View File

@ -10,8 +10,10 @@ from flask import g
from flask import request
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 sqlalchemy import text
from spiffworkflow_backend.models.active_task import ActiveTaskModel
from spiffworkflow_backend.models.group import GroupModel
from spiffworkflow_backend.models.permission_assignment import PermissionAssignmentModel
from spiffworkflow_backend.models.permission_target import PermissionTargetModel
@ -20,6 +22,9 @@ 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.services.process_instance_processor import (
ProcessInstanceProcessor,
)
from spiffworkflow_backend.services.user_service import UserService
@ -27,6 +32,14 @@ class PermissionsFileNotSetError(Exception):
"""PermissionsFileNotSetError."""
class ActiveTaskNotFoundError(Exception):
"""ActiveTaskNotFoundError."""
class UserDoesNotHaveAccessToTaskError(Exception):
"""UserDoesNotHaveAccessToTaskError."""
class AuthorizationService:
"""Determine whether a user has permission to perform their request."""
@ -93,6 +106,19 @@ class AuthorizationService:
db.session.commit()
cls.import_permissions_from_yaml_file()
@classmethod
def associate_user_with_group(cls, user: UserModel, group: GroupModel) -> None:
"""Associate_user_with_group."""
user_group_assignemnt = UserGroupAssignmentModel.query.filter_by(
user_id=user.id, group_id=group.id
).first()
if user_group_assignemnt is None:
user_group_assignemnt = UserGroupAssignmentModel(
user_id=user.id, group_id=group.id
)
db.session.add(user_group_assignemnt)
db.session.commit()
@classmethod
def import_permissions_from_yaml_file(
cls, raise_if_missing_user: bool = False
@ -109,6 +135,20 @@ class AuthorizationService:
with open(current_app.config["PERMISSIONS_FILE_FULLPATH"]) as file:
permission_configs = yaml.safe_load(file)
default_group = None
if "default_group" in permission_configs:
default_group_identifier = permission_configs["default_group"]
default_group = GroupModel.query.filter_by(
identifier=default_group_identifier
).first()
if default_group is None:
default_group = GroupModel(identifier=default_group_identifier)
db.session.add(default_group)
db.session.commit()
UserService.create_principal(
default_group.id, id_column_name="group_id"
)
if "groups" in permission_configs:
for group_identifier, group_config in permission_configs["groups"].items():
group = GroupModel.query.filter_by(identifier=group_identifier).first()
@ -127,15 +167,7 @@ class AuthorizationService:
)
)
continue
user_group_assignemnt = UserGroupAssignmentModel.query.filter_by(
user_id=user.id, group_id=group.id
).first()
if user_group_assignemnt is None:
user_group_assignemnt = UserGroupAssignmentModel(
user_id=user.id, group_id=group.id
)
db.session.add(user_group_assignemnt)
db.session.commit()
cls.associate_user_with_group(user, group)
if "permissions" in permission_configs:
for _permission_identifier, permission_config in permission_configs[
@ -164,14 +196,20 @@ class AuthorizationService:
)
if "users" in permission_config:
for username in permission_config["users"]:
principal = (
PrincipalModel.query.join(UserModel)
.filter(UserModel.username == username)
.first()
)
cls.create_permission_for_principal(
principal, permission_target, allowed_permission
)
user = UserModel.query.filter_by(username=username).first()
if user is not None:
principal = (
PrincipalModel.query.join(UserModel)
.filter(UserModel.username == username)
.first()
)
cls.create_permission_for_principal(
principal, permission_target, allowed_permission
)
if default_group is not None:
for user in UserModel.query.all():
cls.associate_user_with_group(user, default_group)
@classmethod
def create_permission_for_principal(
@ -202,6 +240,7 @@ class AuthorizationService:
@classmethod
def should_disable_auth_for_request(cls) -> bool:
"""Should_disable_auth_for_request."""
swagger_functions = ["get_json_spec"]
authentication_exclusion_list = ["status", "authentication_callback"]
if request.method == "OPTIONS":
return True
@ -218,7 +257,9 @@ class AuthorizationService:
api_view_function
and api_view_function.__name__.startswith("login")
or api_view_function.__name__.startswith("logout")
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
):
return True
@ -277,7 +318,7 @@ class AuthorizationService:
raise ApiError(
error_code="unauthorized",
message="User is not authorized to perform requested action.",
message=f"User {g.user.username} is not authorized to perform requested action: {permission_string} - {request.path}",
status_code=403,
)
@ -359,196 +400,73 @@ class AuthorizationService:
"The Authentication token you provided is invalid. You need a new token. ",
) from exception
# def get_bearer_token_from_internal_token(self, internal_token):
# """Get_bearer_token_from_internal_token."""
# self.decode_auth_token(internal_token)
# print(f"get_user_by_internal_token: {internal_token}")
@staticmethod
def assert_user_can_complete_spiff_task(
processor: ProcessInstanceProcessor,
spiff_task: SpiffTask,
user: UserModel,
) -> bool:
"""Assert_user_can_complete_spiff_task."""
active_task = ActiveTaskModel.query.filter_by(
task_name=spiff_task.task_spec.name,
process_instance_id=processor.process_instance_model.id,
).first()
if active_task is None:
raise ActiveTaskNotFoundError(
f"Could find an active task with task name '{spiff_task.task_spec.name}'"
f" for process instance '{processor.process_instance_model.id}'"
)
# def introspect_token(self, basic_token: str) -> dict:
# """Introspect_token."""
# (
# open_id_server_url,
# open_id_client_id,
# open_id_realm_name,
# open_id_client_secret_key,
# ) = AuthorizationService.get_open_id_args()
#
# bearer_token = AuthorizationService().get_bearer_token(basic_token)
# auth_bearer_string = f"Bearer {bearer_token['access_token']}"
#
# headers = {
# "Content-Type": "application/x-www-form-urlencoded",
# "Authorization": auth_bearer_string,
# }
# data = {
# "client_id": open_id_client_id,
# "client_secret": open_id_client_secret_key,
# "token": basic_token,
# }
# request_url = f"{open_id_server_url}/realms/{open_id_realm_name}/protocol/openid-connect/token/introspect"
#
# introspect_response = requests.post(request_url, headers=headers, data=data)
# introspection = json.loads(introspect_response.text)
#
# return introspection
if user not in active_task.potential_owners:
raise UserDoesNotHaveAccessToTaskError(
f"User {user.username} does not have access to update task'{spiff_task.task_spec.name}'"
f" for process instance '{processor.process_instance_model.id}'"
)
return True
# def get_permission_by_basic_token(self, basic_token: dict) -> list:
# """Get_permission_by_basic_token."""
# (
# open_id_server_url,
# open_id_client_id,
# open_id_realm_name,
# open_id_client_secret_key,
# ) = AuthorizationService.get_open_id_args()
#
# # basic_token = AuthorizationService().refresh_token(basic_token)
# # bearer_token = AuthorizationService().get_bearer_token(basic_token['access_token'])
# bearer_token = AuthorizationService().get_bearer_token(basic_token)
# # auth_bearer_string = f"Bearer {bearer_token['access_token']}"
# auth_bearer_string = f"Bearer {bearer_token}"
#
# headers = {
# "Content-Type": "application/x-www-form-urlencoded",
# "Authorization": auth_bearer_string,
# }
# data = {
# "client_id": open_id_client_id,
# "client_secret": open_id_client_secret_key,
# "grant_type": "urn:ietf:params:oauth:grant-type:uma-ticket",
# "response_mode": "permissions",
# "audience": open_id_client_id,
# "response_include_resource_name": True,
# }
# request_url = f"{open_id_server_url}/realms/{open_id_realm_name}/protocol/openid-connect/token"
# permission_response = requests.post(request_url, headers=headers, data=data)
# permission = json.loads(permission_response.text)
# return permission
@classmethod
def create_user_from_sign_in(cls, user_info: dict) -> UserModel:
"""Create_user_from_sign_in."""
is_new_user = False
user_model = (
UserModel.query.filter(UserModel.service == "open_id")
.filter(UserModel.service_id == user_info["sub"])
.first()
)
# def get_auth_status_for_resource_and_scope_by_token(
# self, basic_token: dict, resource: str, scope: str
# ) -> str:
# """Get_auth_status_for_resource_and_scope_by_token."""
# (
# open_id_server_url,
# open_id_client_id,
# open_id_realm_name,
# open_id_client_secret_key,
# ) = AuthorizationService.get_open_id_args()
#
# # basic_token = AuthorizationService().refresh_token(basic_token)
# bearer_token = AuthorizationService().get_bearer_token(basic_token)
# auth_bearer_string = f"Bearer {bearer_token['access_token']}"
#
# headers = {
# "Content-Type": "application/x-www-form-urlencoded",
# "Authorization": auth_bearer_string,
# }
# data = {
# "client_id": open_id_client_id,
# "client_secret": open_id_client_secret_key,
# "grant_type": "urn:ietf:params:oauth:grant-type:uma-ticket",
# "permission": f"{resource}#{scope}",
# "response_mode": "permissions",
# "audience": open_id_client_id,
# }
# request_url = f"{open_id_server_url}/realms/{open_id_realm_name}/protocol/openid-connect/token"
# auth_response = requests.post(request_url, headers=headers, data=data)
#
# print("get_auth_status_for_resource_and_scope_by_token")
# auth_status: str = json.loads(auth_response.text)
# return auth_status
if user_model is None:
current_app.logger.debug("create_user in login_return")
is_new_user = True
name = username = email = ""
if "name" in user_info:
name = user_info["name"]
if "username" in user_info:
username = user_info["username"]
elif "preferred_username" in user_info:
username = user_info["preferred_username"]
if "email" in user_info:
email = user_info["email"]
user_model = UserService().create_user(
service="open_id",
service_id=user_info["sub"],
name=name,
username=username,
email=email,
)
# def get_permissions_by_token_for_resource_and_scope(
# self, basic_token: str, resource: str|None=None, scope: str|None=None
# ) -> str:
# """Get_permissions_by_token_for_resource_and_scope."""
# (
# open_id_server_url,
# open_id_client_id,
# open_id_realm_name,
# open_id_client_secret_key,
# ) = AuthorizationService.get_open_id_args()
#
# # basic_token = AuthorizationService().refresh_token(basic_token)
# # bearer_token = AuthorizationService().get_bearer_token(basic_token['access_token'])
# bearer_token = AuthorizationService().get_bearer_token(basic_token)
# auth_bearer_string = f"Bearer {bearer_token['access_token']}"
#
# headers = {
# "Content-Type": "application/x-www-form-urlencoded",
# "Authorization": auth_bearer_string,
# }
# permision = ""
# if resource is not None and resource != '':
# permision += resource
# if scope is not None and scope != '':
# permision += "#" + scope
# data = {
# "client_id": open_id_client_id,
# "client_secret": open_id_client_secret_key,
# "grant_type": "urn:ietf:params:oauth:grant-type:uma-ticket",
# "response_mode": "permissions",
# "permission": permision,
# "audience": open_id_client_id,
# "response_include_resource_name": True,
# }
# request_url = f"{open_id_server_url}/realms/{open_id_realm_name}/protocol/openid-connect/token"
# permission_response = requests.post(request_url, headers=headers, data=data)
# permission: str = json.loads(permission_response.text)
# return permission
# this may eventually get too slow.
# when it does, be careful about backgrounding, because
# the user will immediately need permissions to use the site.
# we are also a little apprehensive about pre-creating users
# before the user signs in, because we won't know things like
# the external service user identifier.
cls.import_permissions_from_yaml_file()
# def get_resource_set(self, public_access_token, uri):
# """Get_resource_set."""
# (
# open_id_server_url,
# open_id_client_id,
# open_id_realm_name,
# open_id_client_secret_key,
# ) = AuthorizationService.get_open_id_args()
# bearer_token = AuthorizationService().get_bearer_token(public_access_token)
# auth_bearer_string = f"Bearer {bearer_token['access_token']}"
# headers = {
# "Content-Type": "application/json",
# "Authorization": auth_bearer_string,
# }
# data = {
# "matchingUri": "true",
# "deep": "true",
# "max": "-1",
# "exactName": "false",
# "uri": uri,
# }
#
# # f"matchingUri=true&deep=true&max=-1&exactName=false&uri={URI_TO_TEST_AGAINST}"
# request_url = f"{open_id_server_url}/realms/{open_id_realm_name}/authz/protection/resource_set"
# response = requests.get(request_url, headers=headers, data=data)
#
# print("get_resource_set")
if is_new_user:
UserService.add_user_to_active_tasks_if_appropriate(user_model)
# def get_permission_by_token(self, public_access_token: str) -> dict:
# """Get_permission_by_token."""
# # TODO: Write a test for this
# (
# open_id_server_url,
# open_id_client_id,
# open_id_realm_name,
# open_id_client_secret_key,
# ) = AuthorizationService.get_open_id_args()
# bearer_token = AuthorizationService().get_bearer_token(public_access_token)
# auth_bearer_string = f"Bearer {bearer_token['access_token']}"
# headers = {
# "Content-Type": "application/x-www-form-urlencoded",
# "Authorization": auth_bearer_string,
# }
# data = {
# "grant_type": "urn:ietf:params:oauth:grant-type:uma-ticket",
# "audience": open_id_client_id,
# }
# request_url = f"{open_id_server_url}/realms/{open_id_realm_name}/protocol/openid-connect/token"
# permission_response = requests.post(request_url, headers=headers, data=data)
# permission: dict = json.loads(permission_response.text)
#
# return permission
# this cannot be None so ignore mypy
return user_model # type: ignore
class KeycloakAuthorization:

View File

@ -4,6 +4,7 @@ import decimal
import json
import logging
import os
import re
import time
from datetime import datetime
from typing import Any
@ -55,9 +56,11 @@ from SpiffWorkflow.task import TaskState
from SpiffWorkflow.util.deep_merge import DeepMerge # type: ignore
from spiffworkflow_backend.models.active_task import ActiveTaskModel
from spiffworkflow_backend.models.active_task_user import ActiveTaskUserModel
from spiffworkflow_backend.models.bpmn_process_id_lookup import BpmnProcessIdLookup
from spiffworkflow_backend.models.file import File
from spiffworkflow_backend.models.file import FileType
from spiffworkflow_backend.models.group import GroupModel
from spiffworkflow_backend.models.message_correlation import MessageCorrelationModel
from spiffworkflow_backend.models.message_correlation_message_instance import (
MessageCorrelationMessageInstanceModel,
@ -67,7 +70,6 @@ from spiffworkflow_backend.models.message_correlation_property import (
)
from spiffworkflow_backend.models.message_instance import MessageInstanceModel
from spiffworkflow_backend.models.message_instance import MessageModel
from spiffworkflow_backend.models.principal import PrincipalModel
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
from spiffworkflow_backend.models.process_instance import ProcessInstanceStatus
from spiffworkflow_backend.models.process_model import ProcessModelInfo
@ -108,6 +110,10 @@ class ProcessInstanceProcessorError(Exception):
"""ProcessInstanceProcessorError."""
class NoPotentialOwnersForTaskError(Exception):
"""NoPotentialOwnersForTaskError."""
class CustomBpmnScriptEngine(PythonScriptEngine): # type: ignore
"""This is a custom script processor that can be easily injected into Spiff Workflow.
@ -511,28 +517,46 @@ class ProcessInstanceProcessor:
if self.bpmn_process_instance.is_completed():
self.process_instance_model.end_in_seconds = round(time.time())
db.session.add(self.process_instance_model)
ActiveTaskModel.query.filter_by(
active_tasks = ActiveTaskModel.query.filter_by(
process_instance_id=self.process_instance_model.id
).delete()
).all()
if len(active_tasks) > 0:
for at in active_tasks:
db.session.delete(at)
db.session.add(self.process_instance_model)
db.session.commit()
ready_or_waiting_tasks = self.get_all_ready_or_waiting_tasks()
for ready_or_waiting_task in ready_or_waiting_tasks:
# filter out non-usertasks
if not self.bpmn_process_instance._is_engine_task(
ready_or_waiting_task.task_spec
):
user_id = ready_or_waiting_task.data["current_user"]["id"]
principal = PrincipalModel.query.filter_by(user_id=user_id).first()
if principal is None:
raise (
ApiError(
error_code="principal_not_found",
message=f"Principal not found from user id: {user_id}",
status_code=400,
task_spec = ready_or_waiting_task.task_spec
if not self.bpmn_process_instance._is_engine_task(task_spec):
ready_or_waiting_task.data["current_user"]["id"]
task_lane = "process_initiator"
if task_spec.lane is not None and task_spec.lane != "":
task_lane = task_spec.lane
potential_owner_ids = []
lane_assignment_id = None
if re.match(r"(process.?)initiator", task_lane, re.IGNORECASE):
potential_owner_ids = [
self.process_instance_model.process_initiator_id
]
else:
group_model = GroupModel.query.filter_by(
identifier=task_lane
).first()
if group_model is None:
raise (
NoPotentialOwnersForTaskError(
f"Could not find a group with name matching lane: {task_lane}"
)
)
)
potential_owner_ids = [
i.user_id for i in group_model.user_group_assignments
]
lane_assignment_id = group_model.id
extensions = ready_or_waiting_task.task_spec.extensions
@ -555,7 +579,6 @@ class ProcessInstanceProcessor:
active_task = ActiveTaskModel(
process_instance_id=self.process_instance_model.id,
process_model_display_name=process_model_display_name,
assigned_principal_id=principal.id,
form_file_name=form_file_name,
ui_form_file_name=ui_form_file_name,
task_id=str(ready_or_waiting_task.id),
@ -563,10 +586,17 @@ class ProcessInstanceProcessor:
task_title=ready_or_waiting_task.task_spec.description,
task_type=ready_or_waiting_task.task_spec.__class__.__name__,
task_status=ready_or_waiting_task.get_state_name(),
lane_assignment_id=lane_assignment_id,
)
db.session.add(active_task)
db.session.commit()
db.session.commit()
for potential_owner_id in potential_owner_ids:
active_task_user = ActiveTaskUserModel(
user_id=potential_owner_id, active_task_id=active_task.id
)
db.session.add(active_task_user)
db.session.commit()
@staticmethod
def get_parser() -> MyCustomParser:

View File

@ -19,14 +19,13 @@ from spiffworkflow_backend.models.task import Task
from spiffworkflow_backend.models.task_event import TaskAction
from spiffworkflow_backend.models.task_event import TaskEventModel
from spiffworkflow_backend.models.user import UserModel
from spiffworkflow_backend.services.authorization_service import AuthorizationService
from spiffworkflow_backend.services.git_service import GitService
from spiffworkflow_backend.services.process_instance_processor import (
ProcessInstanceProcessor,
)
from spiffworkflow_backend.services.process_model_service import ProcessModelService
# from SpiffWorkflow.task import TaskState # type: ignore
class ProcessInstanceService:
"""ProcessInstanceService."""
@ -74,8 +73,8 @@ class ProcessInstanceService:
process_instance.status = ProcessInstanceStatus.erroring.value
db.session.add(process_instance)
db.session.commit()
error_message = f"Error running waiting task for process_instance {process_instance.id}"
"({process_instance.process_model_identifier}). {str(e)}"
error_message = f"Error running waiting task for process_instance {process_instance.id}" + \
"({process_instance.process_model_identifier}). {str(e)}"
current_app.logger.error(error_message)
@staticmethod
@ -272,6 +271,10 @@ class ProcessInstanceService:
Abstracted here because we need to do it multiple times when completing all tasks in
a multi-instance task.
"""
AuthorizationService.assert_user_can_complete_spiff_task(
processor, spiff_task, user
)
dot_dct = ProcessInstanceService.create_dot_dict(data)
spiff_task.update_data(dot_dct)
# ProcessInstanceService.post_process_form(spiff_task) # some properties may update the data store.
@ -282,8 +285,7 @@ class ProcessInstanceService:
ProcessInstanceService.log_task_action(
user.id, processor, spiff_task, TaskAction.COMPLETE.value
)
processor.do_engine_steps()
processor.save()
processor.do_engine_steps(save=True)
@staticmethod
def log_task_action(

View File

@ -22,11 +22,8 @@ class ServiceTaskDelegate:
"""ServiceTaskDelegate."""
@staticmethod
def normalize_value(value: Any) -> Any:
"""Normalize_value."""
if isinstance(value, dict):
value = json.dumps(value)
def check_prefixes(value: Any) -> Any:
"""Check_prefixes."""
if isinstance(value, str):
secret_prefix = "secret:" # noqa: S105
if value.startswith(secret_prefix):
@ -48,13 +45,13 @@ class ServiceTaskDelegate:
def call_connector(name: str, bpmn_params: Any, task_data: Any) -> str:
"""Calls a connector via the configured proxy."""
params = {
k: ServiceTaskDelegate.normalize_value(v["value"])
k: ServiceTaskDelegate.check_prefixes(v["value"])
for k, v in bpmn_params.items()
}
params["spiff__task_data"] = json.dumps(task_data)
params["spiff__task_data"] = task_data
proxied_response = requests.post(
f"{connector_proxy_url()}/v1/do/{name}", params
f"{connector_proxy_url()}/v1/do/{name}", json=params
)
if proxied_response.status_code != 200:

View File

@ -7,6 +7,8 @@ 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
from spiffworkflow_backend.models.group import GroupModel
from spiffworkflow_backend.models.principal import PrincipalModel
from spiffworkflow_backend.models.user import AdminSessionModel
@ -313,3 +315,17 @@ class UserService:
if user:
return user
return None
@classmethod
def add_user_to_active_tasks_if_appropriate(cls, user: UserModel) -> None:
"""Add_user_to_active_tasks_if_appropriate."""
group_ids = [g.id for g in user.groups]
active_tasks = ActiveTaskModel.query.filter(
ActiveTaskModel.lane_assignment_id.in_(group_ids) # type: ignore
).all()
for active_task in active_tasks:
active_task_user = ActiveTaskUserModel(
user_id=user.id, active_task_id=active_task.id
)
db.session.add(active_task_user)
db.session.commit()

View File

@ -0,0 +1,103 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:spiffworkflow="http://spiffworkflow.org/bpmn/schema/1.0/core" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_96f6665" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.0.0-dev">
<bpmn:collaboration id="Collaboration_0iyw0q7">
<bpmn:participant id="Participant_17eqap4" processRef="Proccess_yhito9d" />
</bpmn:collaboration>
<bpmn:process id="Proccess_yhito9d" isExecutable="true">
<bpmn:laneSet id="LaneSet_17rankp">
<bpmn:lane id="process_initiator" name="Process Initiator">
<bpmn:flowNodeRef>StartEvent_1</bpmn:flowNodeRef>
<bpmn:flowNodeRef>initator_one</bpmn:flowNodeRef>
<bpmn:flowNodeRef>Event_06f4e68</bpmn:flowNodeRef>
<bpmn:flowNodeRef>initiator_two</bpmn:flowNodeRef>
</bpmn:lane>
<bpmn:lane id="finance_team" name="Finance Team">
<bpmn:flowNodeRef>finance_approval</bpmn:flowNodeRef>
</bpmn:lane>
</bpmn:laneSet>
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>Flow_1tbyols</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="Flow_1tbyols" sourceRef="StartEvent_1" targetRef="initator_one" />
<bpmn:sequenceFlow id="Flow_16ppta1" sourceRef="initator_one" targetRef="finance_approval" />
<bpmn:manualTask id="initator_one" name="Initiator One">
<bpmn:extensionElements>
<spiffworkflow:instructionsForEndUser>This is initiator user?</spiffworkflow:instructionsForEndUser>
</bpmn:extensionElements>
<bpmn:incoming>Flow_1tbyols</bpmn:incoming>
<bpmn:outgoing>Flow_16ppta1</bpmn:outgoing>
</bpmn:manualTask>
<bpmn:manualTask id="finance_approval" name="Finance Approval">
<bpmn:extensionElements>
<spiffworkflow:instructionsForEndUser>This is finance user?</spiffworkflow:instructionsForEndUser>
</bpmn:extensionElements>
<bpmn:incoming>Flow_16ppta1</bpmn:incoming>
<bpmn:outgoing>Flow_1cfcauf</bpmn:outgoing>
</bpmn:manualTask>
<bpmn:sequenceFlow id="Flow_1cfcauf" sourceRef="finance_approval" targetRef="initiator_two" />
<bpmn:endEvent id="Event_06f4e68">
<bpmn:incoming>Flow_0x92f7d</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_0x92f7d" sourceRef="initiator_two" targetRef="Event_06f4e68" />
<bpmn:manualTask id="initiator_two" name="Initiator Two">
<bpmn:extensionElements>
<spiffworkflow:instructionsForEndUser>This is initiator again?</spiffworkflow:instructionsForEndUser>
</bpmn:extensionElements>
<bpmn:incoming>Flow_1cfcauf</bpmn:incoming>
<bpmn:outgoing>Flow_0x92f7d</bpmn:outgoing>
</bpmn:manualTask>
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Collaboration_0iyw0q7">
<bpmndi:BPMNShape id="Participant_17eqap4_di" bpmnElement="Participant_17eqap4" isHorizontal="true">
<dc:Bounds x="129" y="52" width="600" height="370" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Lane_0irvyol_di" bpmnElement="finance_team" isHorizontal="true">
<dc:Bounds x="159" y="302" width="570" height="120" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Lane_1ewsife_di" bpmnElement="process_initiator" isHorizontal="true">
<dc:Bounds x="159" y="52" width="570" height="250" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="179" y="159" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1lm1ald_di" bpmnElement="initator_one">
<dc:Bounds x="270" y="137" width="100" height="80" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1y566d5_di" bpmnElement="finance_approval">
<dc:Bounds x="310" y="320" width="100" height="80" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_06f4e68_di" bpmnElement="Event_06f4e68">
<dc:Bounds x="572" y="159" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1c1xxe3_di" bpmnElement="initiator_two">
<dc:Bounds x="440" y="137" width="100" height="80" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_1tbyols_di" bpmnElement="Flow_1tbyols">
<di:waypoint x="215" y="177" />
<di:waypoint x="270" y="177" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_16ppta1_di" bpmnElement="Flow_16ppta1">
<di:waypoint x="320" y="217" />
<di:waypoint x="320" y="269" />
<di:waypoint x="360" y="269" />
<di:waypoint x="360" y="320" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1cfcauf_di" bpmnElement="Flow_1cfcauf">
<di:waypoint x="410" y="360" />
<di:waypoint x="425" y="360" />
<di:waypoint x="425" y="177" />
<di:waypoint x="440" y="177" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0x92f7d_di" bpmnElement="Flow_0x92f7d">
<di:waypoint x="540" y="177" />
<di:waypoint x="572" y="177" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -207,10 +207,15 @@ class BaseTest:
# return public_access_token
def create_process_instance_from_process_model(
self, process_model: ProcessModelInfo, status: Optional[str] = "not_started"
self,
process_model: ProcessModelInfo,
status: Optional[str] = "not_started",
user: Optional[UserModel] = None,
) -> ProcessInstanceModel:
"""Create_process_instance_from_process_model."""
user = self.find_or_create_user()
if user is None:
user = self.find_or_create_user()
current_time = round(time.time())
process_instance = ProcessInstanceModel(
status=status,

View File

@ -15,6 +15,7 @@ from spiffworkflow_backend.exceptions.process_entity_not_found_error import (
ProcessEntityNotFoundError,
)
from spiffworkflow_backend.models.active_task import ActiveTaskModel
from spiffworkflow_backend.models.group import GroupModel
from spiffworkflow_backend.models.process_group import ProcessGroup
from spiffworkflow_backend.models.process_group import ProcessGroupSchema
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
@ -26,6 +27,7 @@ from spiffworkflow_backend.models.process_model import NotificationType
from spiffworkflow_backend.models.process_model import ProcessModelInfoSchema
from spiffworkflow_backend.models.task_event import TaskEventModel
from spiffworkflow_backend.models.user import UserModel
from spiffworkflow_backend.services.authorization_service import AuthorizationService
from spiffworkflow_backend.services.file_system_service import FileSystemService
from spiffworkflow_backend.services.process_instance_processor import (
ProcessInstanceProcessor,
@ -1772,6 +1774,94 @@ class TestProcessApi(BaseTest):
assert response.json is not None
assert len(response.json["results"]) == 2
def test_correct_user_can_get_and_update_a_task(
self,
app: Flask,
client: FlaskClient,
with_db_and_bpmn_file_cleanup: None,
with_super_admin_user: UserModel,
) -> None:
"""Test_correct_user_can_get_and_update_a_task."""
initiator_user = self.find_or_create_user("testuser4")
finance_user = self.find_or_create_user("testuser2")
assert initiator_user.principal is not None
assert finance_user.principal is not None
AuthorizationService.import_permissions_from_yaml_file()
finance_group = GroupModel.query.filter_by(identifier="Finance Team").first()
assert finance_group is not None
process_model = load_test_spec(
process_model_id="model_with_lanes",
bpmn_file_name="lanes.bpmn",
process_group_id="finance",
)
response = self.create_process_instance(
client,
process_model.process_group_id,
process_model.id,
headers=self.logged_in_headers(initiator_user),
)
assert response.status_code == 201
assert response.json is not None
process_instance_id = response.json["id"]
response = client.post(
f"/v1.0/process-models/{process_model.process_group_id}/{process_model.id}/process-instances/{process_instance_id}/run",
headers=self.logged_in_headers(initiator_user),
)
assert response.status_code == 200
response = client.get(
"/v1.0/tasks",
headers=self.logged_in_headers(finance_user),
)
assert response.status_code == 200
assert response.json is not None
assert len(response.json["results"]) == 0
response = client.get(
"/v1.0/tasks",
headers=self.logged_in_headers(initiator_user),
)
assert response.status_code == 200
assert response.json is not None
assert len(response.json["results"]) == 1
task_id = response.json["results"][0]["id"]
assert task_id is not None
response = client.put(
f"/v1.0/tasks/{process_instance_id}/{task_id}",
headers=self.logged_in_headers(finance_user),
)
assert response.status_code == 500
assert response.json
assert "UserDoesNotHaveAccessToTaskError" in response.json["message"]
response = client.put(
f"/v1.0/tasks/{process_instance_id}/{task_id}",
headers=self.logged_in_headers(initiator_user),
)
assert response.status_code == 202
response = client.get(
"/v1.0/tasks",
headers=self.logged_in_headers(initiator_user),
)
assert response.status_code == 200
assert response.json is not None
assert len(response.json["results"]) == 0
response = client.get(
"/v1.0/tasks",
headers=self.logged_in_headers(finance_user),
)
assert response.status_code == 200
assert response.json is not None
assert len(response.json["results"]) == 1
# TODO: test the auth callback endpoint
# def test_can_store_authentication_secret(
# self, app: Flask, client: FlaskClient, with_db_and_bpmn_file_cleanup: None

View File

@ -2,9 +2,16 @@
import pytest
from flask import Flask
from tests.spiffworkflow_backend.helpers.base_test import BaseTest
from tests.spiffworkflow_backend.helpers.test_data import load_test_spec
from spiffworkflow_backend.models.user import UserNotFoundError
from spiffworkflow_backend.services.authorization_service import AuthorizationService
from spiffworkflow_backend.services.process_instance_processor import (
ProcessInstanceProcessor,
)
from spiffworkflow_backend.services.process_instance_service import (
ProcessInstanceService,
)
class TestAuthorizationService(BaseTest):
@ -19,6 +26,12 @@ class TestAuthorizationService(BaseTest):
raise_if_missing_user=True
)
def test_does_not_fail_if_user_not_created(
self, app: Flask, with_db_and_bpmn_file_cleanup: None
) -> None:
"""Test_does_not_fail_if_user_not_created."""
AuthorizationService.import_permissions_from_yaml_file()
def test_can_import_permissions_from_yaml(
self, app: Flask, with_db_and_bpmn_file_cleanup: None
) -> None:
@ -37,11 +50,17 @@ class TestAuthorizationService(BaseTest):
users[username] = user
AuthorizationService.import_permissions_from_yaml_file()
assert len(users["testadmin1"].groups) == 1
assert users["testadmin1"].groups[0].identifier == "admin"
assert len(users["testuser1"].groups) == 1
assert users["testuser1"].groups[0].identifier == "finance"
assert len(users["testuser2"].groups) == 2
assert len(users["testadmin1"].groups) == 2
testadmin1_group_identifiers = sorted(
[g.identifier for g in users["testadmin1"].groups]
)
assert testadmin1_group_identifiers == ["admin", "everybody"]
assert len(users["testuser1"].groups) == 2
testuser1_group_identifiers = sorted(
[g.identifier for g in users["testuser1"].groups]
)
assert testuser1_group_identifiers == ["Finance Team", "everybody"]
assert len(users["testuser2"].groups) == 3
self.assert_user_has_permission(
users["testuser1"], "update", "/v1.0/process-groups/finance/model1"
@ -55,6 +74,7 @@ class TestAuthorizationService(BaseTest):
self.assert_user_has_permission(
users["testuser4"], "update", "/v1.0/process-groups/finance/model1"
)
# via the user, not the group
self.assert_user_has_permission(
users["testuser4"], "read", "/v1.0/process-groups/finance/model1"
)
@ -67,3 +87,38 @@ class TestAuthorizationService(BaseTest):
self.assert_user_has_permission(
users["testuser2"], "read", "/v1.0/process-groups/"
)
def test_user_can_be_added_to_active_task_on_first_login(
self, app: Flask, with_db_and_bpmn_file_cleanup: None
) -> None:
"""Test_user_can_be_added_to_active_task_on_first_login."""
initiator_user = self.find_or_create_user("initiator_user")
assert initiator_user.principal is not None
AuthorizationService.import_permissions_from_yaml_file()
process_model = load_test_spec(
process_model_id="model_with_lanes", bpmn_file_name="lanes.bpmn"
)
process_instance = self.create_process_instance_from_process_model(
process_model=process_model, user=initiator_user
)
processor = ProcessInstanceProcessor(process_instance)
processor.do_engine_steps(save=True)
active_task = process_instance.active_tasks[0]
spiff_task = processor.__class__.get_task_by_bpmn_identifier(
active_task.task_name, processor.bpmn_process_instance
)
ProcessInstanceService.complete_form_task(
processor, spiff_task, {}, initiator_user
)
active_task = process_instance.active_tasks[0]
spiff_task = processor.__class__.get_task_by_bpmn_identifier(
active_task.task_name, processor.bpmn_process_instance
)
finance_user = AuthorizationService.create_user_from_sign_in(
{"username": "testuser2", "sub": "open_id"}
)
ProcessInstanceService.complete_form_task(
processor, spiff_task, {}, finance_user
)

View File

@ -1,10 +1,21 @@
"""Test_process_instance_processor."""
import pytest
from flask.app import Flask
from tests.spiffworkflow_backend.helpers.base_test import BaseTest
from tests.spiffworkflow_backend.helpers.test_data import load_test_spec
from spiffworkflow_backend.models.group import GroupModel
from spiffworkflow_backend.models.process_instance import ProcessInstanceStatus
from spiffworkflow_backend.services.authorization_service import AuthorizationService
from spiffworkflow_backend.services.authorization_service import (
UserDoesNotHaveAccessToTaskError,
)
from spiffworkflow_backend.services.process_instance_processor import (
ProcessInstanceProcessor,
)
from spiffworkflow_backend.services.process_instance_service import (
ProcessInstanceService,
)
class TestProcessInstanceProcessor(BaseTest):
@ -34,3 +45,76 @@ class TestProcessInstanceProcessor(BaseTest):
result
== "Chuck Norris doesnt read books. He stares them down until he gets the information he wants."
)
def test_sets_permission_correctly_on_active_task(
self,
app: Flask,
with_db_and_bpmn_file_cleanup: None,
) -> None:
"""Test_sets_permission_correctly_on_active_task."""
initiator_user = self.find_or_create_user("initiator_user")
finance_user = self.find_or_create_user("testuser2")
assert initiator_user.principal is not None
assert finance_user.principal is not None
AuthorizationService.import_permissions_from_yaml_file()
finance_group = GroupModel.query.filter_by(identifier="Finance Team").first()
assert finance_group is not None
process_model = load_test_spec(
process_model_id="model_with_lanes", bpmn_file_name="lanes.bpmn"
)
process_instance = self.create_process_instance_from_process_model(
process_model=process_model, user=initiator_user
)
processor = ProcessInstanceProcessor(process_instance)
processor.do_engine_steps(save=True)
assert len(process_instance.active_tasks) == 1
active_task = process_instance.active_tasks[0]
assert active_task.lane_assignment_id is None
assert len(active_task.potential_owners) == 1
assert active_task.potential_owners[0] == initiator_user
spiff_task = processor.__class__.get_task_by_bpmn_identifier(
active_task.task_name, processor.bpmn_process_instance
)
with pytest.raises(UserDoesNotHaveAccessToTaskError):
ProcessInstanceService.complete_form_task(
processor, spiff_task, {}, finance_user
)
ProcessInstanceService.complete_form_task(
processor, spiff_task, {}, initiator_user
)
assert len(process_instance.active_tasks) == 1
active_task = process_instance.active_tasks[0]
assert active_task.lane_assignment_id == finance_group.id
assert len(active_task.potential_owners) == 1
assert active_task.potential_owners[0] == finance_user
spiff_task = processor.__class__.get_task_by_bpmn_identifier(
active_task.task_name, processor.bpmn_process_instance
)
with pytest.raises(UserDoesNotHaveAccessToTaskError):
ProcessInstanceService.complete_form_task(
processor, spiff_task, {}, initiator_user
)
ProcessInstanceService.complete_form_task(
processor, spiff_task, {}, finance_user
)
assert len(process_instance.active_tasks) == 1
active_task = process_instance.active_tasks[0]
assert active_task.lane_assignment_id is None
assert len(active_task.potential_owners) == 1
assert active_task.potential_owners[0] == initiator_user
spiff_task = processor.__class__.get_task_by_bpmn_identifier(
active_task.task_name, processor.bpmn_process_instance
)
ProcessInstanceService.complete_form_task(
processor, spiff_task, {}, initiator_user
)
assert process_instance.status == ProcessInstanceStatus.complete.value

View File

@ -9,25 +9,25 @@ from spiffworkflow_backend.services.service_task_service import ServiceTaskDeleg
class TestServiceTaskDelegate(BaseTest):
"""TestServiceTaskDelegate."""
def test_normalize_value_without_secret(
def test_check_prefixes_without_secret(
self, app: Flask, with_db_and_bpmn_file_cleanup: None
) -> None:
"""Test_normalize_value_without_secret."""
result = ServiceTaskDelegate.normalize_value("hey")
"""Test_check_prefixes_without_secret."""
result = ServiceTaskDelegate.check_prefixes("hey")
assert result == "hey"
def test_normalize_value_with_int(
def test_check_prefixes_with_int(
self, app: Flask, with_db_and_bpmn_file_cleanup: None
) -> None:
"""Test_normalize_value_with_int."""
result = ServiceTaskDelegate.normalize_value(1)
"""Test_check_prefixes_with_int."""
result = ServiceTaskDelegate.check_prefixes(1)
assert result == 1
def test_normalize_value_with_secret(
def test_check_prefixes_with_secret(
self, app: Flask, with_db_and_bpmn_file_cleanup: None
) -> None:
"""Test_normalize_value_with_secret."""
"""Test_check_prefixes_with_secret."""
user = self.find_or_create_user("test_user")
SecretService().add_secret("hot_secret", "my_secret_value", user.id)
result = ServiceTaskDelegate.normalize_value("secret:hot_secret")
result = ServiceTaskDelegate.check_prefixes("secret:hot_secret")
assert result == "my_secret_value"