Merge commit '4a48d9cccd1ca8619b3dbef3c10bcce667c9d9e0'
This commit is contained in:
commit
338c40ae48
|
@ -0,0 +1,14 @@
|
||||||
|
"""Deletes all permissions and then re-imports from yaml file."""
|
||||||
|
from spiffworkflow_backend import get_hacked_up_app_for_script
|
||||||
|
from spiffworkflow_backend.services.authorization_service import AuthorizationService
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
"""Main."""
|
||||||
|
app = get_hacked_up_app_for_script()
|
||||||
|
with app.app_context():
|
||||||
|
AuthorizationService.delete_all_permissions_and_recreate()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
|
@ -630,6 +630,28 @@
|
||||||
"notBefore": 0,
|
"notBefore": 0,
|
||||||
"groups": []
|
"groups": []
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "9b46f3be-a81d-4b76-92e6-2ac8462f5ec8",
|
||||||
|
"createdTimestamp": 1665688255982,
|
||||||
|
"username": "finance_user1",
|
||||||
|
"enabled": true,
|
||||||
|
"totp": false,
|
||||||
|
"emailVerified": false,
|
||||||
|
"credentials": [
|
||||||
|
{
|
||||||
|
"id": "f14722ec-13a7-4d35-a4ec-0475d405ae58",
|
||||||
|
"type": "password",
|
||||||
|
"createdDate": 1665688275943,
|
||||||
|
"secretData": "{\"value\":\"PlNhf8ShIvaSP3CUwCwAJ2tkqcTCVmCWUy4rbuLSXxEIiuGMu4XeZdsrE82R8PWuDQhlWn/YOUOk38xKZS2ySQ==\",\"salt\":\"m7JGY2cWgFBXMYQSSP2JQQ==\",\"additionalParameters\":{}}",
|
||||||
|
"credentialData": "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"disableableCredentialTypes": [],
|
||||||
|
"requiredActions": [],
|
||||||
|
"realmRoles": ["default-roles-spiffworkflow"],
|
||||||
|
"notBefore": 0,
|
||||||
|
"groups": []
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "087bdc16-e362-4340-aa60-1ff71a45f844",
|
"id": "087bdc16-e362-4340-aa60-1ff71a45f844",
|
||||||
"createdTimestamp": 1665516884829,
|
"createdTimestamp": 1665516884829,
|
||||||
|
@ -828,31 +850,6 @@
|
||||||
"notBefore": 0,
|
"notBefore": 0,
|
||||||
"groups": []
|
"groups": []
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "a15da457-7ebb-49d4-9dcc-6876cb71600d",
|
|
||||||
"createdTimestamp": 1657115919770,
|
|
||||||
"username": "repeat_form_user_1",
|
|
||||||
"enabled": true,
|
|
||||||
"totp": false,
|
|
||||||
"emailVerified": false,
|
|
||||||
"credentials": [
|
|
||||||
{
|
|
||||||
"id": "509dfd8d-a54e-4d8b-b250-ec99e585e15d",
|
|
||||||
"type": "password",
|
|
||||||
"createdDate": 1657298008525,
|
|
||||||
"secretData": "{\"value\":\"/47zG9XBvKg+1P2z6fRL4cyUNn+sB4BgXsxBsvi1NYR9Z20WTeWzzOT2uXvv2ajKMRHrv0OqTesldvSJXARPqA==\",\"salt\":\"dODEHOF24xGPx+7QGaIXWQ==\",\"additionalParameters\":{}}",
|
|
||||||
"credentialData": "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"disableableCredentialTypes": [],
|
|
||||||
"requiredActions": [],
|
|
||||||
"realmRoles": ["default-roles-spiffworkflow"],
|
|
||||||
"clientRoles": {
|
|
||||||
"spiffworkflow-backend": ["uma_protection", "repeat-form-role-2"]
|
|
||||||
},
|
|
||||||
"notBefore": 0,
|
|
||||||
"groups": []
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "f3852a7d-8adf-494f-b39d-96ad4c899ee5",
|
"id": "f3852a7d-8adf-494f-b39d-96ad4c899ee5",
|
||||||
"createdTimestamp": 1665516926300,
|
"createdTimestamp": 1665516926300,
|
||||||
|
|
|
@ -10,6 +10,7 @@ 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.process_instance import ProcessInstanceModel
|
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
|
||||||
|
from spiffworkflow_backend.models.user import UserModel
|
||||||
from spiffworkflow_backend.services.process_instance_processor import (
|
from spiffworkflow_backend.services.process_instance_processor import (
|
||||||
ProcessInstanceProcessor,
|
ProcessInstanceProcessor,
|
||||||
)
|
)
|
||||||
|
@ -57,6 +58,7 @@ def with_db_and_bpmn_file_cleanup() -> None:
|
||||||
"""Process_group_resource."""
|
"""Process_group_resource."""
|
||||||
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()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
yield
|
yield
|
||||||
|
@ -66,6 +68,12 @@ def with_db_and_bpmn_file_cleanup() -> None:
|
||||||
shutil.rmtree(process_model_service.root_path())
|
shutil.rmtree(process_model_service.root_path())
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def with_super_admin_user() -> UserModel:
|
||||||
|
"""With_super_admin_user."""
|
||||||
|
return BaseTest.create_user_with_permission("super_admin")
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def setup_process_instances_for_reports() -> list[ProcessInstanceModel]:
|
def setup_process_instances_for_reports() -> list[ProcessInstanceModel]:
|
||||||
"""Setup_process_instances_for_reports."""
|
"""Setup_process_instances_for_reports."""
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
"""empty message
|
"""empty message
|
||||||
|
|
||||||
Revision ID: 9e14b40371f3
|
Revision ID: e6b28d8e3178
|
||||||
Revises:
|
Revises:
|
||||||
Create Date: 2022-10-19 19:31:20.431800
|
Create Date: 2022-10-20 13:05:25.896486
|
||||||
|
|
||||||
"""
|
"""
|
||||||
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 = '9e14b40371f3'
|
revision = 'e6b28d8e3178'
|
||||||
down_revision = None
|
down_revision = None
|
||||||
branch_labels = None
|
branch_labels = None
|
||||||
depends_on = None
|
depends_on = None
|
||||||
|
@ -134,11 +134,21 @@ def upgrade():
|
||||||
op.create_index(op.f('ix_process_instance_report_identifier'), 'process_instance_report', ['identifier'], unique=False)
|
op.create_index(op.f('ix_process_instance_report_identifier'), 'process_instance_report', ['identifier'], unique=False)
|
||||||
op.create_index(op.f('ix_process_instance_report_process_group_identifier'), 'process_instance_report', ['process_group_identifier'], unique=False)
|
op.create_index(op.f('ix_process_instance_report_process_group_identifier'), 'process_instance_report', ['process_group_identifier'], unique=False)
|
||||||
op.create_index(op.f('ix_process_instance_report_process_model_identifier'), 'process_instance_report', ['process_model_identifier'], unique=False)
|
op.create_index(op.f('ix_process_instance_report_process_model_identifier'), 'process_instance_report', ['process_model_identifier'], unique=False)
|
||||||
|
op.create_table('refresh_token',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('token', sa.String(length=1024), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
sa.UniqueConstraint('user_id')
|
||||||
|
)
|
||||||
op.create_table('secret',
|
op.create_table('secret',
|
||||||
sa.Column('id', sa.Integer(), nullable=False),
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
sa.Column('key', sa.String(length=50), nullable=False),
|
sa.Column('key', sa.String(length=50), nullable=False),
|
||||||
sa.Column('value', sa.Text(), nullable=False),
|
sa.Column('value', sa.Text(), nullable=False),
|
||||||
sa.Column('creator_user_id', sa.Integer(), nullable=False),
|
sa.Column('creator_user_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('updated_at_in_seconds', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('created_at_in_seconds', sa.Integer(), nullable=True),
|
||||||
sa.ForeignKeyConstraint(['creator_user_id'], ['user.id'], ),
|
sa.ForeignKeyConstraint(['creator_user_id'], ['user.id'], ),
|
||||||
sa.PrimaryKeyConstraint('id'),
|
sa.PrimaryKeyConstraint('id'),
|
||||||
sa.UniqueConstraint('key')
|
sa.UniqueConstraint('key')
|
||||||
|
@ -226,8 +236,8 @@ def upgrade():
|
||||||
sa.Column('id', sa.Integer(), nullable=False),
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
sa.Column('principal_id', sa.Integer(), nullable=False),
|
sa.Column('principal_id', sa.Integer(), nullable=False),
|
||||||
sa.Column('permission_target_id', sa.Integer(), nullable=False),
|
sa.Column('permission_target_id', sa.Integer(), nullable=False),
|
||||||
sa.Column('grant_type', sa.String(length=50), nullable=True),
|
sa.Column('grant_type', sa.String(length=50), nullable=False),
|
||||||
sa.Column('permission', sa.String(length=50), nullable=True),
|
sa.Column('permission', sa.String(length=50), nullable=False),
|
||||||
sa.ForeignKeyConstraint(['permission_target_id'], ['permission_target.id'], ),
|
sa.ForeignKeyConstraint(['permission_target_id'], ['permission_target.id'], ),
|
||||||
sa.ForeignKeyConstraint(['principal_id'], ['principal.id'], ),
|
sa.ForeignKeyConstraint(['principal_id'], ['principal.id'], ),
|
||||||
sa.PrimaryKeyConstraint('id'),
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
@ -316,6 +326,7 @@ def downgrade():
|
||||||
op.drop_table('active_task')
|
op.drop_table('active_task')
|
||||||
op.drop_table('user_group_assignment')
|
op.drop_table('user_group_assignment')
|
||||||
op.drop_table('secret')
|
op.drop_table('secret')
|
||||||
|
op.drop_table('refresh_token')
|
||||||
op.drop_index(op.f('ix_process_instance_report_process_model_identifier'), table_name='process_instance_report')
|
op.drop_index(op.f('ix_process_instance_report_process_model_identifier'), table_name='process_instance_report')
|
||||||
op.drop_index(op.f('ix_process_instance_report_process_group_identifier'), table_name='process_instance_report')
|
op.drop_index(op.f('ix_process_instance_report_process_group_identifier'), table_name='process_instance_report')
|
||||||
op.drop_index(op.f('ix_process_instance_report_identifier'), table_name='process_instance_report')
|
op.drop_index(op.f('ix_process_instance_report_identifier'), table_name='process_instance_report')
|
|
@ -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"
|
||||||
|
@ -639,7 +639,7 @@ werkzeug = "*"
|
||||||
type = "git"
|
type = "git"
|
||||||
url = "https://github.com/sartography/flask-bpmn"
|
url = "https://github.com/sartography/flask-bpmn"
|
||||||
reference = "main"
|
reference = "main"
|
||||||
resolved_reference = "bd4b45a842ed63a29e74ff02ea7f2a56d7b2298a"
|
resolved_reference = "c8fd01df47518749a074772fec383256c482139f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "Flask-Cors"
|
name = "Flask-Cors"
|
||||||
|
@ -1512,7 +1512,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"
|
||||||
|
@ -1625,7 +1625,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)"]
|
||||||
|
@ -1847,7 +1847,7 @@ test = ["pytest"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "SpiffWorkflow"
|
name = "SpiffWorkflow"
|
||||||
version = "1.2.0"
|
version = "1.2.1"
|
||||||
description = "A workflow framework and BPMN/DMN Processor"
|
description = "A workflow framework and BPMN/DMN Processor"
|
||||||
category = "main"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
|
@ -1858,7 +1858,6 @@ develop = false
|
||||||
celery = "*"
|
celery = "*"
|
||||||
configparser = "*"
|
configparser = "*"
|
||||||
dateparser = "*"
|
dateparser = "*"
|
||||||
importlib-metadata = "<5.0"
|
|
||||||
lxml = "*"
|
lxml = "*"
|
||||||
pytz = "*"
|
pytz = "*"
|
||||||
|
|
||||||
|
@ -1884,19 +1883,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"]
|
||||||
|
|
||||||
|
@ -2030,7 +2029,7 @@ python-versions = "*"
|
||||||
name = "types-PyYAML"
|
name = "types-PyYAML"
|
||||||
version = "6.0.12"
|
version = "6.0.12"
|
||||||
description = "Typing stubs for PyYAML"
|
description = "Typing stubs for PyYAML"
|
||||||
category = "main"
|
category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
|
|
||||||
|
@ -2234,7 +2233,7 @@ testing = ["func-timeout", "jaraco.itertools", "pytest (>=6)", "pytest-black (>=
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "1.1"
|
lock-version = "1.1"
|
||||||
python-versions = ">=3.9,<3.11"
|
python-versions = ">=3.9,<3.11"
|
||||||
content-hash = "2602fd47f14d1163b2590ab01d3adb1ce881c699bb09630e6fdfc56b919a7a4e"
|
content-hash = "cff4bcfd10157833f1a0f0bb806c3543267c3e99cc13f311b328d101c30ac553"
|
||||||
|
|
||||||
[metadata.files]
|
[metadata.files]
|
||||||
alabaster = [
|
alabaster = [
|
||||||
|
@ -3013,18 +3012,7 @@ py = [
|
||||||
{file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"},
|
{file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"},
|
||||||
]
|
]
|
||||||
pyasn1 = [
|
pyasn1 = [
|
||||||
{file = "pyasn1-0.4.8-py2.4.egg", hash = "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"},
|
|
||||||
{file = "pyasn1-0.4.8-py2.5.egg", hash = "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf"},
|
|
||||||
{file = "pyasn1-0.4.8-py2.6.egg", hash = "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00"},
|
|
||||||
{file = "pyasn1-0.4.8-py2.7.egg", hash = "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8"},
|
|
||||||
{file = "pyasn1-0.4.8-py2.py3-none-any.whl", hash = "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d"},
|
{file = "pyasn1-0.4.8-py2.py3-none-any.whl", hash = "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d"},
|
||||||
{file = "pyasn1-0.4.8-py3.1.egg", hash = "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86"},
|
|
||||||
{file = "pyasn1-0.4.8-py3.2.egg", hash = "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7"},
|
|
||||||
{file = "pyasn1-0.4.8-py3.3.egg", hash = "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576"},
|
|
||||||
{file = "pyasn1-0.4.8-py3.4.egg", hash = "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12"},
|
|
||||||
{file = "pyasn1-0.4.8-py3.5.egg", hash = "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2"},
|
|
||||||
{file = "pyasn1-0.4.8-py3.6.egg", hash = "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359"},
|
|
||||||
{file = "pyasn1-0.4.8-py3.7.egg", hash = "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776"},
|
|
||||||
{file = "pyasn1-0.4.8.tar.gz", hash = "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"},
|
{file = "pyasn1-0.4.8.tar.gz", hash = "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"},
|
||||||
]
|
]
|
||||||
pycodestyle = [
|
pycodestyle = [
|
||||||
|
|
|
@ -19,7 +19,9 @@ import spiffworkflow_backend.load_database_models # noqa: F401
|
||||||
from spiffworkflow_backend.config import setup_config
|
from spiffworkflow_backend.config import setup_config
|
||||||
from spiffworkflow_backend.routes.admin_blueprint.admin_blueprint import admin_blueprint
|
from spiffworkflow_backend.routes.admin_blueprint.admin_blueprint import admin_blueprint
|
||||||
from spiffworkflow_backend.routes.process_api_blueprint import process_api_blueprint
|
from spiffworkflow_backend.routes.process_api_blueprint import process_api_blueprint
|
||||||
|
from spiffworkflow_backend.routes.user import verify_token
|
||||||
from spiffworkflow_backend.routes.user_blueprint import user_blueprint
|
from spiffworkflow_backend.routes.user_blueprint import user_blueprint
|
||||||
|
from spiffworkflow_backend.services.authorization_service import AuthorizationService
|
||||||
from spiffworkflow_backend.services.background_processing_service import (
|
from spiffworkflow_backend.services.background_processing_service import (
|
||||||
BackgroundProcessingService,
|
BackgroundProcessingService,
|
||||||
)
|
)
|
||||||
|
@ -114,6 +116,9 @@ def create_app() -> flask.app.Flask:
|
||||||
|
|
||||||
configure_sentry(app)
|
configure_sentry(app)
|
||||||
|
|
||||||
|
app.before_request(verify_token)
|
||||||
|
app.before_request(AuthorizationService.check_for_permission)
|
||||||
|
|
||||||
return app # type: ignore
|
return app # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
openapi: "3.0.2"
|
openapi: "3.0.2"
|
||||||
info:
|
info:
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
title: Workflow Microservice
|
title: spiffworkflow-backend
|
||||||
license:
|
license:
|
||||||
name: MIT
|
name: MIT
|
||||||
servers:
|
servers:
|
||||||
- url: http://localhost:5000/v1.0
|
- url: http://localhost:5000/v1.0
|
||||||
security:
|
# this is handled in flask now
|
||||||
- jwt: ["secret"]
|
security: []
|
||||||
|
# - jwt: ["secret"]
|
||||||
# - oAuth2AuthCode:
|
# - oAuth2AuthCode:
|
||||||
# - read_email
|
# - read_email
|
||||||
# - uid
|
# - uid
|
||||||
|
@ -378,7 +379,6 @@ paths:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/OkTrue"
|
$ref: "#/components/schemas/OkTrue"
|
||||||
# process model update
|
|
||||||
put:
|
put:
|
||||||
operationId: spiffworkflow_backend.routes.process_api_blueprint.process_model_update
|
operationId: spiffworkflow_backend.routes.process_api_blueprint.process_model_update
|
||||||
summary: Modifies an existing process mosel with the given parameters.
|
summary: Modifies an existing process mosel with the given parameters.
|
||||||
|
@ -827,7 +827,6 @@ paths:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/File"
|
$ref: "#/components/schemas/File"
|
||||||
# process_model_file_update
|
|
||||||
put:
|
put:
|
||||||
operationId: spiffworkflow_backend.routes.process_api_blueprint.process_model_file_update
|
operationId: spiffworkflow_backend.routes.process_api_blueprint.process_model_file_update
|
||||||
summary: save the contents to the given file
|
summary: save the contents to the given file
|
||||||
|
@ -1250,6 +1249,25 @@ paths:
|
||||||
"404":
|
"404":
|
||||||
description: Secret does not exist
|
description: Secret does not exist
|
||||||
|
|
||||||
|
/permissions-check:
|
||||||
|
post:
|
||||||
|
operationId: spiffworkflow_backend.routes.process_api_blueprint.permissions_check
|
||||||
|
summary: Checks if current user has access to given list of target uris and permissions.
|
||||||
|
tags:
|
||||||
|
- Permissions
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/Secret"
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Result of permission check
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/Secret"
|
||||||
|
|
||||||
components:
|
components:
|
||||||
securitySchemes:
|
securitySchemes:
|
||||||
jwt:
|
jwt:
|
||||||
|
|
|
@ -1,13 +1,25 @@
|
||||||
groups:
|
groups:
|
||||||
admin:
|
admin:
|
||||||
users:
|
users:
|
||||||
[jakub, kb, alex, dan, mike, jason, amir, jarrad, elizabeth, jon, natalia]
|
[
|
||||||
|
jakub,
|
||||||
|
kb,
|
||||||
|
alex,
|
||||||
|
dan,
|
||||||
|
mike,
|
||||||
|
jason,
|
||||||
|
amir,
|
||||||
|
jarrad,
|
||||||
|
elizabeth,
|
||||||
|
jon,
|
||||||
|
harmeet,
|
||||||
|
sasha,
|
||||||
|
manuchehr,
|
||||||
|
natalia,
|
||||||
|
]
|
||||||
|
|
||||||
finance:
|
finance:
|
||||||
users: [harmeet, sasha]
|
users: [finance_user1]
|
||||||
|
|
||||||
hr:
|
|
||||||
users: [manuchehr]
|
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
admin:
|
admin:
|
||||||
|
@ -20,10 +32,10 @@ permissions:
|
||||||
groups: [finance]
|
groups: [finance]
|
||||||
users: []
|
users: []
|
||||||
allowed_permissions: [create, read, update, delete]
|
allowed_permissions: [create, read, update, delete]
|
||||||
uri: /v1.0/process-groups/finance/*
|
uri: /v1.0/process-groups/execute-procure-to-pay/*
|
||||||
|
|
||||||
read-all:
|
read-all:
|
||||||
groups: [finance, hr, admin]
|
groups: [finance, admin]
|
||||||
users: []
|
users: []
|
||||||
allowed_permissions: [read]
|
allowed_permissions: [read]
|
||||||
uri: /*
|
uri: /*
|
||||||
|
|
|
@ -11,3 +11,7 @@ SPIFFWORKFLOW_BACKEND_LOG_TO_FILE = (
|
||||||
SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME = environ.get(
|
SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME = environ.get(
|
||||||
"SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME", default="testing.yml"
|
"SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME", default="testing.yml"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
SPIFFWORKFLOW_BACKEND_LOG_LEVEL = environ.get(
|
||||||
|
"SPIFFWORKFLOW_BACKEND_LOG_LEVEL", default="debug"
|
||||||
|
)
|
||||||
|
|
|
@ -45,6 +45,7 @@ from spiffworkflow_backend.models.process_instance import (
|
||||||
from spiffworkflow_backend.models.process_instance_report import (
|
from spiffworkflow_backend.models.process_instance_report import (
|
||||||
ProcessInstanceReportModel,
|
ProcessInstanceReportModel,
|
||||||
) # noqa: F401
|
) # noqa: F401
|
||||||
|
from spiffworkflow_backend.models.refresh_token import RefreshTokenModel # noqa: F401
|
||||||
from spiffworkflow_backend.models.secret_model import SecretModel # noqa: F401
|
from spiffworkflow_backend.models.secret_model import SecretModel # noqa: F401
|
||||||
from spiffworkflow_backend.models.spiff_logging import SpiffLoggingModel # noqa: F401
|
from spiffworkflow_backend.models.spiff_logging import SpiffLoggingModel # noqa: F401
|
||||||
from spiffworkflow_backend.models.task_event import TaskEventModel # noqa: F401
|
from spiffworkflow_backend.models.task_event import TaskEventModel # noqa: F401
|
||||||
|
|
|
@ -29,4 +29,4 @@ class GroupModel(FlaskBpmnGroupModel):
|
||||||
secondary="user_group_assignment",
|
secondary="user_group_assignment",
|
||||||
overlaps="user_group_assignments,users",
|
overlaps="user_group_assignments,users",
|
||||||
)
|
)
|
||||||
principal = relationship("PrincipalModel", uselist=False) # type: ignore
|
principal = relationship("PrincipalModel", uselist=False, cascade="all, delete") # type: ignore
|
||||||
|
|
|
@ -31,7 +31,13 @@ class Permission(enum.Enum):
|
||||||
read = "read"
|
read = "read"
|
||||||
update = "update"
|
update = "update"
|
||||||
delete = "delete"
|
delete = "delete"
|
||||||
|
|
||||||
|
# maybe read to GET process_model/process-instances instead?
|
||||||
list = "list"
|
list = "list"
|
||||||
|
|
||||||
|
# maybe use create instead on
|
||||||
|
# POST http://localhost:7000/v1.0/process-models/category_number_one/call-activity/process-instances/*
|
||||||
|
# POST http://localhost:7000/v1.0/process-models/category_number_one/call-activity/process-instances/332/run
|
||||||
instantiate = "instantiate" # this is something you do to a process model
|
instantiate = "instantiate" # this is something you do to a process model
|
||||||
|
|
||||||
|
|
||||||
|
@ -50,10 +56,10 @@ class PermissionAssignmentModel(SpiffworkflowBaseDBModel):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
principal_id = db.Column(ForeignKey(PrincipalModel.id), nullable=False)
|
principal_id = db.Column(ForeignKey(PrincipalModel.id), nullable=False)
|
||||||
permission_target_id = db.Column(
|
permission_target_id = db.Column(
|
||||||
ForeignKey(PermissionTargetModel.id), nullable=False
|
ForeignKey(PermissionTargetModel.id), nullable=False # type: ignore
|
||||||
)
|
)
|
||||||
grant_type = db.Column(db.String(50))
|
grant_type = db.Column(db.String(50), nullable=False)
|
||||||
permission = db.Column(db.String(50))
|
permission = db.Column(db.String(50), nullable=False)
|
||||||
|
|
||||||
@validates("grant_type")
|
@validates("grant_type")
|
||||||
def validate_grant_type(self, key: str, value: str) -> Any:
|
def validate_grant_type(self, key: str, value: str) -> Any:
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
"""PermissionTarget."""
|
"""PermissionTarget."""
|
||||||
import re
|
import re
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
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
|
||||||
|
@ -10,13 +12,23 @@ class InvalidPermissionTargetUriError(Exception):
|
||||||
"""InvalidPermissionTargetUriError."""
|
"""InvalidPermissionTargetUriError."""
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
class PermissionTargetModel(SpiffworkflowBaseDBModel):
|
class PermissionTargetModel(SpiffworkflowBaseDBModel):
|
||||||
"""PermissionTargetModel."""
|
"""PermissionTargetModel."""
|
||||||
|
|
||||||
|
URI_ALL = "/%"
|
||||||
|
|
||||||
__tablename__ = "permission_target"
|
__tablename__ = "permission_target"
|
||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id: int = db.Column(db.Integer, primary_key=True)
|
||||||
uri = db.Column(db.String(255), unique=True, nullable=False)
|
uri: str = db.Column(db.String(255), unique=True, nullable=False)
|
||||||
|
|
||||||
|
def __init__(self, uri: str, id: Optional[int] = None):
|
||||||
|
"""__init__."""
|
||||||
|
if id:
|
||||||
|
self.id = id
|
||||||
|
uri_with_percent = re.sub(r"\*", "%", uri)
|
||||||
|
self.uri = uri_with_percent
|
||||||
|
|
||||||
@validates("uri")
|
@validates("uri")
|
||||||
def validate_uri(self, key: str, value: str) -> str:
|
def validate_uri(self, key: str, value: str) -> str:
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
"""Refresh_token."""
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from flask_bpmn.models.db import db
|
||||||
|
from flask_bpmn.models.db import SpiffworkflowBaseDBModel
|
||||||
|
from sqlalchemy import ForeignKey
|
||||||
|
|
||||||
|
# from sqlalchemy.orm import relationship
|
||||||
|
|
||||||
|
# from spiffworkflow_backend.models.user import UserModel
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass()
|
||||||
|
class RefreshTokenModel(SpiffworkflowBaseDBModel):
|
||||||
|
"""RefreshTokenModel."""
|
||||||
|
|
||||||
|
__tablename__ = "refresh_token"
|
||||||
|
|
||||||
|
id: int = db.Column(db.Integer, primary_key=True)
|
||||||
|
user_id: int = db.Column(ForeignKey("user.id"), nullable=False, unique=True)
|
||||||
|
token: str = db.Column(db.String(1024), nullable=False)
|
||||||
|
# user = relationship("UserModel", back_populates="refresh_token")
|
|
@ -18,6 +18,8 @@ class SecretModel(SpiffworkflowBaseDBModel):
|
||||||
key: str = db.Column(db.String(50), unique=True, nullable=False)
|
key: str = db.Column(db.String(50), unique=True, nullable=False)
|
||||||
value: str = db.Column(db.Text(), nullable=False)
|
value: str = db.Column(db.Text(), nullable=False)
|
||||||
creator_user_id: int = db.Column(ForeignKey(UserModel.id), nullable=False)
|
creator_user_id: int = db.Column(ForeignKey(UserModel.id), nullable=False)
|
||||||
|
updated_at_in_seconds: int = db.Column(db.Integer)
|
||||||
|
created_at_in_seconds: int = db.Column(db.Integer)
|
||||||
|
|
||||||
|
|
||||||
class SecretModelSchema(Schema):
|
class SecretModelSchema(Schema):
|
||||||
|
|
|
@ -55,6 +55,7 @@ from spiffworkflow_backend.models.secret_model import SecretModelSchema
|
||||||
from spiffworkflow_backend.models.spiff_logging import SpiffLoggingModel
|
from spiffworkflow_backend.models.spiff_logging import SpiffLoggingModel
|
||||||
from spiffworkflow_backend.models.user import UserModel
|
from spiffworkflow_backend.models.user import UserModel
|
||||||
from spiffworkflow_backend.routes.user import verify_token
|
from spiffworkflow_backend.routes.user import verify_token
|
||||||
|
from spiffworkflow_backend.services.authorization_service import AuthorizationService
|
||||||
from spiffworkflow_backend.services.error_handling_service import ErrorHandlingService
|
from spiffworkflow_backend.services.error_handling_service import ErrorHandlingService
|
||||||
from spiffworkflow_backend.services.file_system_service import FileSystemService
|
from spiffworkflow_backend.services.file_system_service import FileSystemService
|
||||||
from spiffworkflow_backend.services.git_service import GitService
|
from spiffworkflow_backend.services.git_service import GitService
|
||||||
|
@ -97,6 +98,39 @@ def status() -> flask.wrappers.Response:
|
||||||
return Response(json.dumps({"ok": True}), status=200, mimetype="application/json")
|
return Response(json.dumps({"ok": True}), status=200, mimetype="application/json")
|
||||||
|
|
||||||
|
|
||||||
|
def permissions_check(body: Dict[str, Dict[str, list[str]]]) -> flask.wrappers.Response:
|
||||||
|
"""Permissions_check."""
|
||||||
|
if "requests_to_check" not in body:
|
||||||
|
raise (
|
||||||
|
ApiError(
|
||||||
|
error_code="could_not_requests_to_check",
|
||||||
|
message="The key 'requests_to_check' not found at root of request body.",
|
||||||
|
status_code=400,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
response_dict: dict[str, dict[str, bool]] = {}
|
||||||
|
requests_to_check = body["requests_to_check"]
|
||||||
|
|
||||||
|
for target_uri, http_methods in requests_to_check.items():
|
||||||
|
if target_uri not in response_dict:
|
||||||
|
response_dict[target_uri] = {}
|
||||||
|
|
||||||
|
for http_method in http_methods:
|
||||||
|
permission_string = AuthorizationService.get_permission_from_http_method(
|
||||||
|
http_method
|
||||||
|
)
|
||||||
|
if permission_string:
|
||||||
|
has_permission = AuthorizationService.user_has_permission(
|
||||||
|
user=g.user,
|
||||||
|
permission=permission_string,
|
||||||
|
target_uri=target_uri,
|
||||||
|
)
|
||||||
|
response_dict[target_uri][http_method] = has_permission
|
||||||
|
|
||||||
|
return make_response(jsonify({"results": response_dict}), 200)
|
||||||
|
|
||||||
|
|
||||||
def process_group_add(
|
def process_group_add(
|
||||||
body: Dict[str, Union[str, bool, int]]
|
body: Dict[str, Union[str, bool, int]]
|
||||||
) -> flask.wrappers.Response:
|
) -> flask.wrappers.Response:
|
||||||
|
@ -794,9 +828,8 @@ def authentication_callback(
|
||||||
auth_method: str,
|
auth_method: str,
|
||||||
) -> werkzeug.wrappers.Response:
|
) -> werkzeug.wrappers.Response:
|
||||||
"""Authentication_callback."""
|
"""Authentication_callback."""
|
||||||
verify_token(request.args.get("token"))
|
verify_token(request.args.get("token"), force_run=True)
|
||||||
response = request.args["response"]
|
response = request.args["response"]
|
||||||
print(f"response: {response}")
|
|
||||||
SecretService().update_secret(
|
SecretService().update_secret(
|
||||||
f"{service}/{auth_method}", response, g.user.id, create_if_not_exists=True
|
f"{service}/{auth_method}", response, g.user.id, create_if_not_exists=True
|
||||||
)
|
)
|
||||||
|
@ -848,6 +881,8 @@ def process_instance_report_show(
|
||||||
return Response(json.dumps(result_dict), status=200, mimetype="application/json")
|
return Response(json.dumps(result_dict), status=200, mimetype="application/json")
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: see comment for before_request
|
||||||
|
# @process_api_blueprint.route("/v1.0/tasks", methods=["GET"])
|
||||||
def task_list_my_tasks(page: int = 1, per_page: int = 100) -> flask.wrappers.Response:
|
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()
|
||||||
|
|
|
@ -10,12 +10,13 @@ import jwt
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
from flask import g
|
from flask import g
|
||||||
from flask import redirect
|
from flask import redirect
|
||||||
|
from flask import request
|
||||||
from flask_bpmn.api.api_error import ApiError
|
from flask_bpmn.api.api_error import ApiError
|
||||||
from werkzeug.wrappers import Response
|
from werkzeug.wrappers import Response
|
||||||
|
|
||||||
from spiffworkflow_backend.models.user import UserModel
|
from spiffworkflow_backend.models.user import UserModel
|
||||||
from spiffworkflow_backend.services.authentication_service import (
|
from spiffworkflow_backend.services.authentication_service import (
|
||||||
PublicAuthenticationService,
|
AuthenticationService,
|
||||||
)
|
)
|
||||||
from spiffworkflow_backend.services.authorization_service import AuthorizationService
|
from spiffworkflow_backend.services.authorization_service import AuthorizationService
|
||||||
from spiffworkflow_backend.services.user_service import UserService
|
from spiffworkflow_backend.services.user_service import UserService
|
||||||
|
@ -26,13 +27,17 @@ from spiffworkflow_backend.services.user_service import UserService
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def verify_token(token: Optional[str] = None) -> Dict[str, Optional[Union[str, int]]]:
|
# authorization_exclusion_list = ['status']
|
||||||
|
def verify_token(
|
||||||
|
token: Optional[str] = None, force_run: Optional[bool] = False
|
||||||
|
) -> Optional[Dict[str, Optional[Union[str, int]]]]:
|
||||||
"""Verify the token for the user (if provided).
|
"""Verify the token for the user (if provided).
|
||||||
|
|
||||||
If in production environment and token is not provided, gets user from the SSO headers and returns their token.
|
If in production environment and token is not provided, gets user from the SSO headers and returns their token.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
token: Optional[str]
|
token: Optional[str]
|
||||||
|
force_run: Optional[bool]
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
token: str
|
token: str
|
||||||
|
@ -41,6 +46,12 @@ def verify_token(token: Optional[str] = None) -> Dict[str, Optional[Union[str, i
|
||||||
ApiError: If not on production and token is not valid, returns an 'invalid_token' 403 error.
|
ApiError: If not on production and token is not valid, returns an 'invalid_token' 403 error.
|
||||||
If on production and user is not authenticated, returns a 'no_user' 403 error.
|
If on production and user is not authenticated, returns a 'no_user' 403 error.
|
||||||
"""
|
"""
|
||||||
|
if not force_run and AuthorizationService.should_disable_auth_for_request():
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not token and "Authorization" in request.headers:
|
||||||
|
token = request.headers["Authorization"].removeprefix("Bearer ")
|
||||||
|
|
||||||
if token:
|
if token:
|
||||||
user_model = None
|
user_model = None
|
||||||
decoded_token = get_decoded_token(token)
|
decoded_token = get_decoded_token(token)
|
||||||
|
@ -59,10 +70,34 @@ def verify_token(token: Optional[str] = None) -> Dict[str, Optional[Union[str, i
|
||||||
|
|
||||||
elif "iss" in decoded_token.keys():
|
elif "iss" in decoded_token.keys():
|
||||||
try:
|
try:
|
||||||
user_info = PublicAuthenticationService.get_user_info_from_id_token(
|
user_info = AuthenticationService.get_user_info_from_open_id(token)
|
||||||
token
|
|
||||||
)
|
|
||||||
except ApiError as ae:
|
except ApiError as ae:
|
||||||
|
# Try to refresh the token
|
||||||
|
user = UserService.get_user_by_service_and_service_id(
|
||||||
|
"open_id", decoded_token["sub"]
|
||||||
|
)
|
||||||
|
if user:
|
||||||
|
refresh_token = AuthenticationService.get_refresh_token(user.id)
|
||||||
|
if refresh_token:
|
||||||
|
auth_token: dict = (
|
||||||
|
AuthenticationService.get_auth_token_from_refresh_token(
|
||||||
|
refresh_token
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if auth_token and "error" not in auth_token:
|
||||||
|
# redirect to original url, with auth_token?
|
||||||
|
user_info = (
|
||||||
|
AuthenticationService.get_user_info_from_open_id(
|
||||||
|
auth_token["access_token"]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if not user_info:
|
||||||
|
raise ae
|
||||||
|
else:
|
||||||
|
raise ae
|
||||||
|
else:
|
||||||
|
raise ae
|
||||||
|
else:
|
||||||
raise ae
|
raise ae
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
current_app.logger.error(f"Exception raised in get_token: {e}")
|
current_app.logger.error(f"Exception raised in get_token: {e}")
|
||||||
|
@ -106,9 +141,11 @@ def verify_token(token: Optional[str] = None) -> Dict[str, Optional[Union[str, i
|
||||||
|
|
||||||
# If the user is valid, store the token for this session
|
# If the user is valid, store the token for this session
|
||||||
if g.user:
|
if g.user:
|
||||||
|
# This is an id token, so we don't have a refresh token yet
|
||||||
g.token = token
|
g.token = token
|
||||||
scope = get_scope(token)
|
get_scope(token)
|
||||||
return {"uid": g.user.id, "sub": g.user.id, "scope": scope}
|
return None
|
||||||
|
# return {"uid": g.user.id, "sub": g.user.id, "scope": scope}
|
||||||
# return validate_scope(token, user_info, user_model)
|
# return validate_scope(token, user_info, user_model)
|
||||||
else:
|
else:
|
||||||
raise ApiError(error_code="no_user_id", message="Cannot get a user id")
|
raise ApiError(error_code="no_user_id", message="Cannot get a user id")
|
||||||
|
@ -116,67 +153,20 @@ def verify_token(token: Optional[str] = None) -> Dict[str, Optional[Union[str, i
|
||||||
raise ApiError(
|
raise ApiError(
|
||||||
error_code="invalid_token", message="Cannot validate token.", status_code=401
|
error_code="invalid_token", message="Cannot validate token.", status_code=401
|
||||||
)
|
)
|
||||||
# no token -- do we ever get here?
|
|
||||||
# else:
|
|
||||||
# ...
|
|
||||||
# if current_app.config.get("DEVELOPMENT"):
|
|
||||||
# # Fall back to a default user if this is not production.
|
|
||||||
# g.user = UserModel.query.first()
|
|
||||||
# if not g.user:
|
|
||||||
# raise ApiError(
|
|
||||||
# "no_user",
|
|
||||||
# "You are in development mode, but there are no users in the database. Add one, and it will use it.",
|
|
||||||
# )
|
|
||||||
# token_from_user = g.user.encode_auth_token()
|
|
||||||
# token_info = UserModel.decode_auth_token(token_from_user)
|
|
||||||
# return token_info
|
|
||||||
#
|
|
||||||
# else:
|
|
||||||
# raise ApiError(
|
|
||||||
# error_code="no_auth_token",
|
|
||||||
# message="No authorization token was available.",
|
|
||||||
# status_code=401,
|
|
||||||
# )
|
|
||||||
|
|
||||||
|
|
||||||
def validate_scope(token: Any) -> bool:
|
def validate_scope(token: Any) -> bool:
|
||||||
"""Validate_scope."""
|
"""Validate_scope."""
|
||||||
print("validate_scope")
|
print("validate_scope")
|
||||||
# token = PublicAuthenticationService.refresh_token(token)
|
# token = AuthenticationService.refresh_token(token)
|
||||||
# user_info = PublicAuthenticationService.get_user_info_from_public_access_token(token)
|
# user_info = AuthenticationService.get_user_info_from_public_access_token(token)
|
||||||
# bearer_token = PublicAuthenticationService.get_bearer_token(token)
|
# bearer_token = AuthenticationService.get_bearer_token(token)
|
||||||
# permission = PublicAuthenticationService.get_permission_by_basic_token(token)
|
# permission = AuthenticationService.get_permission_by_basic_token(token)
|
||||||
# permissions = PublicAuthenticationService.get_permissions_by_token_for_resource_and_scope(token)
|
# permissions = AuthenticationService.get_permissions_by_token_for_resource_and_scope(token)
|
||||||
# introspection = PublicAuthenticationService.introspect_token(basic_token)
|
# introspection = AuthenticationService.introspect_token(basic_token)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
# def login_api(redirect_url: str = "/v1.0/ui") -> Response:
|
|
||||||
# """Api_login."""
|
|
||||||
# # TODO: Fix this! mac 20220801
|
|
||||||
# # token:dict = PublicAuthenticationService().get_public_access_token(uid, password)
|
|
||||||
# #
|
|
||||||
# # return token
|
|
||||||
# # if uid:
|
|
||||||
# # sub = f"service:internal::service_id:{uid}"
|
|
||||||
# # token = encode_auth_token(sub)
|
|
||||||
# # user_model = UserModel(username=uid,
|
|
||||||
# # uid=uid,
|
|
||||||
# # service='internal',
|
|
||||||
# # name="API User")
|
|
||||||
# # g.user = user_model
|
|
||||||
# #
|
|
||||||
# # g.token = token
|
|
||||||
# # scope = get_scope(token)
|
|
||||||
# # return token
|
|
||||||
# # return {"uid": uid, "sub": uid, "scope": scope}
|
|
||||||
# return login(redirect_url)
|
|
||||||
|
|
||||||
|
|
||||||
# def login_api_return(code: str, state: str, session_state: str) -> Optional[Response]:
|
|
||||||
# print("login_api_return")
|
|
||||||
|
|
||||||
|
|
||||||
def encode_auth_token(sub: str, token_type: Optional[str] = None) -> str:
|
def encode_auth_token(sub: str, token_type: Optional[str] = None) -> str:
|
||||||
"""Generates the Auth Token.
|
"""Generates the Auth Token.
|
||||||
|
|
||||||
|
@ -202,8 +192,8 @@ def encode_auth_token(sub: str, token_type: Optional[str] = None) -> str:
|
||||||
|
|
||||||
def login(redirect_url: str = "/") -> Response:
|
def login(redirect_url: str = "/") -> Response:
|
||||||
"""Login."""
|
"""Login."""
|
||||||
state = PublicAuthenticationService.generate_state(redirect_url)
|
state = AuthenticationService.generate_state(redirect_url)
|
||||||
login_redirect_url = PublicAuthenticationService().get_login_redirect_url(
|
login_redirect_url = AuthenticationService().get_login_redirect_url(
|
||||||
state.decode("UTF-8")
|
state.decode("UTF-8")
|
||||||
)
|
)
|
||||||
return redirect(login_redirect_url)
|
return redirect(login_redirect_url)
|
||||||
|
@ -214,13 +204,13 @@ def login_return(code: str, state: str, session_state: str) -> Optional[Response
|
||||||
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"]
|
||||||
|
|
||||||
id_token_object = PublicAuthenticationService().get_id_token_object(code)
|
auth_token_object = AuthenticationService().get_auth_token_object(code)
|
||||||
if "id_token" in id_token_object:
|
if "id_token" in auth_token_object:
|
||||||
id_token = id_token_object["id_token"]
|
id_token = auth_token_object["id_token"]
|
||||||
|
|
||||||
if PublicAuthenticationService.validate_id_token(id_token):
|
if AuthenticationService.validate_id_token(id_token):
|
||||||
user_info = PublicAuthenticationService.get_user_info_from_id_token(
|
user_info = AuthenticationService.get_user_info_from_open_id(
|
||||||
id_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 = (
|
||||||
|
@ -250,6 +240,10 @@ def login_return(code: str, state: str, session_state: str) -> Optional[Response
|
||||||
|
|
||||||
if user_model:
|
if user_model:
|
||||||
g.user = user_model.id
|
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.
|
# this may eventually get too slow.
|
||||||
# when it does, be careful about backgrounding, because
|
# when it does, be careful about backgrounding, because
|
||||||
|
@ -261,7 +255,7 @@ def login_return(code: str, state: str, session_state: str) -> Optional[Response
|
||||||
|
|
||||||
redirect_url = (
|
redirect_url = (
|
||||||
f"{state_redirect_url}?"
|
f"{state_redirect_url}?"
|
||||||
+ f"access_token={id_token_object['access_token']}&"
|
+ f"access_token={auth_token_object['access_token']}&"
|
||||||
+ f"id_token={id_token}"
|
+ f"id_token={id_token}"
|
||||||
)
|
)
|
||||||
return redirect(redirect_url)
|
return redirect(redirect_url)
|
||||||
|
@ -283,8 +277,8 @@ def login_return(code: str, state: str, session_state: str) -> Optional[Response
|
||||||
def login_api() -> Response:
|
def login_api() -> Response:
|
||||||
"""Login_api."""
|
"""Login_api."""
|
||||||
redirect_url = "/v1.0/login_api_return"
|
redirect_url = "/v1.0/login_api_return"
|
||||||
state = PublicAuthenticationService.generate_state(redirect_url)
|
state = AuthenticationService.generate_state(redirect_url)
|
||||||
login_redirect_url = PublicAuthenticationService().get_login_redirect_url(
|
login_redirect_url = AuthenticationService().get_login_redirect_url(
|
||||||
state.decode("UTF-8"), redirect_url
|
state.decode("UTF-8"), redirect_url
|
||||||
)
|
)
|
||||||
return redirect(login_redirect_url)
|
return redirect(login_redirect_url)
|
||||||
|
@ -295,10 +289,10 @@ def login_api_return(code: str, state: str, session_state: str) -> str:
|
||||||
state_dict = ast.literal_eval(base64.b64decode(state).decode("utf-8"))
|
state_dict = ast.literal_eval(base64.b64decode(state).decode("utf-8"))
|
||||||
state_dict["redirect_url"]
|
state_dict["redirect_url"]
|
||||||
|
|
||||||
id_token_object = PublicAuthenticationService().get_id_token_object(
|
auth_token_object = AuthenticationService().get_auth_token_object(
|
||||||
code, "/v1.0/login_api_return"
|
code, "/v1.0/login_api_return"
|
||||||
)
|
)
|
||||||
access_token: str = id_token_object["access_token"]
|
access_token: str = auth_token_object["access_token"]
|
||||||
assert access_token # noqa: S101
|
assert access_token # noqa: S101
|
||||||
return access_token
|
return access_token
|
||||||
# return redirect("localhost:7000/v1.0/ui")
|
# return redirect("localhost:7000/v1.0/ui")
|
||||||
|
@ -309,9 +303,7 @@ def logout(id_token: str, redirect_url: Optional[str]) -> Response:
|
||||||
"""Logout."""
|
"""Logout."""
|
||||||
if redirect_url is None:
|
if redirect_url is None:
|
||||||
redirect_url = ""
|
redirect_url = ""
|
||||||
return PublicAuthenticationService().logout(
|
return AuthenticationService().logout(redirect_url=redirect_url, id_token=id_token)
|
||||||
redirect_url=redirect_url, id_token=id_token
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def logout_return() -> Response:
|
def logout_return() -> Response:
|
||||||
|
|
|
@ -10,8 +10,11 @@ import requests
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
from flask import redirect
|
from flask import redirect
|
||||||
from flask_bpmn.api.api_error import ApiError
|
from flask_bpmn.api.api_error import ApiError
|
||||||
|
from flask_bpmn.models.db import db
|
||||||
from werkzeug.wrappers import Response
|
from werkzeug.wrappers import Response
|
||||||
|
|
||||||
|
from spiffworkflow_backend.models.refresh_token import RefreshTokenModel
|
||||||
|
|
||||||
|
|
||||||
class AuthenticationProviderTypes(enum.Enum):
|
class AuthenticationProviderTypes(enum.Enum):
|
||||||
"""AuthenticationServiceProviders."""
|
"""AuthenticationServiceProviders."""
|
||||||
|
@ -20,13 +23,8 @@ class AuthenticationProviderTypes(enum.Enum):
|
||||||
internal = "internal"
|
internal = "internal"
|
||||||
|
|
||||||
|
|
||||||
class PublicAuthenticationService:
|
class AuthenticationService:
|
||||||
"""PublicAuthenticationService."""
|
"""AuthenticationService."""
|
||||||
|
|
||||||
"""Not sure where/if this ultimately lives.
|
|
||||||
It uses a separate public open_id client: spiffworkflow-frontend
|
|
||||||
Used during development to make testing easy.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_open_id_args() -> tuple:
|
def get_open_id_args() -> tuple:
|
||||||
|
@ -45,8 +43,8 @@ class PublicAuthenticationService:
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_user_info_from_id_token(cls, token: str) -> dict:
|
def get_user_info_from_open_id(cls, token: str) -> dict:
|
||||||
"""This seems to work with basic tokens too."""
|
"""The token is an auth_token."""
|
||||||
(
|
(
|
||||||
open_id_server_url,
|
open_id_server_url,
|
||||||
open_id_client_id,
|
open_id_client_id,
|
||||||
|
@ -54,10 +52,6 @@ class PublicAuthenticationService:
|
||||||
open_id_client_secret_key,
|
open_id_client_secret_key,
|
||||||
) = cls.get_open_id_args()
|
) = cls.get_open_id_args()
|
||||||
|
|
||||||
# backend_basic_auth_string = f"{open_id_client_id}:{open_id_client_secret_key}"
|
|
||||||
# backend_basic_auth_bytes = bytes(backend_basic_auth_string, encoding="ascii")
|
|
||||||
# backend_basic_auth = base64.b64encode(backend_basic_auth_bytes)
|
|
||||||
|
|
||||||
headers = {"Authorization": f"Bearer {token}"}
|
headers = {"Authorization": f"Bearer {token}"}
|
||||||
|
|
||||||
request_url = f"{open_id_server_url}/realms/{open_id_realm_name}/protocol/openid-connect/userinfo"
|
request_url = f"{open_id_server_url}/realms/{open_id_realm_name}/protocol/openid-connect/userinfo"
|
||||||
|
@ -85,7 +79,8 @@ class PublicAuthenticationService:
|
||||||
status_code=401,
|
status_code=401,
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_backend_url(self) -> str:
|
@staticmethod
|
||||||
|
def get_backend_url() -> str:
|
||||||
"""Get_backend_url."""
|
"""Get_backend_url."""
|
||||||
return str(current_app.config["SPIFFWORKFLOW_BACKEND_URL"])
|
return str(current_app.config["SPIFFWORKFLOW_BACKEND_URL"])
|
||||||
|
|
||||||
|
@ -99,7 +94,7 @@ class PublicAuthenticationService:
|
||||||
open_id_client_id,
|
open_id_client_id,
|
||||||
open_id_realm_name,
|
open_id_realm_name,
|
||||||
open_id_client_secret_key,
|
open_id_client_secret_key,
|
||||||
) = PublicAuthenticationService.get_open_id_args()
|
) = AuthenticationService.get_open_id_args()
|
||||||
request_url = (
|
request_url = (
|
||||||
f"{open_id_server_url}/realms/{open_id_realm_name}/protocol/openid-connect/logout?"
|
f"{open_id_server_url}/realms/{open_id_realm_name}/protocol/openid-connect/logout?"
|
||||||
+ f"post_logout_redirect_uri={return_redirect_url}&"
|
+ f"post_logout_redirect_uri={return_redirect_url}&"
|
||||||
|
@ -123,7 +118,7 @@ class PublicAuthenticationService:
|
||||||
open_id_client_id,
|
open_id_client_id,
|
||||||
open_id_realm_name,
|
open_id_realm_name,
|
||||||
open_id_client_secret_key,
|
open_id_client_secret_key,
|
||||||
) = PublicAuthenticationService.get_open_id_args()
|
) = AuthenticationService.get_open_id_args()
|
||||||
return_redirect_url = f"{self.get_backend_url()}{redirect_url}"
|
return_redirect_url = f"{self.get_backend_url()}{redirect_url}"
|
||||||
login_redirect_url = (
|
login_redirect_url = (
|
||||||
f"{open_id_server_url}/realms/{open_id_realm_name}/protocol/openid-connect/auth?"
|
f"{open_id_server_url}/realms/{open_id_realm_name}/protocol/openid-connect/auth?"
|
||||||
|
@ -135,16 +130,16 @@ class PublicAuthenticationService:
|
||||||
)
|
)
|
||||||
return login_redirect_url
|
return login_redirect_url
|
||||||
|
|
||||||
def get_id_token_object(
|
def get_auth_token_object(
|
||||||
self, code: str, redirect_url: str = "/v1.0/login_return"
|
self, code: str, redirect_url: str = "/v1.0/login_return"
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""Get_id_token_object."""
|
"""Get_auth_token_object."""
|
||||||
(
|
(
|
||||||
open_id_server_url,
|
open_id_server_url,
|
||||||
open_id_client_id,
|
open_id_client_id,
|
||||||
open_id_realm_name,
|
open_id_realm_name,
|
||||||
open_id_client_secret_key,
|
open_id_client_secret_key,
|
||||||
) = PublicAuthenticationService.get_open_id_args()
|
) = AuthenticationService.get_open_id_args()
|
||||||
|
|
||||||
backend_basic_auth_string = f"{open_id_client_id}:{open_id_client_secret_key}"
|
backend_basic_auth_string = f"{open_id_client_id}:{open_id_client_secret_key}"
|
||||||
backend_basic_auth_bytes = bytes(backend_basic_auth_string, encoding="ascii")
|
backend_basic_auth_bytes = bytes(backend_basic_auth_string, encoding="ascii")
|
||||||
|
@ -162,8 +157,8 @@ class PublicAuthenticationService:
|
||||||
request_url = f"{open_id_server_url}/realms/{open_id_realm_name}/protocol/openid-connect/token"
|
request_url = f"{open_id_server_url}/realms/{open_id_realm_name}/protocol/openid-connect/token"
|
||||||
|
|
||||||
response = requests.post(request_url, data=data, headers=headers)
|
response = requests.post(request_url, data=data, headers=headers)
|
||||||
id_token_object: dict = json.loads(response.text)
|
auth_token_object: dict = json.loads(response.text)
|
||||||
return id_token_object
|
return auth_token_object
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_id_token(cls, id_token: str) -> bool:
|
def validate_id_token(cls, id_token: str) -> bool:
|
||||||
|
@ -211,3 +206,65 @@ class PublicAuthenticationService:
|
||||||
)
|
)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def store_refresh_token(user_id: int, refresh_token: str) -> None:
|
||||||
|
"""Store_refresh_token."""
|
||||||
|
refresh_token_model = RefreshTokenModel.query.filter(
|
||||||
|
RefreshTokenModel.user_id == user_id
|
||||||
|
).first()
|
||||||
|
if refresh_token_model:
|
||||||
|
refresh_token_model.token = refresh_token
|
||||||
|
else:
|
||||||
|
refresh_token_model = RefreshTokenModel(
|
||||||
|
user_id=user_id, token=refresh_token
|
||||||
|
)
|
||||||
|
db.session.add(refresh_token_model)
|
||||||
|
try:
|
||||||
|
db.session.commit()
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
raise ApiError(
|
||||||
|
error_code="store_refresh_token_error",
|
||||||
|
message=f"We could not store the refresh token. Original error is {e}",
|
||||||
|
) from e
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_refresh_token(user_id: int) -> Optional[str]:
|
||||||
|
"""Get_refresh_token."""
|
||||||
|
refresh_token_object: RefreshTokenModel = RefreshTokenModel.query.filter(
|
||||||
|
RefreshTokenModel.user_id == user_id
|
||||||
|
).first()
|
||||||
|
assert refresh_token_object # noqa: S101
|
||||||
|
return refresh_token_object.token
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_auth_token_from_refresh_token(cls, refresh_token: str) -> dict:
|
||||||
|
"""Get a new auth_token from a refresh_token."""
|
||||||
|
(
|
||||||
|
open_id_server_url,
|
||||||
|
open_id_client_id,
|
||||||
|
open_id_realm_name,
|
||||||
|
open_id_client_secret_key,
|
||||||
|
) = cls.get_open_id_args()
|
||||||
|
|
||||||
|
backend_basic_auth_string = f"{open_id_client_id}:{open_id_client_secret_key}"
|
||||||
|
backend_basic_auth_bytes = bytes(backend_basic_auth_string, encoding="ascii")
|
||||||
|
backend_basic_auth = base64.b64encode(backend_basic_auth_bytes)
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
"Authorization": f"Basic {backend_basic_auth.decode('utf-8')}",
|
||||||
|
}
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"grant_type": "refresh_token",
|
||||||
|
"refresh_token": refresh_token,
|
||||||
|
"client_id": open_id_client_id,
|
||||||
|
"client_secret": open_id_client_secret_key,
|
||||||
|
}
|
||||||
|
|
||||||
|
request_url = f"{open_id_server_url}/realms/{open_id_realm_name}/protocol/openid-connect/token"
|
||||||
|
|
||||||
|
response = requests.post(request_url, data=data, headers=headers)
|
||||||
|
auth_token_object: dict = json.loads(response.text)
|
||||||
|
return auth_token_object
|
||||||
|
|
|
@ -6,6 +6,8 @@ from typing import Union
|
||||||
import jwt
|
import jwt
|
||||||
import yaml
|
import yaml
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
|
from flask import g
|
||||||
|
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 sqlalchemy import text
|
from sqlalchemy import text
|
||||||
|
@ -21,6 +23,10 @@ from spiffworkflow_backend.models.user_group_assignment import UserGroupAssignme
|
||||||
from spiffworkflow_backend.services.user_service import UserService
|
from spiffworkflow_backend.services.user_service import UserService
|
||||||
|
|
||||||
|
|
||||||
|
class PermissionsFileNotSetError(Exception):
|
||||||
|
"""PermissionsFileNotSetError."""
|
||||||
|
|
||||||
|
|
||||||
class AuthorizationService:
|
class AuthorizationService:
|
||||||
"""Determine whether a user has permission to perform their request."""
|
"""Determine whether a user has permission to perform their request."""
|
||||||
|
|
||||||
|
@ -47,7 +53,9 @@ class AuthorizationService:
|
||||||
elif permission_assignment.grant_type == "deny":
|
elif permission_assignment.grant_type == "deny":
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
raise Exception("Unknown grant type")
|
raise Exception(
|
||||||
|
f"Unknown grant type: {permission_assignment.grant_type}"
|
||||||
|
)
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -72,11 +80,31 @@ class AuthorizationService:
|
||||||
|
|
||||||
return cls.has_permission(principals, permission, target_uri)
|
return cls.has_permission(principals, permission, target_uri)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def delete_all_permissions_and_recreate(cls) -> None:
|
||||||
|
"""Delete_all_permissions_and_recreate."""
|
||||||
|
for model in [PermissionAssignmentModel, PermissionTargetModel]:
|
||||||
|
db.session.query(model).delete()
|
||||||
|
|
||||||
|
# cascading to principals doesn't seem to work when attempting to delete all so do it like this instead
|
||||||
|
for group in GroupModel.query.all():
|
||||||
|
db.session.delete(group)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
cls.import_permissions_from_yaml_file()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def import_permissions_from_yaml_file(
|
def import_permissions_from_yaml_file(
|
||||||
cls, raise_if_missing_user: bool = False
|
cls, raise_if_missing_user: bool = False
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Import_permissions_from_yaml_file."""
|
"""Import_permissions_from_yaml_file."""
|
||||||
|
if current_app.config["SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME"] is None:
|
||||||
|
raise (
|
||||||
|
PermissionsFileNotSetError(
|
||||||
|
"SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME needs to be set in order to import permissions"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
permission_configs = None
|
permission_configs = None
|
||||||
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)
|
||||||
|
@ -171,6 +199,88 @@ class AuthorizationService:
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return permission_assignment
|
return permission_assignment
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def should_disable_auth_for_request(cls) -> bool:
|
||||||
|
"""Should_disable_auth_for_request."""
|
||||||
|
authentication_exclusion_list = ["status", "authentication_callback"]
|
||||||
|
if request.method == "OPTIONS":
|
||||||
|
return True
|
||||||
|
|
||||||
|
# if the endpoint does not exist then let the system 404
|
||||||
|
#
|
||||||
|
# for some reason this runs before connexion checks if the
|
||||||
|
# endpoint exists.
|
||||||
|
if not request.endpoint:
|
||||||
|
return True
|
||||||
|
|
||||||
|
api_view_function = current_app.view_functions[request.endpoint]
|
||||||
|
if (
|
||||||
|
api_view_function
|
||||||
|
and api_view_function.__name__.startswith("login")
|
||||||
|
or api_view_function.__name__.startswith("logout")
|
||||||
|
or api_view_function.__name__ in authentication_exclusion_list
|
||||||
|
):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_permission_from_http_method(cls, http_method: str) -> Optional[str]:
|
||||||
|
"""Get_permission_from_request_method."""
|
||||||
|
request_method_mapper = {
|
||||||
|
"POST": "create",
|
||||||
|
"GET": "read",
|
||||||
|
"PUT": "update",
|
||||||
|
"DELETE": "delete",
|
||||||
|
}
|
||||||
|
if http_method in request_method_mapper:
|
||||||
|
return request_method_mapper[http_method]
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
# TODO: we can add the before_request to the blueprint
|
||||||
|
# directly when we switch over from connexion routes
|
||||||
|
# to blueprint routes
|
||||||
|
# @process_api_blueprint.before_request
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def check_for_permission(cls) -> None:
|
||||||
|
"""Check_for_permission."""
|
||||||
|
if cls.should_disable_auth_for_request():
|
||||||
|
return None
|
||||||
|
|
||||||
|
authorization_exclusion_list = ["permissions_check"]
|
||||||
|
|
||||||
|
if not hasattr(g, "user"):
|
||||||
|
raise ApiError(
|
||||||
|
error_code="user_not_logged_in",
|
||||||
|
message="User is not logged in. Please log in",
|
||||||
|
status_code=401,
|
||||||
|
)
|
||||||
|
|
||||||
|
api_view_function = current_app.view_functions[request.endpoint]
|
||||||
|
if (
|
||||||
|
api_view_function
|
||||||
|
and api_view_function.__name__ in authorization_exclusion_list
|
||||||
|
):
|
||||||
|
return None
|
||||||
|
|
||||||
|
permission_string = cls.get_permission_from_http_method(request.method)
|
||||||
|
if permission_string:
|
||||||
|
has_permission = AuthorizationService.user_has_permission(
|
||||||
|
user=g.user,
|
||||||
|
permission=permission_string,
|
||||||
|
target_uri=request.path,
|
||||||
|
)
|
||||||
|
if has_permission:
|
||||||
|
return None
|
||||||
|
|
||||||
|
raise ApiError(
|
||||||
|
error_code="unauthorized",
|
||||||
|
message="User is not authorized to perform requested action.",
|
||||||
|
status_code=403,
|
||||||
|
)
|
||||||
|
|
||||||
# def refresh_token(self, token: str) -> str:
|
# def refresh_token(self, token: str) -> str:
|
||||||
# """Refresh_token."""
|
# """Refresh_token."""
|
||||||
# # if isinstance(token, str):
|
# # if isinstance(token, str):
|
||||||
|
|
|
@ -889,9 +889,6 @@ class ProcessInstanceProcessor:
|
||||||
self.process_bpmn_messages()
|
self.process_bpmn_messages()
|
||||||
self.queue_waiting_receive_messages()
|
self.queue_waiting_receive_messages()
|
||||||
|
|
||||||
if save:
|
|
||||||
self.save()
|
|
||||||
|
|
||||||
except WorkflowTaskExecException as we:
|
except WorkflowTaskExecException as we:
|
||||||
raise ApiError.from_workflow_exception("task_error", str(we), we) from we
|
raise ApiError.from_workflow_exception("task_error", str(we), we) from we
|
||||||
|
|
||||||
|
|
|
@ -299,3 +299,17 @@ class UserService:
|
||||||
ugam = UserGroupAssignmentModel(user_id=user.id, group_id=group.id)
|
ugam = UserGroupAssignmentModel(user_id=user.id, group_id=group.id)
|
||||||
db.session.add(ugam)
|
db.session.add(ugam)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_user_by_service_and_service_id(
|
||||||
|
service: str, service_id: str
|
||||||
|
) -> Optional[UserModel]:
|
||||||
|
"""Get_user_by_service_and_service_id."""
|
||||||
|
user: UserModel = (
|
||||||
|
UserModel.query.filter(UserModel.service == service)
|
||||||
|
.filter(UserModel.service_id == service_id)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if user:
|
||||||
|
return user
|
||||||
|
return None
|
||||||
|
|
|
@ -15,6 +15,8 @@ from flask_bpmn.models.db import db
|
||||||
from tests.spiffworkflow_backend.helpers.test_data import load_test_spec
|
from tests.spiffworkflow_backend.helpers.test_data import load_test_spec
|
||||||
from werkzeug.test import TestResponse # type: ignore
|
from werkzeug.test import TestResponse # type: ignore
|
||||||
|
|
||||||
|
from spiffworkflow_backend.models.permission_assignment import Permission
|
||||||
|
from spiffworkflow_backend.models.permission_target import PermissionTargetModel
|
||||||
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
|
||||||
|
@ -92,6 +94,7 @@ class BaseTest:
|
||||||
exception_notification_addresses: Optional[list] = None,
|
exception_notification_addresses: Optional[list] = None,
|
||||||
primary_process_id: Optional[str] = None,
|
primary_process_id: Optional[str] = None,
|
||||||
primary_file_name: Optional[str] = None,
|
primary_file_name: Optional[str] = None,
|
||||||
|
user: Optional[UserModel] = None,
|
||||||
) -> TestResponse:
|
) -> TestResponse:
|
||||||
"""Create_process_model."""
|
"""Create_process_model."""
|
||||||
process_model_service = ProcessModelService()
|
process_model_service = ProcessModelService()
|
||||||
|
@ -121,7 +124,9 @@ class BaseTest:
|
||||||
fault_or_suspend_on_exception=fault_or_suspend_on_exception,
|
fault_or_suspend_on_exception=fault_or_suspend_on_exception,
|
||||||
exception_notification_addresses=exception_notification_addresses,
|
exception_notification_addresses=exception_notification_addresses,
|
||||||
)
|
)
|
||||||
|
if user is None:
|
||||||
user = self.find_or_create_user()
|
user = self.find_or_create_user()
|
||||||
|
|
||||||
response = client.post(
|
response = client.post(
|
||||||
"/v1.0/process-models",
|
"/v1.0/process-models",
|
||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
|
@ -139,6 +144,7 @@ class BaseTest:
|
||||||
process_model: Optional[ProcessModelInfo] = None,
|
process_model: Optional[ProcessModelInfo] = None,
|
||||||
file_name: str = "random_fact.svg",
|
file_name: str = "random_fact.svg",
|
||||||
file_data: bytes = b"abcdef",
|
file_data: bytes = b"abcdef",
|
||||||
|
user: Optional[UserModel] = None,
|
||||||
) -> Any:
|
) -> Any:
|
||||||
"""Test_create_spec_file."""
|
"""Test_create_spec_file."""
|
||||||
if process_model is None:
|
if process_model is None:
|
||||||
|
@ -146,6 +152,7 @@ class BaseTest:
|
||||||
process_model_id, process_group_id=process_group_id
|
process_model_id, process_group_id=process_group_id
|
||||||
)
|
)
|
||||||
data = {"file": (io.BytesIO(file_data), file_name)}
|
data = {"file": (io.BytesIO(file_data), file_name)}
|
||||||
|
if user is None:
|
||||||
user = self.find_or_create_user()
|
user = self.find_or_create_user()
|
||||||
response = client.post(
|
response = client.post(
|
||||||
f"/v1.0/process-models/{process_model.process_group_id}/{process_model.id}/files",
|
f"/v1.0/process-models/{process_model.process_group_id}/{process_model.id}/files",
|
||||||
|
@ -194,7 +201,7 @@ class BaseTest:
|
||||||
# @staticmethod
|
# @staticmethod
|
||||||
# def get_public_access_token(username: str, password: str) -> dict:
|
# def get_public_access_token(username: str, password: str) -> dict:
|
||||||
# """Get_public_access_token."""
|
# """Get_public_access_token."""
|
||||||
# public_access_token = PublicAuthenticationService().get_public_access_token(
|
# public_access_token = AuthenticationService().get_public_access_token(
|
||||||
# username, password
|
# username, password
|
||||||
# )
|
# )
|
||||||
# return public_access_token
|
# return public_access_token
|
||||||
|
@ -218,6 +225,46 @@ class BaseTest:
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return process_instance
|
return process_instance
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_user_with_permission(
|
||||||
|
cls,
|
||||||
|
username: str,
|
||||||
|
target_uri: str = PermissionTargetModel.URI_ALL,
|
||||||
|
permission_names: Optional[list[str]] = None,
|
||||||
|
) -> UserModel:
|
||||||
|
"""Create_user_with_permission."""
|
||||||
|
user = BaseTest.find_or_create_user(username=username)
|
||||||
|
return cls.add_permissions_to_user(
|
||||||
|
user, target_uri=target_uri, permission_names=permission_names
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def add_permissions_to_user(
|
||||||
|
cls,
|
||||||
|
user: UserModel,
|
||||||
|
target_uri: str = PermissionTargetModel.URI_ALL,
|
||||||
|
permission_names: Optional[list[str]] = None,
|
||||||
|
) -> UserModel:
|
||||||
|
"""Add_permissions_to_user."""
|
||||||
|
permission_target = PermissionTargetModel.query.filter_by(
|
||||||
|
uri=target_uri
|
||||||
|
).first()
|
||||||
|
if permission_target is None:
|
||||||
|
permission_target = PermissionTargetModel(uri=target_uri)
|
||||||
|
db.session.add(permission_target)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
if permission_names is None:
|
||||||
|
permission_names = [member.name for member in Permission]
|
||||||
|
|
||||||
|
for permission in permission_names:
|
||||||
|
AuthorizationService.create_permission_for_principal(
|
||||||
|
principal=user.principal,
|
||||||
|
permission_target=permission_target,
|
||||||
|
permission=permission,
|
||||||
|
)
|
||||||
|
return user
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def logged_in_headers(
|
def logged_in_headers(
|
||||||
user: UserModel, _redirect_url: str = "http://some/frontend/url"
|
user: UserModel, _redirect_url: str = "http://some/frontend/url"
|
||||||
|
|
|
@ -5,7 +5,7 @@ import base64
|
||||||
from tests.spiffworkflow_backend.helpers.base_test import BaseTest
|
from tests.spiffworkflow_backend.helpers.base_test import BaseTest
|
||||||
|
|
||||||
from spiffworkflow_backend.services.authentication_service import (
|
from spiffworkflow_backend.services.authentication_service import (
|
||||||
PublicAuthenticationService,
|
AuthenticationService,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ class TestAuthentication(BaseTest):
|
||||||
def test_get_login_state(self) -> None:
|
def test_get_login_state(self) -> None:
|
||||||
"""Test_get_login_state."""
|
"""Test_get_login_state."""
|
||||||
redirect_url = "http://example.com/"
|
redirect_url = "http://example.com/"
|
||||||
state = PublicAuthenticationService.generate_state(redirect_url)
|
state = AuthenticationService.generate_state(redirect_url)
|
||||||
state_dict = ast.literal_eval(base64.b64decode(state).decode("utf-8"))
|
state_dict = ast.literal_eval(base64.b64decode(state).decode("utf-8"))
|
||||||
|
|
||||||
assert isinstance(state_dict, dict)
|
assert isinstance(state_dict, dict)
|
||||||
|
@ -24,9 +24,9 @@ class TestAuthentication(BaseTest):
|
||||||
|
|
||||||
# def test_get_login_redirect_url(self):
|
# def test_get_login_redirect_url(self):
|
||||||
# redirect_url = "http://example.com/"
|
# redirect_url = "http://example.com/"
|
||||||
# state = PublicAuthenticationService.generate_state(redirect_url)
|
# state = AuthenticationService.generate_state(redirect_url)
|
||||||
# with current_app.app_context():
|
# with current_app.app_context():
|
||||||
# login_redirect_url = PublicAuthenticationService().get_login_redirect_url(state.decode("UTF-8"))
|
# login_redirect_url = AuthenticationService().get_login_redirect_url(state.decode("UTF-8"))
|
||||||
# print("test_get_login_redirect_url")
|
# print("test_get_login_redirect_url")
|
||||||
# print("test_get_login_redirect_url")
|
# print("test_get_login_redirect_url")
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ class TestAuthorization(BaseTest):
|
||||||
# """Test_get_bearer_token."""
|
# """Test_get_bearer_token."""
|
||||||
# for user_id in ("user_1", "user_2", "admin_1", "admin_2"):
|
# for user_id in ("user_1", "user_2", "admin_1", "admin_2"):
|
||||||
# public_access_token = self.get_public_access_token(user_id, user_id)
|
# public_access_token = self.get_public_access_token(user_id, user_id)
|
||||||
# bearer_token = PublicAuthenticationService.get_bearer_token(public_access_token)
|
# bearer_token = AuthenticationService.get_bearer_token(public_access_token)
|
||||||
# assert isinstance(public_access_token, str)
|
# assert isinstance(public_access_token, str)
|
||||||
# assert isinstance(bearer_token, dict)
|
# assert isinstance(bearer_token, dict)
|
||||||
# assert "access_token" in bearer_token
|
# assert "access_token" in bearer_token
|
||||||
|
@ -25,7 +25,7 @@ class TestAuthorization(BaseTest):
|
||||||
# """Test_get_user_info_from_public_access_token."""
|
# """Test_get_user_info_from_public_access_token."""
|
||||||
# for user_id in ("user_1", "user_2", "admin_1", "admin_2"):
|
# for user_id in ("user_1", "user_2", "admin_1", "admin_2"):
|
||||||
# public_access_token = self.get_public_access_token(user_id, user_id)
|
# public_access_token = self.get_public_access_token(user_id, user_id)
|
||||||
# user_info = PublicAuthenticationService.get_user_info_from_id_token(
|
# user_info = AuthenticationService.get_user_info_from_id_token(
|
||||||
# public_access_token
|
# public_access_token
|
||||||
# )
|
# )
|
||||||
# assert "sub" in user_info
|
# assert "sub" in user_info
|
||||||
|
@ -46,7 +46,7 @@ class TestAuthorization(BaseTest):
|
||||||
# ) = self.get_keycloak_constants(app)
|
# ) = self.get_keycloak_constants(app)
|
||||||
# for user_id in ("user_1", "user_2", "admin_1", "admin_2"):
|
# for user_id in ("user_1", "user_2", "admin_1", "admin_2"):
|
||||||
# basic_token = self.get_public_access_token(user_id, user_id)
|
# basic_token = self.get_public_access_token(user_id, user_id)
|
||||||
# introspection = PublicAuthenticationService.introspect_token(basic_token)
|
# introspection = AuthenticationService.introspect_token(basic_token)
|
||||||
# assert isinstance(introspection, dict)
|
# assert isinstance(introspection, dict)
|
||||||
# assert introspection["typ"] == "Bearer"
|
# assert introspection["typ"] == "Bearer"
|
||||||
# assert introspection["preferred_username"] == user_id
|
# assert introspection["preferred_username"] == user_id
|
||||||
|
@ -80,7 +80,7 @@ class TestAuthorization(BaseTest):
|
||||||
# for user_id in ("user_1", "user_2", "admin_1", "admin_2"):
|
# for user_id in ("user_1", "user_2", "admin_1", "admin_2"):
|
||||||
# output[user_id] = {}
|
# output[user_id] = {}
|
||||||
# basic_token = self.get_public_access_token(user_id, user_id)
|
# basic_token = self.get_public_access_token(user_id, user_id)
|
||||||
# permissions = PublicAuthenticationService.get_permission_by_basic_token(
|
# permissions = AuthenticationService.get_permission_by_basic_token(
|
||||||
# basic_token
|
# basic_token
|
||||||
# )
|
# )
|
||||||
# if isinstance(permissions, list):
|
# if isinstance(permissions, list):
|
||||||
|
@ -136,7 +136,7 @@ class TestAuthorization(BaseTest):
|
||||||
# for resource in resources:
|
# for resource in resources:
|
||||||
# output[user_id][resource] = {}
|
# output[user_id][resource] = {}
|
||||||
# for scope in "instantiate", "read", "update", "delete":
|
# for scope in "instantiate", "read", "update", "delete":
|
||||||
# auth_status = PublicAuthenticationService.get_auth_status_for_resource_and_scope_by_token(
|
# auth_status = AuthenticationService.get_auth_status_for_resource_and_scope_by_token(
|
||||||
# basic_token, resource, scope
|
# basic_token, resource, scope
|
||||||
# )
|
# )
|
||||||
# output[user_id][resource][scope] = auth_status
|
# output[user_id][resource][scope] = auth_status
|
||||||
|
@ -152,7 +152,7 @@ class TestAuthorization(BaseTest):
|
||||||
# for resource in resource_names:
|
# for resource in resource_names:
|
||||||
# output[user_id][resource] = {}
|
# output[user_id][resource] = {}
|
||||||
# for scope in "instantiate", "read", "update", "delete":
|
# for scope in "instantiate", "read", "update", "delete":
|
||||||
# permissions = PublicAuthenticationService.get_permissions_by_token_for_resource_and_scope(
|
# permissions = AuthenticationService.get_permissions_by_token_for_resource_and_scope(
|
||||||
# basic_token, resource, scope
|
# basic_token, resource, scope
|
||||||
# )
|
# )
|
||||||
# output[user_id][resource][scope] = permissions
|
# output[user_id][resource][scope] = permissions
|
||||||
|
|
|
@ -3,18 +3,23 @@ from flask.app import Flask
|
||||||
from flask.testing import FlaskClient
|
from flask.testing import FlaskClient
|
||||||
from tests.spiffworkflow_backend.helpers.base_test import BaseTest
|
from tests.spiffworkflow_backend.helpers.base_test import BaseTest
|
||||||
|
|
||||||
|
from spiffworkflow_backend.models.user import UserModel
|
||||||
|
|
||||||
|
|
||||||
class TestLoggingService(BaseTest):
|
class TestLoggingService(BaseTest):
|
||||||
"""Test logging service."""
|
"""Test logging service."""
|
||||||
|
|
||||||
def test_logging_service_spiff_logger(
|
def test_logging_service_spiff_logger(
|
||||||
self, app: Flask, client: FlaskClient, with_db_and_bpmn_file_cleanup: None
|
self,
|
||||||
|
app: Flask,
|
||||||
|
client: FlaskClient,
|
||||||
|
with_db_and_bpmn_file_cleanup: None,
|
||||||
|
with_super_admin_user: UserModel,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test_process_instance_run."""
|
"""Test_process_instance_run."""
|
||||||
process_group_id = "test_logging_spiff_logger"
|
process_group_id = "test_logging_spiff_logger"
|
||||||
process_model_id = "simple_script"
|
process_model_id = "simple_script"
|
||||||
user = self.find_or_create_user()
|
headers = self.logged_in_headers(with_super_admin_user)
|
||||||
headers = self.logged_in_headers(user)
|
|
||||||
response = self.create_process_instance(
|
response = self.create_process_instance(
|
||||||
client, process_group_id, process_model_id, headers
|
client, process_group_id, process_model_id, headers
|
||||||
)
|
)
|
||||||
|
@ -22,13 +27,13 @@ class TestLoggingService(BaseTest):
|
||||||
process_instance_id = response.json["id"]
|
process_instance_id = response.json["id"]
|
||||||
response = client.post(
|
response = client.post(
|
||||||
f"/v1.0/process-models/{process_group_id}/{process_model_id}/process-instances/{process_instance_id}/run",
|
f"/v1.0/process-models/{process_group_id}/{process_model_id}/process-instances/{process_instance_id}/run",
|
||||||
headers=self.logged_in_headers(user),
|
headers=headers,
|
||||||
)
|
)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
log_response = client.get(
|
log_response = client.get(
|
||||||
f"/v1.0/process-models/{process_group_id}/{process_model_id}/process-instances/{process_instance_id}/logs",
|
f"/v1.0/process-models/{process_group_id}/{process_model_id}/process-instances/{process_instance_id}/logs",
|
||||||
headers=self.logged_in_headers(user),
|
headers=headers,
|
||||||
)
|
)
|
||||||
assert log_response.status_code == 200
|
assert log_response.status_code == 200
|
||||||
assert log_response.json
|
assert log_response.json
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -48,6 +48,7 @@ class SecretServiceTestHelpers(BaseTest):
|
||||||
process_model_id=self.test_process_model_id,
|
process_model_id=self.test_process_model_id,
|
||||||
process_model_display_name=self.test_process_model_display_name,
|
process_model_display_name=self.test_process_model_display_name,
|
||||||
process_model_description=self.test_process_model_description,
|
process_model_description=self.test_process_model_description,
|
||||||
|
user=user,
|
||||||
)
|
)
|
||||||
process_model_info = ProcessModelService().get_process_model(
|
process_model_info = ProcessModelService().get_process_model(
|
||||||
self.test_process_model_id, self.test_process_group_id
|
self.test_process_model_id, self.test_process_group_id
|
||||||
|
@ -58,118 +59,153 @@ class SecretServiceTestHelpers(BaseTest):
|
||||||
class TestSecretService(SecretServiceTestHelpers):
|
class TestSecretService(SecretServiceTestHelpers):
|
||||||
"""TestSecretService."""
|
"""TestSecretService."""
|
||||||
|
|
||||||
def test_add_secret(self, app: Flask, with_db_and_bpmn_file_cleanup: None) -> None:
|
def test_add_secret(
|
||||||
|
self,
|
||||||
|
app: Flask,
|
||||||
|
with_db_and_bpmn_file_cleanup: None,
|
||||||
|
with_super_admin_user: UserModel,
|
||||||
|
) -> None:
|
||||||
"""Test_add_secret."""
|
"""Test_add_secret."""
|
||||||
user = self.find_or_create_user()
|
test_secret = self.add_test_secret(with_super_admin_user)
|
||||||
test_secret = self.add_test_secret(user)
|
|
||||||
|
|
||||||
assert test_secret is not None
|
assert test_secret is not None
|
||||||
assert test_secret.key == self.test_key
|
assert test_secret.key == self.test_key
|
||||||
assert test_secret.value == self.test_value
|
assert test_secret.value == self.test_value
|
||||||
assert test_secret.creator_user_id == user.id
|
assert test_secret.creator_user_id == with_super_admin_user.id
|
||||||
|
|
||||||
def test_add_secret_duplicate_key_fails(
|
def test_add_secret_duplicate_key_fails(
|
||||||
self, app: Flask, with_db_and_bpmn_file_cleanup: None
|
self,
|
||||||
|
app: Flask,
|
||||||
|
with_db_and_bpmn_file_cleanup: None,
|
||||||
|
with_super_admin_user: UserModel,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test_add_secret_duplicate_key_fails."""
|
"""Test_add_secret_duplicate_key_fails."""
|
||||||
user = self.find_or_create_user()
|
self.add_test_secret(with_super_admin_user)
|
||||||
self.add_test_secret(user)
|
|
||||||
with pytest.raises(ApiError) as ae:
|
with pytest.raises(ApiError) as ae:
|
||||||
self.add_test_secret(user)
|
self.add_test_secret(with_super_admin_user)
|
||||||
assert ae.value.error_code == "create_secret_error"
|
assert ae.value.error_code == "create_secret_error"
|
||||||
|
|
||||||
def test_get_secret(self, app: Flask, with_db_and_bpmn_file_cleanup: None) -> None:
|
def test_get_secret(
|
||||||
|
self,
|
||||||
|
app: Flask,
|
||||||
|
with_db_and_bpmn_file_cleanup: None,
|
||||||
|
with_super_admin_user: UserModel,
|
||||||
|
) -> None:
|
||||||
"""Test_get_secret."""
|
"""Test_get_secret."""
|
||||||
user = self.find_or_create_user()
|
self.add_test_secret(with_super_admin_user)
|
||||||
self.add_test_secret(user)
|
|
||||||
|
|
||||||
secret = SecretService().get_secret(self.test_key)
|
secret = SecretService().get_secret(self.test_key)
|
||||||
assert secret is not None
|
assert secret is not None
|
||||||
assert secret.value == self.test_value
|
assert secret.value == self.test_value
|
||||||
|
|
||||||
def test_get_secret_bad_key_fails(
|
def test_get_secret_bad_key_fails(
|
||||||
self, app: Flask, with_db_and_bpmn_file_cleanup: None
|
self,
|
||||||
|
app: Flask,
|
||||||
|
with_db_and_bpmn_file_cleanup: None,
|
||||||
|
with_super_admin_user: UserModel,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test_get_secret_bad_service."""
|
"""Test_get_secret_bad_service."""
|
||||||
user = self.find_or_create_user()
|
self.add_test_secret(with_super_admin_user)
|
||||||
self.add_test_secret(user)
|
|
||||||
|
|
||||||
with pytest.raises(ApiError):
|
with pytest.raises(ApiError):
|
||||||
SecretService().get_secret("bad_key")
|
SecretService().get_secret("bad_key")
|
||||||
|
|
||||||
def test_update_secret(
|
def test_update_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,
|
||||||
|
with_super_admin_user: UserModel,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test update secret."""
|
"""Test update secret."""
|
||||||
user = self.find_or_create_user()
|
self.add_test_secret(with_super_admin_user)
|
||||||
self.add_test_secret(user)
|
|
||||||
secret = SecretService.get_secret(self.test_key)
|
secret = SecretService.get_secret(self.test_key)
|
||||||
assert secret
|
assert secret
|
||||||
assert secret.value == self.test_value
|
assert secret.value == self.test_value
|
||||||
SecretService.update_secret(self.test_key, "new_secret_value", user.id)
|
SecretService.update_secret(
|
||||||
|
self.test_key, "new_secret_value", with_super_admin_user.id
|
||||||
|
)
|
||||||
new_secret = SecretService.get_secret(self.test_key)
|
new_secret = SecretService.get_secret(self.test_key)
|
||||||
assert new_secret
|
assert new_secret
|
||||||
assert new_secret.value == "new_secret_value" # noqa: S105
|
assert new_secret.value == "new_secret_value" # noqa: S105
|
||||||
|
|
||||||
def test_update_secret_bad_user_fails(
|
def test_update_secret_bad_user_fails(
|
||||||
self, app: Flask, client: FlaskClient, with_db_and_bpmn_file_cleanup: None
|
self,
|
||||||
|
app: Flask,
|
||||||
|
client: FlaskClient,
|
||||||
|
with_db_and_bpmn_file_cleanup: None,
|
||||||
|
with_super_admin_user: UserModel,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test_update_secret_bad_user."""
|
"""Test_update_secret_bad_user."""
|
||||||
user = self.find_or_create_user()
|
self.add_test_secret(with_super_admin_user)
|
||||||
self.add_test_secret(user)
|
|
||||||
with pytest.raises(ApiError) as ae:
|
with pytest.raises(ApiError) as ae:
|
||||||
SecretService.update_secret(
|
SecretService.update_secret(
|
||||||
self.test_key, "new_secret_value", user.id + 1
|
self.test_key, "new_secret_value", with_super_admin_user.id + 1
|
||||||
) # noqa: S105
|
) # noqa: S105
|
||||||
assert (
|
assert (
|
||||||
ae.value.message
|
ae.value.message
|
||||||
== f"User: {user.id+1} cannot update the secret with key : test_key"
|
== f"User: {with_super_admin_user.id+1} cannot update the secret with key : test_key"
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_update_secret_bad_secret_fails(
|
def test_update_secret_bad_secret_fails(
|
||||||
self, app: Flask, client: FlaskClient, with_db_and_bpmn_file_cleanup: None
|
self,
|
||||||
|
app: Flask,
|
||||||
|
client: FlaskClient,
|
||||||
|
with_db_and_bpmn_file_cleanup: None,
|
||||||
|
with_super_admin_user: UserModel,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test_update_secret_bad_secret_fails."""
|
"""Test_update_secret_bad_secret_fails."""
|
||||||
user = self.find_or_create_user()
|
secret = self.add_test_secret(with_super_admin_user)
|
||||||
secret = self.add_test_secret(user)
|
|
||||||
with pytest.raises(ApiError) as ae:
|
with pytest.raises(ApiError) as ae:
|
||||||
SecretService.update_secret(secret.key + "x", "some_new_value", user.id)
|
SecretService.update_secret(
|
||||||
|
secret.key + "x", "some_new_value", with_super_admin_user.id
|
||||||
|
)
|
||||||
assert "Resource does not exist" in ae.value.message
|
assert "Resource does not exist" in ae.value.message
|
||||||
assert ae.value.error_code == "update_secret_error"
|
assert ae.value.error_code == "update_secret_error"
|
||||||
|
|
||||||
def test_delete_secret(
|
def test_delete_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,
|
||||||
|
with_super_admin_user: UserModel,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test delete secret."""
|
"""Test delete secret."""
|
||||||
user = self.find_or_create_user()
|
self.add_test_secret(with_super_admin_user)
|
||||||
self.add_test_secret(user)
|
|
||||||
secrets = SecretModel.query.all()
|
secrets = SecretModel.query.all()
|
||||||
assert len(secrets) == 1
|
assert len(secrets) == 1
|
||||||
assert secrets[0].creator_user_id == user.id
|
assert secrets[0].creator_user_id == with_super_admin_user.id
|
||||||
SecretService.delete_secret(self.test_key, user.id)
|
SecretService.delete_secret(self.test_key, with_super_admin_user.id)
|
||||||
secrets = SecretModel.query.all()
|
secrets = SecretModel.query.all()
|
||||||
assert len(secrets) == 0
|
assert len(secrets) == 0
|
||||||
|
|
||||||
def test_delete_secret_bad_user_fails(
|
def test_delete_secret_bad_user_fails(
|
||||||
self, app: Flask, client: FlaskClient, with_db_and_bpmn_file_cleanup: None
|
self,
|
||||||
|
app: Flask,
|
||||||
|
client: FlaskClient,
|
||||||
|
with_db_and_bpmn_file_cleanup: None,
|
||||||
|
with_super_admin_user: UserModel,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test_delete_secret_bad_user."""
|
"""Test_delete_secret_bad_user."""
|
||||||
user = self.find_or_create_user()
|
self.add_test_secret(with_super_admin_user)
|
||||||
self.add_test_secret(user)
|
|
||||||
with pytest.raises(ApiError) as ae:
|
with pytest.raises(ApiError) as ae:
|
||||||
SecretService.delete_secret(self.test_key, user.id + 1)
|
SecretService.delete_secret(self.test_key, with_super_admin_user.id + 1)
|
||||||
assert (
|
assert (
|
||||||
f"User: {user.id+1} cannot delete the secret with key" in ae.value.message
|
f"User: {with_super_admin_user.id+1} cannot delete the secret with key"
|
||||||
|
in ae.value.message
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_delete_secret_bad_secret_fails(
|
def test_delete_secret_bad_secret_fails(
|
||||||
self, app: Flask, client: FlaskClient, with_db_and_bpmn_file_cleanup: None
|
self,
|
||||||
|
app: Flask,
|
||||||
|
client: FlaskClient,
|
||||||
|
with_db_and_bpmn_file_cleanup: None,
|
||||||
|
with_super_admin_user: UserModel,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test_delete_secret_bad_secret_fails."""
|
"""Test_delete_secret_bad_secret_fails."""
|
||||||
user = self.find_or_create_user()
|
self.add_test_secret(with_super_admin_user)
|
||||||
self.add_test_secret(user)
|
|
||||||
with pytest.raises(ApiError) as ae:
|
with pytest.raises(ApiError) as ae:
|
||||||
SecretService.delete_secret(self.test_key + "x", user.id)
|
SecretService.delete_secret(self.test_key + "x", with_super_admin_user.id)
|
||||||
assert "Resource does not exist" in ae.value.message
|
assert "Resource does not exist" in ae.value.message
|
||||||
|
|
||||||
|
|
||||||
|
@ -177,19 +213,22 @@ class TestSecretServiceApi(SecretServiceTestHelpers):
|
||||||
"""TestSecretServiceApi."""
|
"""TestSecretServiceApi."""
|
||||||
|
|
||||||
def test_add_secret(
|
def test_add_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,
|
||||||
|
with_super_admin_user: UserModel,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test_add_secret."""
|
"""Test_add_secret."""
|
||||||
user = self.find_or_create_user()
|
|
||||||
secret_model = SecretModel(
|
secret_model = SecretModel(
|
||||||
key=self.test_key,
|
key=self.test_key,
|
||||||
value=self.test_value,
|
value=self.test_value,
|
||||||
creator_user_id=user.id,
|
creator_user_id=with_super_admin_user.id,
|
||||||
)
|
)
|
||||||
data = json.dumps(SecretModelSchema().dump(secret_model))
|
data = json.dumps(SecretModelSchema().dump(secret_model))
|
||||||
response: TestResponse = client.post(
|
response: TestResponse = client.post(
|
||||||
"/v1.0/secrets",
|
"/v1.0/secrets",
|
||||||
headers=self.logged_in_headers(user),
|
headers=self.logged_in_headers(with_super_admin_user),
|
||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
data=data,
|
data=data,
|
||||||
)
|
)
|
||||||
|
@ -199,17 +238,20 @@ class TestSecretServiceApi(SecretServiceTestHelpers):
|
||||||
assert key in secret.keys()
|
assert key in secret.keys()
|
||||||
assert secret["key"] == self.test_key
|
assert secret["key"] == self.test_key
|
||||||
assert secret["value"] == self.test_value
|
assert secret["value"] == self.test_value
|
||||||
assert secret["creator_user_id"] == user.id
|
assert secret["creator_user_id"] == with_super_admin_user.id
|
||||||
|
|
||||||
def test_get_secret(
|
def test_get_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,
|
||||||
|
with_super_admin_user: UserModel,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test get secret."""
|
"""Test get secret."""
|
||||||
user = self.find_or_create_user()
|
self.add_test_secret(with_super_admin_user)
|
||||||
self.add_test_secret(user)
|
|
||||||
secret_response = client.get(
|
secret_response = client.get(
|
||||||
f"/v1.0/secrets/{self.test_key}",
|
f"/v1.0/secrets/{self.test_key}",
|
||||||
headers=self.logged_in_headers(user),
|
headers=self.logged_in_headers(with_super_admin_user),
|
||||||
)
|
)
|
||||||
assert secret_response
|
assert secret_response
|
||||||
assert secret_response.status_code == 200
|
assert secret_response.status_code == 200
|
||||||
|
@ -217,20 +259,25 @@ class TestSecretServiceApi(SecretServiceTestHelpers):
|
||||||
assert secret_response.json["value"] == self.test_value
|
assert secret_response.json["value"] == self.test_value
|
||||||
|
|
||||||
def test_update_secret(
|
def test_update_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,
|
||||||
|
with_super_admin_user: UserModel,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test_update_secret."""
|
"""Test_update_secret."""
|
||||||
user = self.find_or_create_user()
|
self.add_test_secret(with_super_admin_user)
|
||||||
self.add_test_secret(user)
|
|
||||||
secret: Optional[SecretModel] = SecretService.get_secret(self.test_key)
|
secret: Optional[SecretModel] = SecretService.get_secret(self.test_key)
|
||||||
assert secret
|
assert secret
|
||||||
assert secret.value == self.test_value
|
assert secret.value == self.test_value
|
||||||
secret_model = SecretModel(
|
secret_model = SecretModel(
|
||||||
key=self.test_key, value="new_secret_value", creator_user_id=user.id
|
key=self.test_key,
|
||||||
|
value="new_secret_value",
|
||||||
|
creator_user_id=with_super_admin_user.id,
|
||||||
)
|
)
|
||||||
response = client.put(
|
response = client.put(
|
||||||
f"/v1.0/secrets/{self.test_key}",
|
f"/v1.0/secrets/{self.test_key}",
|
||||||
headers=self.logged_in_headers(user),
|
headers=self.logged_in_headers(with_super_admin_user),
|
||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
data=json.dumps(SecretModelSchema().dump(secret_model)),
|
data=json.dumps(SecretModelSchema().dump(secret_model)),
|
||||||
)
|
)
|
||||||
|
@ -242,42 +289,61 @@ class TestSecretServiceApi(SecretServiceTestHelpers):
|
||||||
assert secret_model.value == "new_secret_value"
|
assert secret_model.value == "new_secret_value"
|
||||||
|
|
||||||
def test_delete_secret(
|
def test_delete_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,
|
||||||
|
with_super_admin_user: UserModel,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test delete secret."""
|
"""Test delete secret."""
|
||||||
user = self.find_or_create_user()
|
self.add_test_secret(with_super_admin_user)
|
||||||
self.add_test_secret(user)
|
|
||||||
secret = SecretService.get_secret(self.test_key)
|
secret = SecretService.get_secret(self.test_key)
|
||||||
assert secret
|
assert secret
|
||||||
assert secret.value == self.test_value
|
assert secret.value == self.test_value
|
||||||
secret_response = client.delete(
|
secret_response = client.delete(
|
||||||
f"/v1.0/secrets/{self.test_key}",
|
f"/v1.0/secrets/{self.test_key}",
|
||||||
headers=self.logged_in_headers(user),
|
headers=self.logged_in_headers(with_super_admin_user),
|
||||||
)
|
)
|
||||||
assert secret_response.status_code == 200
|
assert secret_response.status_code == 200
|
||||||
with pytest.raises(ApiError):
|
with pytest.raises(ApiError):
|
||||||
secret = SecretService.get_secret(self.test_key)
|
secret = SecretService.get_secret(self.test_key)
|
||||||
|
|
||||||
def test_delete_secret_bad_user(
|
def test_delete_secret_bad_user(
|
||||||
self, app: Flask, client: FlaskClient, with_db_and_bpmn_file_cleanup: None
|
self,
|
||||||
|
app: Flask,
|
||||||
|
client: FlaskClient,
|
||||||
|
with_db_and_bpmn_file_cleanup: None,
|
||||||
|
with_super_admin_user: UserModel,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test_delete_secret_bad_user."""
|
"""Test_delete_secret_bad_user."""
|
||||||
user_1 = self.find_or_create_user()
|
user_1 = self.find_or_create_user()
|
||||||
user_2 = self.find_or_create_user("test_user_2")
|
user_2 = self.find_or_create_user("test_user_2")
|
||||||
self.add_test_secret(user_1)
|
self.add_test_secret(user_1)
|
||||||
|
|
||||||
|
# ensure user has permissions to delete the given secret
|
||||||
|
self.add_permissions_to_user(
|
||||||
|
user_2,
|
||||||
|
target_uri=f"/v1.0/secrets/{self.test_key}",
|
||||||
|
permission_names=["delete"],
|
||||||
|
)
|
||||||
secret_response = client.delete(
|
secret_response = client.delete(
|
||||||
f"/v1.0/secrets/{self.test_key}",
|
f"/v1.0/secrets/{self.test_key}",
|
||||||
headers=self.logged_in_headers(user_2),
|
headers=self.logged_in_headers(user_2),
|
||||||
)
|
)
|
||||||
assert secret_response.status_code == 401
|
assert secret_response.status_code == 401
|
||||||
|
assert secret_response.json
|
||||||
|
assert secret_response.json["error_code"] == "delete_secret_error"
|
||||||
|
|
||||||
def test_delete_secret_bad_key(
|
def test_delete_secret_bad_key(
|
||||||
self, app: Flask, client: FlaskClient, with_db_and_bpmn_file_cleanup: None
|
self,
|
||||||
|
app: Flask,
|
||||||
|
client: FlaskClient,
|
||||||
|
with_db_and_bpmn_file_cleanup: None,
|
||||||
|
with_super_admin_user: UserModel,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test delete secret."""
|
"""Test delete secret."""
|
||||||
user = self.find_or_create_user()
|
|
||||||
secret_response = client.delete(
|
secret_response = client.delete(
|
||||||
"/v1.0/secrets/bad_secret_key",
|
"/v1.0/secrets/bad_secret_key",
|
||||||
headers=self.logged_in_headers(user),
|
headers=self.logged_in_headers(with_super_admin_user),
|
||||||
)
|
)
|
||||||
assert secret_response.status_code == 404
|
assert secret_response.status_code == 404
|
||||||
|
|
|
@ -13,10 +13,10 @@ from spiffworkflow_backend.models.permission_target import PermissionTargetModel
|
||||||
class TestPermissionTarget(BaseTest):
|
class TestPermissionTarget(BaseTest):
|
||||||
"""TestPermissionTarget."""
|
"""TestPermissionTarget."""
|
||||||
|
|
||||||
def test_asterisk_must_go_at_the_end_of_uri(
|
def test_wildcard_must_go_at_the_end_of_uri(
|
||||||
self, app: Flask, with_db_and_bpmn_file_cleanup: None
|
self, app: Flask, with_db_and_bpmn_file_cleanup: None
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test_asterisk_must_go_at_the_end_of_uri."""
|
"""Test_wildcard_must_go_at_the_end_of_uri."""
|
||||||
permission_target = PermissionTargetModel(uri="/test_group/%")
|
permission_target = PermissionTargetModel(uri="/test_group/%")
|
||||||
db.session.add(permission_target)
|
db.session.add(permission_target)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
@ -30,3 +30,13 @@ class TestPermissionTarget(BaseTest):
|
||||||
assert (
|
assert (
|
||||||
str(exception.value) == "Wildcard must appear at end: /test_group/%/model"
|
str(exception.value) == "Wildcard must appear at end: /test_group/%/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_can_change_asterisk_to_percent_on_creation(
|
||||||
|
self, app: Flask, with_db_and_bpmn_file_cleanup: None
|
||||||
|
) -> None:
|
||||||
|
"""Test_can_change_asterisk_to_percent_on_creation."""
|
||||||
|
permission_target = PermissionTargetModel(uri="/test_group/*")
|
||||||
|
db.session.add(permission_target)
|
||||||
|
db.session.commit()
|
||||||
|
assert isinstance(permission_target.id, int)
|
||||||
|
assert permission_target.uri == "/test_group/%"
|
||||||
|
|
Loading…
Reference in New Issue