mirror of
https://github.com/sartography/spiffworkflow-backend.git
synced 2025-02-23 21:08:18 +00:00
Merge pull request #148 from sartography/feature/respect_lanes
Feature/respect lanes
This commit is contained in:
commit
b978f502a0
2
.gitignore
vendored
2
.gitignore
vendored
@ -18,4 +18,4 @@ node_modules
|
|||||||
/tests/spiffworkflow_backend/files
|
/tests/spiffworkflow_backend/files
|
||||||
/bin/import_secrets.py
|
/bin/import_secrets.py
|
||||||
/src/spiffworkflow_backend/config/secrets.py
|
/src/spiffworkflow_backend/config/secrets.py
|
||||||
_null-ls_*
|
*null-ls_*
|
||||||
|
@ -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.base_test import BaseTest
|
||||||
from tests.spiffworkflow_backend.helpers.test_data import load_test_spec
|
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.process_instance import ProcessInstanceModel
|
||||||
from spiffworkflow_backend.models.user import UserModel
|
from spiffworkflow_backend.models.user import UserModel
|
||||||
from spiffworkflow_backend.services.process_instance_processor import (
|
from spiffworkflow_backend.services.process_instance_processor import (
|
||||||
@ -56,6 +57,8 @@ def app() -> Flask:
|
|||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def with_db_and_bpmn_file_cleanup() -> None:
|
def with_db_and_bpmn_file_cleanup() -> None:
|
||||||
"""Process_group_resource."""
|
"""Process_group_resource."""
|
||||||
|
db.session.query(ActiveTaskUserModel).delete()
|
||||||
|
|
||||||
for model in SpiffworkflowBaseDBModel._all_subclasses():
|
for model in SpiffworkflowBaseDBModel._all_subclasses():
|
||||||
db.session.query(model).delete()
|
db.session.query(model).delete()
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
"""empty message
|
"""empty message
|
||||||
|
|
||||||
Revision ID: e12e98d4e7e4
|
Revision ID: 4ba2ed52a63a
|
||||||
Revises:
|
Revises:
|
||||||
Create Date: 2022-10-21 08:53:52.815491
|
Create Date: 2022-10-21 09:31:30.520942
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from alembic import op
|
from alembic import op
|
||||||
@ -10,7 +10,7 @@ import sqlalchemy as sa
|
|||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
revision = 'e12e98d4e7e4'
|
revision = '4ba2ed52a63a'
|
||||||
down_revision = None
|
down_revision = None
|
||||||
branch_labels = None
|
branch_labels = None
|
||||||
depends_on = None
|
depends_on = None
|
||||||
@ -165,7 +165,8 @@ def upgrade():
|
|||||||
op.create_table('active_task',
|
op.create_table('active_task',
|
||||||
sa.Column('id', sa.Integer(), nullable=False),
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
sa.Column('process_instance_id', sa.Integer(), nullable=False),
|
sa.Column('process_instance_id', sa.Integer(), nullable=False),
|
||||||
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('form_file_name', sa.String(length=50), nullable=True),
|
||||||
sa.Column('ui_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),
|
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_type', sa.String(length=50), nullable=True),
|
||||||
sa.Column('task_status', 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.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.ForeignKeyConstraint(['process_instance_id'], ['process_instance.id'], ),
|
||||||
sa.PrimaryKeyConstraint('id'),
|
sa.PrimaryKeyConstraint('id'),
|
||||||
sa.UniqueConstraint('task_id', 'process_instance_id', name='active_task_unique')
|
sa.UniqueConstraint('task_id', 'process_instance_id', name='active_task_unique')
|
||||||
@ -279,6 +281,17 @@ def upgrade():
|
|||||||
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
|
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
|
||||||
sa.PrimaryKeyConstraint('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',
|
op.create_table('data_store',
|
||||||
sa.Column('id', sa.Integer(), nullable=False),
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
sa.Column('updated_at_in_seconds', sa.Integer(), nullable=True),
|
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_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('message_correlation_message_instance')
|
||||||
op.drop_table('data_store')
|
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('task_event')
|
||||||
op.drop_table('spiff_logging')
|
op.drop_table('spiff_logging')
|
||||||
op.drop_table('permission_assignment')
|
op.drop_table('permission_assignment')
|
38
poetry.lock
generated
38
poetry.lock
generated
@ -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"]
|
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"]
|
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 = ["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]]
|
[[package]]
|
||||||
name = "Babel"
|
name = "Babel"
|
||||||
@ -268,7 +268,7 @@ optional = false
|
|||||||
python-versions = ">=3.6.0"
|
python-versions = ">=3.6.0"
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
unicode_backport = ["unicodedata2"]
|
unicode-backport = ["unicodedata2"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "classify-imports"
|
name = "classify-imports"
|
||||||
@ -618,7 +618,7 @@ description = "Flask Bpmn"
|
|||||||
category = "main"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "^3.7"
|
python-versions = "^3.7"
|
||||||
develop = false
|
develop = true
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
click = "^8.0.1"
|
click = "^8.0.1"
|
||||||
@ -636,10 +636,8 @@ spiffworkflow = "*"
|
|||||||
werkzeug = "*"
|
werkzeug = "*"
|
||||||
|
|
||||||
[package.source]
|
[package.source]
|
||||||
type = "git"
|
type = "directory"
|
||||||
url = "https://github.com/sartography/flask-bpmn"
|
url = "../flask-bpmn"
|
||||||
reference = "main"
|
|
||||||
resolved_reference = "c8fd01df47518749a074772fec383256c482139f"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "Flask-Cors"
|
name = "Flask-Cors"
|
||||||
@ -1512,7 +1510,7 @@ urllib3 = ">=1.21.1,<1.27"
|
|||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
socks = ["PySocks (>=1.5.6,!=1.5.7)"]
|
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]]
|
[[package]]
|
||||||
name = "requests-toolbelt"
|
name = "requests-toolbelt"
|
||||||
@ -1604,7 +1602,7 @@ gitlab = ["python-gitlab (>=1.3.0)"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sentry-sdk"
|
name = "sentry-sdk"
|
||||||
version = "1.9.10"
|
version = "1.10.1"
|
||||||
description = "Python client for Sentry (https://sentry.io)"
|
description = "Python client for Sentry (https://sentry.io)"
|
||||||
category = "main"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
@ -1625,7 +1623,7 @@ falcon = ["falcon (>=1.4)"]
|
|||||||
fastapi = ["fastapi (>=0.79.0)"]
|
fastapi = ["fastapi (>=0.79.0)"]
|
||||||
flask = ["blinker (>=1.1)", "flask (>=0.11)"]
|
flask = ["blinker (>=1.1)", "flask (>=0.11)"]
|
||||||
httpx = ["httpx (>=0.16.0)"]
|
httpx = ["httpx (>=0.16.0)"]
|
||||||
pure_eval = ["asttokens", "executing", "pure-eval"]
|
pure-eval = ["asttokens", "executing", "pure-eval"]
|
||||||
pyspark = ["pyspark (>=2.4.4)"]
|
pyspark = ["pyspark (>=2.4.4)"]
|
||||||
quart = ["blinker (>=1.1)", "quart (>=0.16.1)"]
|
quart = ["blinker (>=1.1)", "quart (>=0.16.1)"]
|
||||||
rq = ["rq (>=0.6)"]
|
rq = ["rq (>=0.6)"]
|
||||||
@ -1891,19 +1889,19 @@ aiomysql = ["aiomysql", "greenlet (!=0.4.17)"]
|
|||||||
aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing_extensions (!=3.10.0.1)"]
|
aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing_extensions (!=3.10.0.1)"]
|
||||||
asyncio = ["greenlet (!=0.4.17)"]
|
asyncio = ["greenlet (!=0.4.17)"]
|
||||||
asyncmy = ["asyncmy (>=0.2.3,!=0.2.4)", "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 = ["pyodbc"]
|
||||||
mssql_pymssql = ["pymssql"]
|
mssql-pymssql = ["pymssql"]
|
||||||
mssql_pyodbc = ["pyodbc"]
|
mssql-pyodbc = ["pyodbc"]
|
||||||
mypy = ["mypy (>=0.910)", "sqlalchemy2-stubs"]
|
mypy = ["mypy (>=0.910)", "sqlalchemy2-stubs"]
|
||||||
mysql = ["mysqlclient (>=1.4.0)", "mysqlclient (>=1.4.0,<2)"]
|
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)"]
|
oracle = ["cx_oracle (>=7)", "cx_oracle (>=7,<8)"]
|
||||||
postgresql = ["psycopg2 (>=2.7)"]
|
postgresql = ["psycopg2 (>=2.7)"]
|
||||||
postgresql_asyncpg = ["asyncpg", "greenlet (!=0.4.17)"]
|
postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"]
|
||||||
postgresql_pg8000 = ["pg8000 (>=1.16.6,!=1.29.0)"]
|
postgresql-pg8000 = ["pg8000 (>=1.16.6,!=1.29.0)"]
|
||||||
postgresql_psycopg2binary = ["psycopg2-binary"]
|
postgresql-psycopg2binary = ["psycopg2-binary"]
|
||||||
postgresql_psycopg2cffi = ["psycopg2cffi"]
|
postgresql-psycopg2cffi = ["psycopg2cffi"]
|
||||||
pymysql = ["pymysql", "pymysql (<1)"]
|
pymysql = ["pymysql", "pymysql (<1)"]
|
||||||
sqlcipher = ["sqlcipher3_binary"]
|
sqlcipher = ["sqlcipher3_binary"]
|
||||||
|
|
||||||
@ -3323,8 +3321,8 @@ safety = [
|
|||||||
{file = "safety-2.3.1.tar.gz", hash = "sha256:6e6fcb7d4e8321098cf289f59b65051cafd3467f089c6e57c9f894ae32c23b71"},
|
{file = "safety-2.3.1.tar.gz", hash = "sha256:6e6fcb7d4e8321098cf289f59b65051cafd3467f089c6e57c9f894ae32c23b71"},
|
||||||
]
|
]
|
||||||
sentry-sdk = [
|
sentry-sdk = [
|
||||||
{file = "sentry-sdk-1.9.10.tar.gz", hash = "sha256:4fbace9a763285b608c06f01a807b51acb35f6059da6a01236654e08b0ee81ff"},
|
{file = "sentry-sdk-1.10.1.tar.gz", hash = "sha256:105faf7bd7b7fa25653404619ee261527266b14103fe1389e0ce077bd23a9691"},
|
||||||
{file = "sentry_sdk-1.9.10-py2.py3-none-any.whl", hash = "sha256:2469240f6190aaebcb453033519eae69cfe8cc602065b4667e18ee14fc1e35dc"},
|
{file = "sentry_sdk-1.10.1-py2.py3-none-any.whl", hash = "sha256:06c0fa9ccfdc80d7e3b5d2021978d6eb9351fa49db9b5847cf4d1f2a473414ad"},
|
||||||
]
|
]
|
||||||
setuptools = [
|
setuptools = [
|
||||||
{file = "setuptools-65.5.0-py3-none-any.whl", hash = "sha256:f62ea9da9ed6289bfe868cd6845968a2c854d1427f8548d52cae02a42b4f0356"},
|
{file = "setuptools-65.5.0-py3-none-any.whl", hash = "sha256:f62ea9da9ed6289bfe868cd6845968a2c854d1427f8548d52cae02a42b4f0356"},
|
||||||
|
@ -31,7 +31,7 @@ werkzeug = "*"
|
|||||||
SpiffWorkflow = {git = "https://github.com/sartography/SpiffWorkflow", rev = "main"}
|
SpiffWorkflow = {git = "https://github.com/sartography/SpiffWorkflow", rev = "main"}
|
||||||
# SpiffWorkflow = {develop = true, path = "/Users/kevin/projects/github/sartography/SpiffWorkflow"}
|
# SpiffWorkflow = {develop = true, path = "/Users/kevin/projects/github/sartography/SpiffWorkflow"}
|
||||||
# SpiffWorkflow = {develop = true, path = "/home/jason/projects/github/sartography/SpiffWorkflow"}
|
# SpiffWorkflow = {develop = true, path = "/home/jason/projects/github/sartography/SpiffWorkflow"}
|
||||||
sentry-sdk = "^1.9.10"
|
sentry-sdk = "^1.10"
|
||||||
sphinx-autoapi = "^1.8.4"
|
sphinx-autoapi = "^1.8.4"
|
||||||
# flask-bpmn = {develop = true, path = "/home/jason/projects/github/sartography/flask-bpmn"}
|
# 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 = {develop = true, path = "/Users/kevin/projects/github/sartography/flask-bpmn"}
|
||||||
|
@ -145,7 +145,6 @@ def get_hacked_up_app_for_script() -> flask.app.Flask:
|
|||||||
def configure_sentry(app: flask.app.Flask) -> None:
|
def configure_sentry(app: flask.app.Flask) -> None:
|
||||||
"""Configure_sentry."""
|
"""Configure_sentry."""
|
||||||
import sentry_sdk
|
import sentry_sdk
|
||||||
from flask import Flask
|
|
||||||
from sentry_sdk.integrations.flask import FlaskIntegration
|
from sentry_sdk.integrations.flask import FlaskIntegration
|
||||||
|
|
||||||
def before_send(event: Any, hint: Any) -> Any:
|
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),
|
traces_sample_rate=float(sentry_sample_rate),
|
||||||
before_send=before_send,
|
before_send=before_send,
|
||||||
)
|
)
|
||||||
|
|
||||||
app = Flask(__name__)
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
from os import environ
|
from os import environ
|
||||||
|
|
||||||
SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME = environ.get(
|
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(
|
SPIFFWORKFLOW_BACKEND_LOG_LEVEL = environ.get(
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
default_group: everybody
|
||||||
|
|
||||||
groups:
|
groups:
|
||||||
admin:
|
admin:
|
||||||
users:
|
users:
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
default_group: everybody
|
||||||
|
|
||||||
groups:
|
groups:
|
||||||
admin:
|
admin:
|
||||||
users:
|
users:
|
@ -1,8 +1,10 @@
|
|||||||
|
default_group: everybody
|
||||||
|
|
||||||
groups:
|
groups:
|
||||||
admin:
|
admin:
|
||||||
users: [testadmin1, testadmin2]
|
users: [testadmin1, testadmin2]
|
||||||
|
|
||||||
finance:
|
Finance Team:
|
||||||
users: [testuser1, testuser2]
|
users: [testuser1, testuser2]
|
||||||
|
|
||||||
hr:
|
hr:
|
||||||
@ -16,13 +18,26 @@ permissions:
|
|||||||
uri: /*
|
uri: /*
|
||||||
|
|
||||||
read-all:
|
read-all:
|
||||||
groups: [finance, hr, admin]
|
groups: ["Finance Team", hr, admin]
|
||||||
users: []
|
users: []
|
||||||
allowed_permissions: [read]
|
allowed_permissions: [read]
|
||||||
uri: /*
|
uri: /*
|
||||||
|
|
||||||
finance-admin:
|
tasks-crud:
|
||||||
groups: [finance]
|
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]
|
users: [testuser4]
|
||||||
allowed_permissions: [create, read, update, delete]
|
allowed_permissions: [create, read, update, delete]
|
||||||
uri: /v1.0/process-groups/finance/*
|
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/*
|
||||||
|
@ -10,10 +10,11 @@ avoid circular imports
|
|||||||
|
|
||||||
from flask_bpmn.models.db import add_listeners
|
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 (
|
from spiffworkflow_backend.models.user_group_assignment import (
|
||||||
UserGroupAssignmentModel,
|
UserGroupAssignmentModel,
|
||||||
) # noqa: F401
|
) # noqa: F401
|
||||||
|
from spiffworkflow_backend.models.principal import PrincipalModel # noqa: F401
|
||||||
|
|
||||||
|
|
||||||
from spiffworkflow_backend.models.active_task import ActiveTaskModel # 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 (
|
from spiffworkflow_backend.models.permission_target import (
|
||||||
PermissionTargetModel,
|
PermissionTargetModel,
|
||||||
) # noqa: F401
|
) # noqa: F401
|
||||||
from spiffworkflow_backend.models.principal import PrincipalModel # noqa: F401
|
|
||||||
from spiffworkflow_backend.models.process_instance import (
|
from spiffworkflow_backend.models.process_instance import (
|
||||||
ProcessInstanceModel,
|
ProcessInstanceModel,
|
||||||
) # noqa: F401
|
) # noqa: F401
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from flask_bpmn.models.db import db
|
from flask_bpmn.models.db import db
|
||||||
from flask_bpmn.models.db import SpiffworkflowBaseDBModel
|
from flask_bpmn.models.db import SpiffworkflowBaseDBModel
|
||||||
@ -9,9 +10,16 @@ from sqlalchemy import ForeignKey
|
|||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
from sqlalchemy.orm import RelationshipProperty
|
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.process_instance import ProcessInstanceModel
|
||||||
from spiffworkflow_backend.models.task import Task
|
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
|
@dataclass
|
||||||
@ -25,14 +33,13 @@ class ActiveTaskModel(SpiffworkflowBaseDBModel):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
assigned_principal: RelationshipProperty[PrincipalModel] = relationship(
|
actual_owner: RelationshipProperty[UserModel] = relationship(UserModel)
|
||||||
PrincipalModel
|
|
||||||
)
|
|
||||||
id: int = db.Column(db.Integer, primary_key=True)
|
id: int = db.Column(db.Integer, primary_key=True)
|
||||||
process_instance_id: int = db.Column(
|
process_instance_id: int = db.Column(
|
||||||
ForeignKey(ProcessInstanceModel.id), nullable=False # type: ignore
|
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))
|
form_file_name: str | None = db.Column(db.String(50))
|
||||||
ui_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))
|
task_status = db.Column(db.String(50))
|
||||||
process_model_display_name = db.Column(db.String(255))
|
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
|
@classmethod
|
||||||
def to_task(cls, task: ActiveTaskModel) -> Task:
|
def to_task(cls, task: ActiveTaskModel) -> Task:
|
||||||
"""To_task."""
|
"""To_task."""
|
||||||
|
32
src/spiffworkflow_backend/models/active_task_user.py
Normal file
32
src/spiffworkflow_backend/models/active_task_user.py
Normal 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)
|
@ -4,6 +4,7 @@ from dataclasses import dataclass
|
|||||||
from flask_bpmn.models.db import db
|
from flask_bpmn.models.db import db
|
||||||
from flask_bpmn.models.db import SpiffworkflowBaseDBModel
|
from flask_bpmn.models.db import SpiffworkflowBaseDBModel
|
||||||
from sqlalchemy import ForeignKey
|
from sqlalchemy import ForeignKey
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
from sqlalchemy.schema import CheckConstraint
|
from sqlalchemy.schema import CheckConstraint
|
||||||
|
|
||||||
from spiffworkflow_backend.models.group import GroupModel
|
from spiffworkflow_backend.models.group import GroupModel
|
||||||
@ -28,3 +29,6 @@ class PrincipalModel(SpiffworkflowBaseDBModel):
|
|||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
user_id = db.Column(ForeignKey(UserModel.id), nullable=True, unique=True)
|
user_id = db.Column(ForeignKey(UserModel.id), nullable=True, unique=True)
|
||||||
group_id = db.Column(ForeignKey(GroupModel.id), nullable=True, unique=True)
|
group_id = db.Column(ForeignKey(GroupModel.id), nullable=True, unique=True)
|
||||||
|
|
||||||
|
user = relationship("UserModel", viewonly=True)
|
||||||
|
group = relationship("GroupModel", viewonly=True)
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
"""User."""
|
"""User."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import jwt
|
import jwt
|
||||||
|
@ -28,12 +28,14 @@ from lxml import etree # type: ignore
|
|||||||
from lxml.builder import ElementMaker # type: ignore
|
from lxml.builder import ElementMaker # type: ignore
|
||||||
from SpiffWorkflow.task import Task as SpiffTask # type: ignore
|
from SpiffWorkflow.task import Task as SpiffTask # type: ignore
|
||||||
from SpiffWorkflow.task import TaskState
|
from SpiffWorkflow.task import TaskState
|
||||||
|
from sqlalchemy import asc
|
||||||
from sqlalchemy import desc
|
from sqlalchemy import desc
|
||||||
|
|
||||||
from spiffworkflow_backend.exceptions.process_entity_not_found_error import (
|
from spiffworkflow_backend.exceptions.process_entity_not_found_error import (
|
||||||
ProcessEntityNotFoundError,
|
ProcessEntityNotFoundError,
|
||||||
)
|
)
|
||||||
from spiffworkflow_backend.models.active_task import ActiveTaskModel
|
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.file import FileSchema
|
||||||
from spiffworkflow_backend.models.message_instance import MessageInstanceModel
|
from spiffworkflow_backend.models.message_instance import MessageInstanceModel
|
||||||
from spiffworkflow_backend.models.message_model import MessageModel
|
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:
|
def task_list_my_tasks(page: int = 1, per_page: int = 100) -> flask.wrappers.Response:
|
||||||
"""Task_list_my_tasks."""
|
"""Task_list_my_tasks."""
|
||||||
principal = find_principal_or_raise()
|
principal = find_principal_or_raise()
|
||||||
|
|
||||||
active_tasks = (
|
active_tasks = (
|
||||||
ActiveTaskModel.query.filter_by(assigned_principal_id=principal.id)
|
ActiveTaskModel.query.order_by(desc(ActiveTaskModel.id)) # type: ignore
|
||||||
.order_by(desc(ActiveTaskModel.id)) # type: ignore
|
|
||||||
.join(ProcessInstanceModel)
|
.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.
|
# just need this add_columns to add the process_model_identifier. Then add everything back that was removed.
|
||||||
.add_columns(
|
.add_columns(
|
||||||
ProcessInstanceModel.process_model_identifier,
|
ProcessInstanceModel.process_model_identifier,
|
||||||
@ -1085,18 +1087,15 @@ def task_submit(
|
|||||||
) -> flask.wrappers.Response:
|
) -> flask.wrappers.Response:
|
||||||
"""Task_submit_user_data."""
|
"""Task_submit_user_data."""
|
||||||
principal = find_principal_or_raise()
|
principal = find_principal_or_raise()
|
||||||
active_task_assigned_to_me = find_active_task_by_id_or_raise(
|
process_instance = find_process_instance_by_id_or_raise(process_instance_id)
|
||||||
process_instance_id, task_id, principal.id
|
|
||||||
)
|
|
||||||
|
|
||||||
process_instance = find_process_instance_by_id_or_raise(
|
|
||||||
active_task_assigned_to_me.process_instance_id
|
|
||||||
)
|
|
||||||
|
|
||||||
processor = ProcessInstanceProcessor(process_instance)
|
processor = ProcessInstanceProcessor(process_instance)
|
||||||
spiff_task = get_spiff_task_from_process_instance(
|
spiff_task = get_spiff_task_from_process_instance(
|
||||||
task_id, process_instance, processor=processor
|
task_id, process_instance, processor=processor
|
||||||
)
|
)
|
||||||
|
AuthorizationService.assert_user_can_complete_spiff_task(
|
||||||
|
processor, spiff_task, principal.user
|
||||||
|
)
|
||||||
|
|
||||||
if spiff_task.state != TaskState.READY:
|
if spiff_task.state != TaskState.READY:
|
||||||
raise (
|
raise (
|
||||||
@ -1110,10 +1109,6 @@ def task_submit(
|
|||||||
if terminate_loop and spiff_task.is_looping():
|
if terminate_loop and spiff_task.is_looping():
|
||||||
spiff_task.terminate_loop()
|
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)
|
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
|
# 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)
|
ProcessInstanceService.update_task_assignments(processor)
|
||||||
|
|
||||||
next_active_task_assigned_to_me = ActiveTaskModel.query.filter_by(
|
next_active_task_assigned_to_me = (
|
||||||
assigned_principal_id=principal.id, process_instance_id=process_instance.id
|
ActiveTaskModel.query.filter_by(process_instance_id=process_instance_id)
|
||||||
).first()
|
.order_by(asc(ActiveTaskModel.id)) # type: ignore
|
||||||
|
.join(ActiveTaskUserModel)
|
||||||
|
.filter_by(user_id=principal.user_id)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
if next_active_task_assigned_to_me:
|
if next_active_task_assigned_to_me:
|
||||||
return make_response(
|
return make_response(
|
||||||
jsonify(ActiveTaskModel.to_task(next_active_task_assigned_to_me)), 200
|
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
|
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(
|
def find_process_instance_by_id_or_raise(
|
||||||
process_instance_id: int,
|
process_instance_id: int,
|
||||||
) -> ProcessInstanceModel:
|
) -> ProcessInstanceModel:
|
||||||
|
@ -203,7 +203,6 @@ def login_return(code: str, state: str, session_state: str) -> Optional[Response
|
|||||||
"""Login_return."""
|
"""Login_return."""
|
||||||
state_dict = ast.literal_eval(base64.b64decode(state).decode("utf-8"))
|
state_dict = ast.literal_eval(base64.b64decode(state).decode("utf-8"))
|
||||||
state_redirect_url = state_dict["redirect_url"]
|
state_redirect_url = state_dict["redirect_url"]
|
||||||
|
|
||||||
auth_token_object = AuthenticationService().get_auth_token_object(code)
|
auth_token_object = AuthenticationService().get_auth_token_object(code)
|
||||||
if "id_token" in auth_token_object:
|
if "id_token" in auth_token_object:
|
||||||
id_token = auth_token_object["id_token"]
|
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"]
|
auth_token_object["access_token"]
|
||||||
)
|
)
|
||||||
if user_info and "error" not in user_info:
|
if user_info and "error" not in user_info:
|
||||||
user_model = (
|
user_model = AuthorizationService.create_user_from_sign_in(user_info)
|
||||||
UserModel.query.filter(UserModel.service == "open_id")
|
g.user = user_model.id
|
||||||
.filter(UserModel.service_id == user_info["sub"])
|
g.token = auth_token_object["id_token"]
|
||||||
.first()
|
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 = (
|
redirect_url = (
|
||||||
f"{state_redirect_url}?"
|
f"{state_redirect_url}?"
|
||||||
+ f"access_token={auth_token_object['access_token']}&"
|
+ f"access_token={auth_token_object['access_token']}&"
|
||||||
|
@ -10,8 +10,10 @@ from flask import g
|
|||||||
from flask import request
|
from flask import request
|
||||||
from flask_bpmn.api.api_error import ApiError
|
from flask_bpmn.api.api_error import ApiError
|
||||||
from flask_bpmn.models.db import db
|
from flask_bpmn.models.db import db
|
||||||
|
from SpiffWorkflow.task import Task as SpiffTask # type: ignore
|
||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
|
|
||||||
|
from spiffworkflow_backend.models.active_task import ActiveTaskModel
|
||||||
from spiffworkflow_backend.models.group import GroupModel
|
from spiffworkflow_backend.models.group import GroupModel
|
||||||
from spiffworkflow_backend.models.permission_assignment import PermissionAssignmentModel
|
from spiffworkflow_backend.models.permission_assignment import PermissionAssignmentModel
|
||||||
from spiffworkflow_backend.models.permission_target import PermissionTargetModel
|
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 UserModel
|
||||||
from spiffworkflow_backend.models.user import UserNotFoundError
|
from spiffworkflow_backend.models.user import UserNotFoundError
|
||||||
from spiffworkflow_backend.models.user_group_assignment import UserGroupAssignmentModel
|
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
|
from spiffworkflow_backend.services.user_service import UserService
|
||||||
|
|
||||||
|
|
||||||
@ -27,6 +32,14 @@ class PermissionsFileNotSetError(Exception):
|
|||||||
"""PermissionsFileNotSetError."""
|
"""PermissionsFileNotSetError."""
|
||||||
|
|
||||||
|
|
||||||
|
class ActiveTaskNotFoundError(Exception):
|
||||||
|
"""ActiveTaskNotFoundError."""
|
||||||
|
|
||||||
|
|
||||||
|
class UserDoesNotHaveAccessToTaskError(Exception):
|
||||||
|
"""UserDoesNotHaveAccessToTaskError."""
|
||||||
|
|
||||||
|
|
||||||
class AuthorizationService:
|
class AuthorizationService:
|
||||||
"""Determine whether a user has permission to perform their request."""
|
"""Determine whether a user has permission to perform their request."""
|
||||||
|
|
||||||
@ -93,6 +106,19 @@ class AuthorizationService:
|
|||||||
db.session.commit()
|
db.session.commit()
|
||||||
cls.import_permissions_from_yaml_file()
|
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
|
@classmethod
|
||||||
def import_permissions_from_yaml_file(
|
def import_permissions_from_yaml_file(
|
||||||
cls, raise_if_missing_user: bool = False
|
cls, raise_if_missing_user: bool = False
|
||||||
@ -109,6 +135,20 @@ class AuthorizationService:
|
|||||||
with open(current_app.config["PERMISSIONS_FILE_FULLPATH"]) as file:
|
with open(current_app.config["PERMISSIONS_FILE_FULLPATH"]) as file:
|
||||||
permission_configs = yaml.safe_load(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:
|
if "groups" in permission_configs:
|
||||||
for group_identifier, group_config in permission_configs["groups"].items():
|
for group_identifier, group_config in permission_configs["groups"].items():
|
||||||
group = GroupModel.query.filter_by(identifier=group_identifier).first()
|
group = GroupModel.query.filter_by(identifier=group_identifier).first()
|
||||||
@ -127,15 +167,7 @@ class AuthorizationService:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
user_group_assignemnt = UserGroupAssignmentModel.query.filter_by(
|
cls.associate_user_with_group(user, group)
|
||||||
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()
|
|
||||||
|
|
||||||
if "permissions" in permission_configs:
|
if "permissions" in permission_configs:
|
||||||
for _permission_identifier, permission_config in permission_configs[
|
for _permission_identifier, permission_config in permission_configs[
|
||||||
@ -164,14 +196,20 @@ class AuthorizationService:
|
|||||||
)
|
)
|
||||||
if "users" in permission_config:
|
if "users" in permission_config:
|
||||||
for username in permission_config["users"]:
|
for username in permission_config["users"]:
|
||||||
principal = (
|
user = UserModel.query.filter_by(username=username).first()
|
||||||
PrincipalModel.query.join(UserModel)
|
if user is not None:
|
||||||
.filter(UserModel.username == username)
|
principal = (
|
||||||
.first()
|
PrincipalModel.query.join(UserModel)
|
||||||
)
|
.filter(UserModel.username == username)
|
||||||
cls.create_permission_for_principal(
|
.first()
|
||||||
principal, permission_target, allowed_permission
|
)
|
||||||
)
|
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
|
@classmethod
|
||||||
def create_permission_for_principal(
|
def create_permission_for_principal(
|
||||||
@ -202,6 +240,7 @@ class AuthorizationService:
|
|||||||
@classmethod
|
@classmethod
|
||||||
def should_disable_auth_for_request(cls) -> bool:
|
def should_disable_auth_for_request(cls) -> bool:
|
||||||
"""Should_disable_auth_for_request."""
|
"""Should_disable_auth_for_request."""
|
||||||
|
swagger_functions = ["get_json_spec"]
|
||||||
authentication_exclusion_list = ["status", "authentication_callback"]
|
authentication_exclusion_list = ["status", "authentication_callback"]
|
||||||
if request.method == "OPTIONS":
|
if request.method == "OPTIONS":
|
||||||
return True
|
return True
|
||||||
@ -218,7 +257,9 @@ class AuthorizationService:
|
|||||||
api_view_function
|
api_view_function
|
||||||
and api_view_function.__name__.startswith("login")
|
and api_view_function.__name__.startswith("login")
|
||||||
or api_view_function.__name__.startswith("logout")
|
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 authentication_exclusion_list
|
||||||
|
or api_view_function.__name__ in swagger_functions
|
||||||
):
|
):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -277,7 +318,7 @@ class AuthorizationService:
|
|||||||
|
|
||||||
raise ApiError(
|
raise ApiError(
|
||||||
error_code="unauthorized",
|
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,
|
status_code=403,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -359,196 +400,73 @@ class AuthorizationService:
|
|||||||
"The Authentication token you provided is invalid. You need a new token. ",
|
"The Authentication token you provided is invalid. You need a new token. ",
|
||||||
) from exception
|
) from exception
|
||||||
|
|
||||||
# def get_bearer_token_from_internal_token(self, internal_token):
|
@staticmethod
|
||||||
# """Get_bearer_token_from_internal_token."""
|
def assert_user_can_complete_spiff_task(
|
||||||
# self.decode_auth_token(internal_token)
|
processor: ProcessInstanceProcessor,
|
||||||
# print(f"get_user_by_internal_token: {internal_token}")
|
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:
|
if user not in active_task.potential_owners:
|
||||||
# """Introspect_token."""
|
raise UserDoesNotHaveAccessToTaskError(
|
||||||
# (
|
f"User {user.username} does not have access to update task'{spiff_task.task_spec.name}'"
|
||||||
# open_id_server_url,
|
f" for process instance '{processor.process_instance_model.id}'"
|
||||||
# open_id_client_id,
|
)
|
||||||
# open_id_realm_name,
|
return True
|
||||||
# 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
|
|
||||||
|
|
||||||
# def get_permission_by_basic_token(self, basic_token: dict) -> list:
|
@classmethod
|
||||||
# """Get_permission_by_basic_token."""
|
def create_user_from_sign_in(cls, user_info: dict) -> UserModel:
|
||||||
# (
|
"""Create_user_from_sign_in."""
|
||||||
# open_id_server_url,
|
is_new_user = False
|
||||||
# open_id_client_id,
|
user_model = (
|
||||||
# open_id_realm_name,
|
UserModel.query.filter(UserModel.service == "open_id")
|
||||||
# open_id_client_secret_key,
|
.filter(UserModel.service_id == user_info["sub"])
|
||||||
# ) = AuthorizationService.get_open_id_args()
|
.first()
|
||||||
#
|
)
|
||||||
# # 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
|
|
||||||
|
|
||||||
# def get_auth_status_for_resource_and_scope_by_token(
|
if user_model is None:
|
||||||
# self, basic_token: dict, resource: str, scope: str
|
current_app.logger.debug("create_user in login_return")
|
||||||
# ) -> str:
|
is_new_user = True
|
||||||
# """Get_auth_status_for_resource_and_scope_by_token."""
|
name = username = email = ""
|
||||||
# (
|
if "name" in user_info:
|
||||||
# open_id_server_url,
|
name = user_info["name"]
|
||||||
# open_id_client_id,
|
if "username" in user_info:
|
||||||
# open_id_realm_name,
|
username = user_info["username"]
|
||||||
# open_id_client_secret_key,
|
elif "preferred_username" in user_info:
|
||||||
# ) = AuthorizationService.get_open_id_args()
|
username = user_info["preferred_username"]
|
||||||
#
|
if "email" in user_info:
|
||||||
# # basic_token = AuthorizationService().refresh_token(basic_token)
|
email = user_info["email"]
|
||||||
# bearer_token = AuthorizationService().get_bearer_token(basic_token)
|
user_model = UserService().create_user(
|
||||||
# auth_bearer_string = f"Bearer {bearer_token['access_token']}"
|
service="open_id",
|
||||||
#
|
service_id=user_info["sub"],
|
||||||
# headers = {
|
name=name,
|
||||||
# "Content-Type": "application/x-www-form-urlencoded",
|
username=username,
|
||||||
# "Authorization": auth_bearer_string,
|
email=email,
|
||||||
# }
|
)
|
||||||
# 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
|
|
||||||
|
|
||||||
# def get_permissions_by_token_for_resource_and_scope(
|
# this may eventually get too slow.
|
||||||
# self, basic_token: str, resource: str|None=None, scope: str|None=None
|
# when it does, be careful about backgrounding, because
|
||||||
# ) -> str:
|
# the user will immediately need permissions to use the site.
|
||||||
# """Get_permissions_by_token_for_resource_and_scope."""
|
# we are also a little apprehensive about pre-creating users
|
||||||
# (
|
# before the user signs in, because we won't know things like
|
||||||
# open_id_server_url,
|
# the external service user identifier.
|
||||||
# open_id_client_id,
|
cls.import_permissions_from_yaml_file()
|
||||||
# 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
|
|
||||||
|
|
||||||
# def get_resource_set(self, public_access_token, uri):
|
if is_new_user:
|
||||||
# """Get_resource_set."""
|
UserService.add_user_to_active_tasks_if_appropriate(user_model)
|
||||||
# (
|
|
||||||
# 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")
|
|
||||||
|
|
||||||
# def get_permission_by_token(self, public_access_token: str) -> dict:
|
# this cannot be None so ignore mypy
|
||||||
# """Get_permission_by_token."""
|
return user_model # type: ignore
|
||||||
# # 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
|
|
||||||
|
|
||||||
|
|
||||||
class KeycloakAuthorization:
|
class KeycloakAuthorization:
|
||||||
|
@ -4,6 +4,7 @@ import decimal
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import time
|
import time
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@ -55,9 +56,11 @@ from SpiffWorkflow.task import TaskState
|
|||||||
from SpiffWorkflow.util.deep_merge import DeepMerge # type: ignore
|
from SpiffWorkflow.util.deep_merge import DeepMerge # type: ignore
|
||||||
|
|
||||||
from spiffworkflow_backend.models.active_task import ActiveTaskModel
|
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.bpmn_process_id_lookup import BpmnProcessIdLookup
|
||||||
from spiffworkflow_backend.models.file import File
|
from spiffworkflow_backend.models.file import File
|
||||||
from spiffworkflow_backend.models.file import FileType
|
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 import MessageCorrelationModel
|
||||||
from spiffworkflow_backend.models.message_correlation_message_instance import (
|
from spiffworkflow_backend.models.message_correlation_message_instance import (
|
||||||
MessageCorrelationMessageInstanceModel,
|
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 MessageInstanceModel
|
||||||
from spiffworkflow_backend.models.message_instance import MessageModel
|
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 ProcessInstanceModel
|
||||||
from spiffworkflow_backend.models.process_instance import ProcessInstanceStatus
|
from spiffworkflow_backend.models.process_instance import ProcessInstanceStatus
|
||||||
from spiffworkflow_backend.models.process_model import ProcessModelInfo
|
from spiffworkflow_backend.models.process_model import ProcessModelInfo
|
||||||
@ -108,6 +110,10 @@ class ProcessInstanceProcessorError(Exception):
|
|||||||
"""ProcessInstanceProcessorError."""
|
"""ProcessInstanceProcessorError."""
|
||||||
|
|
||||||
|
|
||||||
|
class NoPotentialOwnersForTaskError(Exception):
|
||||||
|
"""NoPotentialOwnersForTaskError."""
|
||||||
|
|
||||||
|
|
||||||
class CustomBpmnScriptEngine(PythonScriptEngine): # type: ignore
|
class CustomBpmnScriptEngine(PythonScriptEngine): # type: ignore
|
||||||
"""This is a custom script processor that can be easily injected into Spiff Workflow.
|
"""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():
|
if self.bpmn_process_instance.is_completed():
|
||||||
self.process_instance_model.end_in_seconds = round(time.time())
|
self.process_instance_model.end_in_seconds = round(time.time())
|
||||||
|
|
||||||
db.session.add(self.process_instance_model)
|
active_tasks = ActiveTaskModel.query.filter_by(
|
||||||
|
|
||||||
ActiveTaskModel.query.filter_by(
|
|
||||||
process_instance_id=self.process_instance_model.id
|
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()
|
ready_or_waiting_tasks = self.get_all_ready_or_waiting_tasks()
|
||||||
for ready_or_waiting_task in ready_or_waiting_tasks:
|
for ready_or_waiting_task in ready_or_waiting_tasks:
|
||||||
# filter out non-usertasks
|
# filter out non-usertasks
|
||||||
if not self.bpmn_process_instance._is_engine_task(
|
task_spec = ready_or_waiting_task.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"]
|
||||||
user_id = ready_or_waiting_task.data["current_user"]["id"]
|
task_lane = "process_initiator"
|
||||||
principal = PrincipalModel.query.filter_by(user_id=user_id).first()
|
if task_spec.lane is not None:
|
||||||
if principal is None:
|
task_lane = task_spec.lane
|
||||||
raise (
|
|
||||||
ApiError(
|
potential_owner_ids = []
|
||||||
error_code="principal_not_found",
|
lane_assignment_id = None
|
||||||
message=f"Principal not found from user id: {user_id}",
|
if re.match(r"(process.?)initiator", task_lane, re.IGNORECASE):
|
||||||
status_code=400,
|
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
|
extensions = ready_or_waiting_task.task_spec.extensions
|
||||||
|
|
||||||
@ -555,7 +579,6 @@ class ProcessInstanceProcessor:
|
|||||||
active_task = ActiveTaskModel(
|
active_task = ActiveTaskModel(
|
||||||
process_instance_id=self.process_instance_model.id,
|
process_instance_id=self.process_instance_model.id,
|
||||||
process_model_display_name=process_model_display_name,
|
process_model_display_name=process_model_display_name,
|
||||||
assigned_principal_id=principal.id,
|
|
||||||
form_file_name=form_file_name,
|
form_file_name=form_file_name,
|
||||||
ui_form_file_name=ui_form_file_name,
|
ui_form_file_name=ui_form_file_name,
|
||||||
task_id=str(ready_or_waiting_task.id),
|
task_id=str(ready_or_waiting_task.id),
|
||||||
@ -563,10 +586,17 @@ class ProcessInstanceProcessor:
|
|||||||
task_title=ready_or_waiting_task.task_spec.description,
|
task_title=ready_or_waiting_task.task_spec.description,
|
||||||
task_type=ready_or_waiting_task.task_spec.__class__.__name__,
|
task_type=ready_or_waiting_task.task_spec.__class__.__name__,
|
||||||
task_status=ready_or_waiting_task.get_state_name(),
|
task_status=ready_or_waiting_task.get_state_name(),
|
||||||
|
lane_assignment_id=lane_assignment_id,
|
||||||
)
|
)
|
||||||
db.session.add(active_task)
|
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
|
@staticmethod
|
||||||
def get_parser() -> MyCustomParser:
|
def get_parser() -> MyCustomParser:
|
||||||
|
@ -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 TaskAction
|
||||||
from spiffworkflow_backend.models.task_event import TaskEventModel
|
from spiffworkflow_backend.models.task_event import TaskEventModel
|
||||||
from spiffworkflow_backend.models.user import UserModel
|
from spiffworkflow_backend.models.user import UserModel
|
||||||
|
from spiffworkflow_backend.services.authorization_service import AuthorizationService
|
||||||
from spiffworkflow_backend.services.git_service import GitService
|
from spiffworkflow_backend.services.git_service import GitService
|
||||||
from spiffworkflow_backend.services.process_instance_processor import (
|
from spiffworkflow_backend.services.process_instance_processor import (
|
||||||
ProcessInstanceProcessor,
|
ProcessInstanceProcessor,
|
||||||
)
|
)
|
||||||
from spiffworkflow_backend.services.process_model_service import ProcessModelService
|
from spiffworkflow_backend.services.process_model_service import ProcessModelService
|
||||||
|
|
||||||
# from SpiffWorkflow.task import TaskState # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
class ProcessInstanceService:
|
class ProcessInstanceService:
|
||||||
"""ProcessInstanceService."""
|
"""ProcessInstanceService."""
|
||||||
@ -272,6 +271,10 @@ class ProcessInstanceService:
|
|||||||
Abstracted here because we need to do it multiple times when completing all tasks in
|
Abstracted here because we need to do it multiple times when completing all tasks in
|
||||||
a multi-instance task.
|
a multi-instance task.
|
||||||
"""
|
"""
|
||||||
|
AuthorizationService.assert_user_can_complete_spiff_task(
|
||||||
|
processor, spiff_task, user
|
||||||
|
)
|
||||||
|
|
||||||
dot_dct = ProcessInstanceService.create_dot_dict(data)
|
dot_dct = ProcessInstanceService.create_dot_dict(data)
|
||||||
spiff_task.update_data(dot_dct)
|
spiff_task.update_data(dot_dct)
|
||||||
# ProcessInstanceService.post_process_form(spiff_task) # some properties may update the data store.
|
# ProcessInstanceService.post_process_form(spiff_task) # some properties may update the data store.
|
||||||
@ -282,8 +285,7 @@ class ProcessInstanceService:
|
|||||||
ProcessInstanceService.log_task_action(
|
ProcessInstanceService.log_task_action(
|
||||||
user.id, processor, spiff_task, TaskAction.COMPLETE.value
|
user.id, processor, spiff_task, TaskAction.COMPLETE.value
|
||||||
)
|
)
|
||||||
processor.do_engine_steps()
|
processor.do_engine_steps(save=True)
|
||||||
processor.save()
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def log_task_action(
|
def log_task_action(
|
||||||
|
@ -7,6 +7,8 @@ from flask import g
|
|||||||
from flask_bpmn.api.api_error import ApiError
|
from flask_bpmn.api.api_error import ApiError
|
||||||
from flask_bpmn.models.db import db
|
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.group import GroupModel
|
||||||
from spiffworkflow_backend.models.principal import PrincipalModel
|
from spiffworkflow_backend.models.principal import PrincipalModel
|
||||||
from spiffworkflow_backend.models.user import AdminSessionModel
|
from spiffworkflow_backend.models.user import AdminSessionModel
|
||||||
@ -313,3 +315,17 @@ class UserService:
|
|||||||
if user:
|
if user:
|
||||||
return user
|
return user
|
||||||
return None
|
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()
|
||||||
|
103
tests/data/model_with_lanes/lanes.bpmn
Normal file
103
tests/data/model_with_lanes/lanes.bpmn
Normal 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>
|
@ -207,10 +207,15 @@ class BaseTest:
|
|||||||
# return public_access_token
|
# return public_access_token
|
||||||
|
|
||||||
def create_process_instance_from_process_model(
|
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:
|
) -> ProcessInstanceModel:
|
||||||
"""Create_process_instance_from_process_model."""
|
"""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())
|
current_time = round(time.time())
|
||||||
process_instance = ProcessInstanceModel(
|
process_instance = ProcessInstanceModel(
|
||||||
status=status,
|
status=status,
|
||||||
|
@ -15,6 +15,7 @@ from spiffworkflow_backend.exceptions.process_entity_not_found_error import (
|
|||||||
ProcessEntityNotFoundError,
|
ProcessEntityNotFoundError,
|
||||||
)
|
)
|
||||||
from spiffworkflow_backend.models.active_task import ActiveTaskModel
|
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 ProcessGroup
|
||||||
from spiffworkflow_backend.models.process_group import ProcessGroupSchema
|
from spiffworkflow_backend.models.process_group import ProcessGroupSchema
|
||||||
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
|
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
|
||||||
@ -26,6 +27,7 @@ from spiffworkflow_backend.models.process_model import NotificationType
|
|||||||
from spiffworkflow_backend.models.process_model import ProcessModelInfoSchema
|
from spiffworkflow_backend.models.process_model import ProcessModelInfoSchema
|
||||||
from spiffworkflow_backend.models.task_event import TaskEventModel
|
from spiffworkflow_backend.models.task_event import TaskEventModel
|
||||||
from spiffworkflow_backend.models.user import UserModel
|
from spiffworkflow_backend.models.user import UserModel
|
||||||
|
from spiffworkflow_backend.services.authorization_service import AuthorizationService
|
||||||
from spiffworkflow_backend.services.file_system_service import FileSystemService
|
from spiffworkflow_backend.services.file_system_service import FileSystemService
|
||||||
from spiffworkflow_backend.services.process_instance_processor import (
|
from spiffworkflow_backend.services.process_instance_processor import (
|
||||||
ProcessInstanceProcessor,
|
ProcessInstanceProcessor,
|
||||||
@ -1772,6 +1774,94 @@ class TestProcessApi(BaseTest):
|
|||||||
assert response.json is not None
|
assert response.json is not None
|
||||||
assert len(response.json["results"]) == 2
|
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
|
# TODO: test the auth callback endpoint
|
||||||
# def test_can_store_authentication_secret(
|
# def test_can_store_authentication_secret(
|
||||||
# self, app: Flask, client: FlaskClient, with_db_and_bpmn_file_cleanup: None
|
# self, app: Flask, client: FlaskClient, with_db_and_bpmn_file_cleanup: None
|
||||||
|
@ -2,9 +2,16 @@
|
|||||||
import pytest
|
import pytest
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
from tests.spiffworkflow_backend.helpers.base_test import BaseTest
|
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.models.user import UserNotFoundError
|
||||||
from spiffworkflow_backend.services.authorization_service import AuthorizationService
|
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):
|
class TestAuthorizationService(BaseTest):
|
||||||
@ -19,6 +26,12 @@ class TestAuthorizationService(BaseTest):
|
|||||||
raise_if_missing_user=True
|
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(
|
def test_can_import_permissions_from_yaml(
|
||||||
self, app: Flask, with_db_and_bpmn_file_cleanup: None
|
self, app: Flask, with_db_and_bpmn_file_cleanup: None
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -37,11 +50,17 @@ class TestAuthorizationService(BaseTest):
|
|||||||
users[username] = user
|
users[username] = user
|
||||||
|
|
||||||
AuthorizationService.import_permissions_from_yaml_file()
|
AuthorizationService.import_permissions_from_yaml_file()
|
||||||
assert len(users["testadmin1"].groups) == 1
|
assert len(users["testadmin1"].groups) == 2
|
||||||
assert users["testadmin1"].groups[0].identifier == "admin"
|
testadmin1_group_identifiers = sorted(
|
||||||
assert len(users["testuser1"].groups) == 1
|
[g.identifier for g in users["testadmin1"].groups]
|
||||||
assert users["testuser1"].groups[0].identifier == "finance"
|
)
|
||||||
assert len(users["testuser2"].groups) == 2
|
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(
|
self.assert_user_has_permission(
|
||||||
users["testuser1"], "update", "/v1.0/process-groups/finance/model1"
|
users["testuser1"], "update", "/v1.0/process-groups/finance/model1"
|
||||||
@ -55,6 +74,7 @@ class TestAuthorizationService(BaseTest):
|
|||||||
self.assert_user_has_permission(
|
self.assert_user_has_permission(
|
||||||
users["testuser4"], "update", "/v1.0/process-groups/finance/model1"
|
users["testuser4"], "update", "/v1.0/process-groups/finance/model1"
|
||||||
)
|
)
|
||||||
|
# via the user, not the group
|
||||||
self.assert_user_has_permission(
|
self.assert_user_has_permission(
|
||||||
users["testuser4"], "read", "/v1.0/process-groups/finance/model1"
|
users["testuser4"], "read", "/v1.0/process-groups/finance/model1"
|
||||||
)
|
)
|
||||||
@ -67,3 +87,38 @@ class TestAuthorizationService(BaseTest):
|
|||||||
self.assert_user_has_permission(
|
self.assert_user_has_permission(
|
||||||
users["testuser2"], "read", "/v1.0/process-groups/"
|
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
|
||||||
|
)
|
||||||
|
@ -1,10 +1,21 @@
|
|||||||
"""Test_process_instance_processor."""
|
"""Test_process_instance_processor."""
|
||||||
|
import pytest
|
||||||
from flask.app import Flask
|
from flask.app import Flask
|
||||||
from tests.spiffworkflow_backend.helpers.base_test import BaseTest
|
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 (
|
from spiffworkflow_backend.services.process_instance_processor import (
|
||||||
ProcessInstanceProcessor,
|
ProcessInstanceProcessor,
|
||||||
)
|
)
|
||||||
|
from spiffworkflow_backend.services.process_instance_service import (
|
||||||
|
ProcessInstanceService,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestProcessInstanceProcessor(BaseTest):
|
class TestProcessInstanceProcessor(BaseTest):
|
||||||
@ -34,3 +45,76 @@ class TestProcessInstanceProcessor(BaseTest):
|
|||||||
result
|
result
|
||||||
== "Chuck Norris doesn’t read books. He stares them down until he gets the information he wants."
|
== "Chuck Norris doesn’t 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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user