From 4a48d9cccd1ca8619b3dbef3c10bcce667c9d9e0 Mon Sep 17 00:00:00 2001 From: burnettk Date: Thu, 20 Oct 2022 16:00:12 -0400 Subject: [PATCH] Squashed 'spiffworkflow-backend/' changes from f9c2fa21e..5225a8b4c 5225a8b4c pyl 259f74a1e Merge branch 'main' into bug/refresh-token d452208ef Merge pull request #135 from sartography/feature/permissions3 8e1075406 Merge branch 'main' into bug/refresh-token 2b01d2fe7 fixed authentication_callback and getting the user w/ burnettk 476e36c7d mypy changes 6403e62c0 Fix migration after merging main 594a32b67 merged in main and resolved conflicts w/ burnettk b285ba1a1 added updated columns to secrets and updated flask-bpmn 7c53fc9fa Merge remote-tracking branch 'origin/main' into feature/permissions3 201a6918a pyl changes a6112f7fb Merge branch 'main' into bug/refresh-token 87f65a6c6 auth_token should be dictionary, not string f163de61c pyl 1f443bb94 PublicAuthenticationService -> AuthenticationService 6c491a3df Don't refresh token here. They just logged in. We are validating the returned token. If it is bad, raise an error. 91b8649f8 id_token -> auth_token fc94774bb Move `store_refresh_token` to authentication_service 00d66e9c5 mypy c4e415dbe mypy 1e75716eb Pre commit a72b03e09 Rename method. We pass it auth_tokens, not id_tokens 9a6700a6d Too many things expect g.token. Reverting my change 74883fb23 Noe store refresh_token, and try to use it if auth_token is expired Renamed some methods to use correct token type be0557013 Cleanup - remove unused code cf01f0d51 Add refresh_token model 1c0c937af added method to delete all permissions so we can recreate them w/ burnettk aaeaac879 Merge remote-tracking branch 'origin/main' into feature/permissions3 44856fce2 added api endpoint to check if user has permissions based on given target uris w/ burnettk ae830054d precommit w/ burnettk 94d50efb1 created common method to check whether an api method should have auth w/ burnettk c955335d0 precommit w/ burnettk 37caf1a69 added a finance user to keycloak and fixed up the staging permission yml w/ burnettk 93c456294 merged in main and resolved conflicts w/ burnettk 06a7c6485 remaining tests are now passing w/ burnettk 50529d04c added test to make sure api gives a 403 if a permission is not found w/ burnettk 6a9d0a68a api calls are somewhat respecting permissions now and the process api tests are passing d07fbbeff attempting to respect permissions w/ burnettk git-subtree-dir: spiffworkflow-backend git-subtree-split: 5225a8b4c101133567d4f7efa33632d36c29c81d --- bin/delete_and_import_all_permissions.py | 14 + bin/spiffworkflow-realm.json | 47 +- conftest.py | 8 + .../{9e14b40371f3_.py => e6b28d8e3178_.py} | 21 +- poetry.lock | 44 +- src/spiffworkflow_backend/__init__.py | 5 + src/spiffworkflow_backend/api.yml | 28 +- .../config/permissions/staging.yml | 26 +- src/spiffworkflow_backend/config/testing.py | 4 + .../load_database_models.py | 1 + src/spiffworkflow_backend/models/group.py | 2 +- .../models/permission_assignment.py | 12 +- .../models/permission_target.py | 16 +- .../models/refresh_token.py | 22 + .../models/secret_model.py | 2 + .../routes/process_api_blueprint.py | 39 +- src/spiffworkflow_backend/routes/user.py | 146 ++--- .../services/authentication_service.py | 99 ++- .../services/authorization_service.py | 112 +++- .../services/process_instance_processor.py | 3 - .../services/user_service.py | 14 + .../helpers/base_test.py | 53 +- .../integration/test_authentication.py | 8 +- .../integration/test_authorization.py | 12 +- .../integration/test_logging_service.py | 15 +- .../integration/test_process_api.py | 591 ++++++++++++------ .../integration/test_secret_service.py | 192 ++++-- .../unit/test_permission_target.py | 14 +- 28 files changed, 1095 insertions(+), 455 deletions(-) create mode 100644 bin/delete_and_import_all_permissions.py rename migrations/versions/{9e14b40371f3_.py => e6b28d8e3178_.py} (96%) create mode 100644 src/spiffworkflow_backend/models/refresh_token.py diff --git a/bin/delete_and_import_all_permissions.py b/bin/delete_and_import_all_permissions.py new file mode 100644 index 000000000..a55e36e7f --- /dev/null +++ b/bin/delete_and_import_all_permissions.py @@ -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() diff --git a/bin/spiffworkflow-realm.json b/bin/spiffworkflow-realm.json index 52a346015..b7fb049b1 100644 --- a/bin/spiffworkflow-realm.json +++ b/bin/spiffworkflow-realm.json @@ -630,6 +630,28 @@ "notBefore": 0, "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", "createdTimestamp": 1665516884829, @@ -828,31 +850,6 @@ "notBefore": 0, "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", "createdTimestamp": 1665516926300, diff --git a/conftest.py b/conftest.py index 98846ca19..242f75a0f 100644 --- a/conftest.py +++ b/conftest.py @@ -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 spiffworkflow_backend.models.process_instance import ProcessInstanceModel +from spiffworkflow_backend.models.user import UserModel from spiffworkflow_backend.services.process_instance_processor import ( ProcessInstanceProcessor, ) @@ -57,6 +58,7 @@ def with_db_and_bpmn_file_cleanup() -> None: """Process_group_resource.""" for model in SpiffworkflowBaseDBModel._all_subclasses(): db.session.query(model).delete() + db.session.commit() try: yield @@ -66,6 +68,12 @@ def with_db_and_bpmn_file_cleanup() -> None: 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() def setup_process_instances_for_reports() -> list[ProcessInstanceModel]: """Setup_process_instances_for_reports.""" diff --git a/migrations/versions/9e14b40371f3_.py b/migrations/versions/e6b28d8e3178_.py similarity index 96% rename from migrations/versions/9e14b40371f3_.py rename to migrations/versions/e6b28d8e3178_.py index 69e6631d7..ee75ee4a1 100644 --- a/migrations/versions/9e14b40371f3_.py +++ b/migrations/versions/e6b28d8e3178_.py @@ -1,8 +1,8 @@ """empty message -Revision ID: 9e14b40371f3 +Revision ID: e6b28d8e3178 Revises: -Create Date: 2022-10-19 19:31:20.431800 +Create Date: 2022-10-20 13:05:25.896486 """ from alembic import op @@ -10,7 +10,7 @@ import sqlalchemy as sa # revision identifiers, used by Alembic. -revision = '9e14b40371f3' +revision = 'e6b28d8e3178' down_revision = None branch_labels = 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_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_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', sa.Column('id', sa.Integer(), nullable=False), sa.Column('key', sa.String(length=50), nullable=False), sa.Column('value', sa.Text(), 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.PrimaryKeyConstraint('id'), sa.UniqueConstraint('key') @@ -226,8 +236,8 @@ def upgrade(): sa.Column('id', sa.Integer(), nullable=False), sa.Column('principal_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('permission', sa.String(length=50), nullable=True), + sa.Column('grant_type', sa.String(length=50), nullable=False), + sa.Column('permission', sa.String(length=50), nullable=False), sa.ForeignKeyConstraint(['permission_target_id'], ['permission_target.id'], ), sa.ForeignKeyConstraint(['principal_id'], ['principal.id'], ), sa.PrimaryKeyConstraint('id'), @@ -316,6 +326,7 @@ def downgrade(): op.drop_table('active_task') op.drop_table('user_group_assignment') 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_group_identifier'), table_name='process_instance_report') op.drop_index(op.f('ix_process_instance_report_identifier'), table_name='process_instance_report') diff --git a/poetry.lock b/poetry.lock index 0c85c6764..f35e96631 100644 --- a/poetry.lock +++ b/poetry.lock @@ -95,7 +95,7 @@ python-versions = ">=3.5" dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"] docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"] tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"] -tests_no_zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"] +tests-no-zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"] [[package]] name = "Babel" @@ -268,7 +268,7 @@ optional = false python-versions = ">=3.6.0" [package.extras] -unicode_backport = ["unicodedata2"] +unicode-backport = ["unicodedata2"] [[package]] name = "classify-imports" @@ -639,7 +639,7 @@ werkzeug = "*" type = "git" url = "https://github.com/sartography/flask-bpmn" reference = "main" -resolved_reference = "bd4b45a842ed63a29e74ff02ea7f2a56d7b2298a" +resolved_reference = "c8fd01df47518749a074772fec383256c482139f" [[package]] name = "Flask-Cors" @@ -1512,7 +1512,7 @@ urllib3 = ">=1.21.1,<1.27" [package.extras] socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "requests-toolbelt" @@ -1625,7 +1625,7 @@ falcon = ["falcon (>=1.4)"] fastapi = ["fastapi (>=0.79.0)"] flask = ["blinker (>=1.1)", "flask (>=0.11)"] httpx = ["httpx (>=0.16.0)"] -pure_eval = ["asttokens", "executing", "pure-eval"] +pure-eval = ["asttokens", "executing", "pure-eval"] pyspark = ["pyspark (>=2.4.4)"] quart = ["blinker (>=1.1)", "quart (>=0.16.1)"] rq = ["rq (>=0.6)"] @@ -1847,7 +1847,7 @@ test = ["pytest"] [[package]] name = "SpiffWorkflow" -version = "1.2.0" +version = "1.2.1" description = "A workflow framework and BPMN/DMN Processor" category = "main" optional = false @@ -1858,7 +1858,6 @@ develop = false celery = "*" configparser = "*" dateparser = "*" -importlib-metadata = "<5.0" lxml = "*" pytz = "*" @@ -1884,19 +1883,19 @@ aiomysql = ["aiomysql", "greenlet (!=0.4.17)"] aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing_extensions (!=3.10.0.1)"] asyncio = ["greenlet (!=0.4.17)"] asyncmy = ["asyncmy (>=0.2.3,!=0.2.4)", "greenlet (!=0.4.17)"] -mariadb_connector = ["mariadb (>=1.0.1,!=1.1.2)"] +mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2)"] mssql = ["pyodbc"] -mssql_pymssql = ["pymssql"] -mssql_pyodbc = ["pyodbc"] +mssql-pymssql = ["pymssql"] +mssql-pyodbc = ["pyodbc"] mypy = ["mypy (>=0.910)", "sqlalchemy2-stubs"] mysql = ["mysqlclient (>=1.4.0)", "mysqlclient (>=1.4.0,<2)"] -mysql_connector = ["mysql-connector-python"] +mysql-connector = ["mysql-connector-python"] oracle = ["cx_oracle (>=7)", "cx_oracle (>=7,<8)"] postgresql = ["psycopg2 (>=2.7)"] -postgresql_asyncpg = ["asyncpg", "greenlet (!=0.4.17)"] -postgresql_pg8000 = ["pg8000 (>=1.16.6,!=1.29.0)"] -postgresql_psycopg2binary = ["psycopg2-binary"] -postgresql_psycopg2cffi = ["psycopg2cffi"] +postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"] +postgresql-pg8000 = ["pg8000 (>=1.16.6,!=1.29.0)"] +postgresql-psycopg2binary = ["psycopg2-binary"] +postgresql-psycopg2cffi = ["psycopg2cffi"] pymysql = ["pymysql", "pymysql (<1)"] sqlcipher = ["sqlcipher3_binary"] @@ -2030,7 +2029,7 @@ python-versions = "*" name = "types-PyYAML" version = "6.0.12" description = "Typing stubs for PyYAML" -category = "main" +category = "dev" optional = false python-versions = "*" @@ -2234,7 +2233,7 @@ testing = ["func-timeout", "jaraco.itertools", "pytest (>=6)", "pytest-black (>= [metadata] lock-version = "1.1" python-versions = ">=3.9,<3.11" -content-hash = "2602fd47f14d1163b2590ab01d3adb1ce881c699bb09630e6fdfc56b919a7a4e" +content-hash = "cff4bcfd10157833f1a0f0bb806c3543267c3e99cc13f311b328d101c30ac553" [metadata.files] alabaster = [ @@ -3013,18 +3012,7 @@ py = [ {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, ] pyasn1 = [ - {file = "pyasn1-0.4.8-py2.4.egg", hash = "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"}, - {file = "pyasn1-0.4.8-py2.5.egg", hash = "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf"}, - {file = "pyasn1-0.4.8-py2.6.egg", hash = "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00"}, - {file = "pyasn1-0.4.8-py2.7.egg", hash = "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8"}, {file = "pyasn1-0.4.8-py2.py3-none-any.whl", hash = "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d"}, - {file = "pyasn1-0.4.8-py3.1.egg", hash = "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86"}, - {file = "pyasn1-0.4.8-py3.2.egg", hash = "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7"}, - {file = "pyasn1-0.4.8-py3.3.egg", hash = "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576"}, - {file = "pyasn1-0.4.8-py3.4.egg", hash = "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12"}, - {file = "pyasn1-0.4.8-py3.5.egg", hash = "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2"}, - {file = "pyasn1-0.4.8-py3.6.egg", hash = "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359"}, - {file = "pyasn1-0.4.8-py3.7.egg", hash = "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776"}, {file = "pyasn1-0.4.8.tar.gz", hash = "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"}, ] pycodestyle = [ diff --git a/src/spiffworkflow_backend/__init__.py b/src/spiffworkflow_backend/__init__.py index fa00e10e8..1358c06a0 100644 --- a/src/spiffworkflow_backend/__init__.py +++ b/src/spiffworkflow_backend/__init__.py @@ -19,7 +19,9 @@ import spiffworkflow_backend.load_database_models # noqa: F401 from spiffworkflow_backend.config import setup_config from spiffworkflow_backend.routes.admin_blueprint.admin_blueprint import admin_blueprint from spiffworkflow_backend.routes.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.services.authorization_service import AuthorizationService from spiffworkflow_backend.services.background_processing_service import ( BackgroundProcessingService, ) @@ -114,6 +116,9 @@ def create_app() -> flask.app.Flask: configure_sentry(app) + app.before_request(verify_token) + app.before_request(AuthorizationService.check_for_permission) + return app # type: ignore diff --git a/src/spiffworkflow_backend/api.yml b/src/spiffworkflow_backend/api.yml index 4059283ba..489b00fcf 100755 --- a/src/spiffworkflow_backend/api.yml +++ b/src/spiffworkflow_backend/api.yml @@ -1,13 +1,14 @@ openapi: "3.0.2" info: version: 1.0.0 - title: Workflow Microservice + title: spiffworkflow-backend license: name: MIT servers: - url: http://localhost:5000/v1.0 -security: - - jwt: ["secret"] +# this is handled in flask now +security: [] +# - jwt: ["secret"] # - oAuth2AuthCode: # - read_email # - uid @@ -378,7 +379,6 @@ paths: application/json: schema: $ref: "#/components/schemas/OkTrue" - # process model update put: operationId: spiffworkflow_backend.routes.process_api_blueprint.process_model_update summary: Modifies an existing process mosel with the given parameters. @@ -827,7 +827,6 @@ paths: application/json: schema: $ref: "#/components/schemas/File" - # process_model_file_update put: operationId: spiffworkflow_backend.routes.process_api_blueprint.process_model_file_update summary: save the contents to the given file @@ -1250,6 +1249,25 @@ paths: "404": 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: securitySchemes: jwt: diff --git a/src/spiffworkflow_backend/config/permissions/staging.yml b/src/spiffworkflow_backend/config/permissions/staging.yml index 283a4cb91..ed71a5038 100644 --- a/src/spiffworkflow_backend/config/permissions/staging.yml +++ b/src/spiffworkflow_backend/config/permissions/staging.yml @@ -1,13 +1,25 @@ groups: admin: 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: - users: [harmeet, sasha] - - hr: - users: [manuchehr] + users: [finance_user1] permissions: admin: @@ -20,10 +32,10 @@ permissions: groups: [finance] users: [] allowed_permissions: [create, read, update, delete] - uri: /v1.0/process-groups/finance/* + uri: /v1.0/process-groups/execute-procure-to-pay/* read-all: - groups: [finance, hr, admin] + groups: [finance, admin] users: [] allowed_permissions: [read] uri: /* diff --git a/src/spiffworkflow_backend/config/testing.py b/src/spiffworkflow_backend/config/testing.py index 9501cafcb..30776eaed 100644 --- a/src/spiffworkflow_backend/config/testing.py +++ b/src/spiffworkflow_backend/config/testing.py @@ -11,3 +11,7 @@ SPIFFWORKFLOW_BACKEND_LOG_TO_FILE = ( SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME = environ.get( "SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME", default="testing.yml" ) + +SPIFFWORKFLOW_BACKEND_LOG_LEVEL = environ.get( + "SPIFFWORKFLOW_BACKEND_LOG_LEVEL", default="debug" +) diff --git a/src/spiffworkflow_backend/load_database_models.py b/src/spiffworkflow_backend/load_database_models.py index 33d32c1ad..c064613af 100644 --- a/src/spiffworkflow_backend/load_database_models.py +++ b/src/spiffworkflow_backend/load_database_models.py @@ -45,6 +45,7 @@ from spiffworkflow_backend.models.process_instance import ( from spiffworkflow_backend.models.process_instance_report import ( ProcessInstanceReportModel, ) # 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.spiff_logging import SpiffLoggingModel # noqa: F401 from spiffworkflow_backend.models.task_event import TaskEventModel # noqa: F401 diff --git a/src/spiffworkflow_backend/models/group.py b/src/spiffworkflow_backend/models/group.py index 8287c40ca..b8928d733 100644 --- a/src/spiffworkflow_backend/models/group.py +++ b/src/spiffworkflow_backend/models/group.py @@ -29,4 +29,4 @@ class GroupModel(FlaskBpmnGroupModel): secondary="user_group_assignment", overlaps="user_group_assignments,users", ) - principal = relationship("PrincipalModel", uselist=False) # type: ignore + principal = relationship("PrincipalModel", uselist=False, cascade="all, delete") # type: ignore diff --git a/src/spiffworkflow_backend/models/permission_assignment.py b/src/spiffworkflow_backend/models/permission_assignment.py index 5fc7ae31d..63295f74e 100644 --- a/src/spiffworkflow_backend/models/permission_assignment.py +++ b/src/spiffworkflow_backend/models/permission_assignment.py @@ -31,7 +31,13 @@ class Permission(enum.Enum): read = "read" update = "update" delete = "delete" + + # maybe read to GET process_model/process-instances instead? 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 @@ -50,10 +56,10 @@ class PermissionAssignmentModel(SpiffworkflowBaseDBModel): id = db.Column(db.Integer, primary_key=True) principal_id = db.Column(ForeignKey(PrincipalModel.id), nullable=False) permission_target_id = db.Column( - ForeignKey(PermissionTargetModel.id), nullable=False + ForeignKey(PermissionTargetModel.id), nullable=False # type: ignore ) - grant_type = db.Column(db.String(50)) - permission = db.Column(db.String(50)) + grant_type = db.Column(db.String(50), nullable=False) + permission = db.Column(db.String(50), nullable=False) @validates("grant_type") def validate_grant_type(self, key: str, value: str) -> Any: diff --git a/src/spiffworkflow_backend/models/permission_target.py b/src/spiffworkflow_backend/models/permission_target.py index 3a341c282..53334baf0 100644 --- a/src/spiffworkflow_backend/models/permission_target.py +++ b/src/spiffworkflow_backend/models/permission_target.py @@ -1,5 +1,7 @@ """PermissionTarget.""" import re +from dataclasses import dataclass +from typing import Optional from flask_bpmn.models.db import db from flask_bpmn.models.db import SpiffworkflowBaseDBModel @@ -10,13 +12,23 @@ class InvalidPermissionTargetUriError(Exception): """InvalidPermissionTargetUriError.""" +@dataclass class PermissionTargetModel(SpiffworkflowBaseDBModel): """PermissionTargetModel.""" + URI_ALL = "/%" + __tablename__ = "permission_target" - id = db.Column(db.Integer, primary_key=True) - uri = db.Column(db.String(255), unique=True, nullable=False) + id: int = db.Column(db.Integer, primary_key=True) + 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") def validate_uri(self, key: str, value: str) -> str: diff --git a/src/spiffworkflow_backend/models/refresh_token.py b/src/spiffworkflow_backend/models/refresh_token.py new file mode 100644 index 000000000..2e96b7f05 --- /dev/null +++ b/src/spiffworkflow_backend/models/refresh_token.py @@ -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") diff --git a/src/spiffworkflow_backend/models/secret_model.py b/src/spiffworkflow_backend/models/secret_model.py index 0d650ab96..fed25b1a8 100644 --- a/src/spiffworkflow_backend/models/secret_model.py +++ b/src/spiffworkflow_backend/models/secret_model.py @@ -18,6 +18,8 @@ class SecretModel(SpiffworkflowBaseDBModel): key: str = db.Column(db.String(50), unique=True, nullable=False) value: str = db.Column(db.Text(), 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): diff --git a/src/spiffworkflow_backend/routes/process_api_blueprint.py b/src/spiffworkflow_backend/routes/process_api_blueprint.py index 3b5a9ea2d..aa9152f7f 100644 --- a/src/spiffworkflow_backend/routes/process_api_blueprint.py +++ b/src/spiffworkflow_backend/routes/process_api_blueprint.py @@ -55,6 +55,7 @@ from spiffworkflow_backend.models.secret_model import SecretModelSchema from spiffworkflow_backend.models.spiff_logging import SpiffLoggingModel from spiffworkflow_backend.models.user import UserModel 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.file_system_service import FileSystemService 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") +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( body: Dict[str, Union[str, bool, int]] ) -> flask.wrappers.Response: @@ -794,9 +828,8 @@ def authentication_callback( auth_method: str, ) -> werkzeug.wrappers.Response: """Authentication_callback.""" - verify_token(request.args.get("token")) + verify_token(request.args.get("token"), force_run=True) response = request.args["response"] - print(f"response: {response}") SecretService().update_secret( 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") +# 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: """Task_list_my_tasks.""" principal = find_principal_or_raise() diff --git a/src/spiffworkflow_backend/routes/user.py b/src/spiffworkflow_backend/routes/user.py index 0fa0a65ec..60e1814b1 100644 --- a/src/spiffworkflow_backend/routes/user.py +++ b/src/spiffworkflow_backend/routes/user.py @@ -10,12 +10,13 @@ import jwt from flask import current_app from flask import g from flask import redirect +from flask import request from flask_bpmn.api.api_error import ApiError from werkzeug.wrappers import Response from spiffworkflow_backend.models.user import UserModel from spiffworkflow_backend.services.authentication_service import ( - PublicAuthenticationService, + AuthenticationService, ) from spiffworkflow_backend.services.authorization_service import AuthorizationService 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). If in production environment and token is not provided, gets user from the SSO headers and returns their token. Args: token: Optional[str] + force_run: Optional[bool] Returns: 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. 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: user_model = None decoded_token = get_decoded_token(token) @@ -59,11 +70,35 @@ def verify_token(token: Optional[str] = None) -> Dict[str, Optional[Union[str, i elif "iss" in decoded_token.keys(): try: - user_info = PublicAuthenticationService.get_user_info_from_id_token( - token - ) + user_info = AuthenticationService.get_user_info_from_open_id(token) except ApiError as ae: - raise 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 except Exception as e: current_app.logger.error(f"Exception raised in get_token: {e}") raise ApiError( @@ -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 g.user: + # This is an id token, so we don't have a refresh token yet g.token = token - scope = get_scope(token) - return {"uid": g.user.id, "sub": g.user.id, "scope": scope} + get_scope(token) + return None + # return {"uid": g.user.id, "sub": g.user.id, "scope": scope} # return validate_scope(token, user_info, user_model) else: 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( 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: """Validate_scope.""" print("validate_scope") - # token = PublicAuthenticationService.refresh_token(token) - # user_info = PublicAuthenticationService.get_user_info_from_public_access_token(token) - # bearer_token = PublicAuthenticationService.get_bearer_token(token) - # permission = PublicAuthenticationService.get_permission_by_basic_token(token) - # permissions = PublicAuthenticationService.get_permissions_by_token_for_resource_and_scope(token) - # introspection = PublicAuthenticationService.introspect_token(basic_token) + # token = AuthenticationService.refresh_token(token) + # user_info = AuthenticationService.get_user_info_from_public_access_token(token) + # bearer_token = AuthenticationService.get_bearer_token(token) + # permission = AuthenticationService.get_permission_by_basic_token(token) + # permissions = AuthenticationService.get_permissions_by_token_for_resource_and_scope(token) + # introspection = AuthenticationService.introspect_token(basic_token) 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: """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: """Login.""" - state = PublicAuthenticationService.generate_state(redirect_url) - login_redirect_url = PublicAuthenticationService().get_login_redirect_url( + state = AuthenticationService.generate_state(redirect_url) + login_redirect_url = AuthenticationService().get_login_redirect_url( state.decode("UTF-8") ) 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_redirect_url = state_dict["redirect_url"] - id_token_object = PublicAuthenticationService().get_id_token_object(code) - if "id_token" in id_token_object: - id_token = id_token_object["id_token"] + auth_token_object = AuthenticationService().get_auth_token_object(code) + if "id_token" in auth_token_object: + id_token = auth_token_object["id_token"] - if PublicAuthenticationService.validate_id_token(id_token): - user_info = PublicAuthenticationService.get_user_info_from_id_token( - id_token_object["access_token"] + if AuthenticationService.validate_id_token(id_token): + user_info = AuthenticationService.get_user_info_from_open_id( + auth_token_object["access_token"] ) if user_info and "error" not in user_info: user_model = ( @@ -250,6 +240,10 @@ def login_return(code: str, state: str, session_state: str) -> Optional[Response if user_model: g.user = user_model.id + g.token = auth_token_object["id_token"] + AuthenticationService.store_refresh_token( + user_model.id, auth_token_object["refresh_token"] + ) # this may eventually get too slow. # when it does, be careful about backgrounding, because @@ -261,7 +255,7 @@ def login_return(code: str, state: str, session_state: str) -> Optional[Response 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}" ) return redirect(redirect_url) @@ -283,8 +277,8 @@ def login_return(code: str, state: str, session_state: str) -> Optional[Response def login_api() -> Response: """Login_api.""" redirect_url = "/v1.0/login_api_return" - state = PublicAuthenticationService.generate_state(redirect_url) - login_redirect_url = PublicAuthenticationService().get_login_redirect_url( + state = AuthenticationService.generate_state(redirect_url) + login_redirect_url = AuthenticationService().get_login_redirect_url( state.decode("UTF-8"), 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["redirect_url"] - id_token_object = PublicAuthenticationService().get_id_token_object( + auth_token_object = AuthenticationService().get_auth_token_object( 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 return access_token # return redirect("localhost:7000/v1.0/ui") @@ -309,9 +303,7 @@ def logout(id_token: str, redirect_url: Optional[str]) -> Response: """Logout.""" if redirect_url is None: redirect_url = "" - return PublicAuthenticationService().logout( - redirect_url=redirect_url, id_token=id_token - ) + return AuthenticationService().logout(redirect_url=redirect_url, id_token=id_token) def logout_return() -> Response: diff --git a/src/spiffworkflow_backend/services/authentication_service.py b/src/spiffworkflow_backend/services/authentication_service.py index 666aad27c..18f08d0f3 100644 --- a/src/spiffworkflow_backend/services/authentication_service.py +++ b/src/spiffworkflow_backend/services/authentication_service.py @@ -10,8 +10,11 @@ import requests from flask import current_app from flask import redirect from flask_bpmn.api.api_error import ApiError +from flask_bpmn.models.db import db from werkzeug.wrappers import Response +from spiffworkflow_backend.models.refresh_token import RefreshTokenModel + class AuthenticationProviderTypes(enum.Enum): """AuthenticationServiceProviders.""" @@ -20,13 +23,8 @@ class AuthenticationProviderTypes(enum.Enum): internal = "internal" -class PublicAuthenticationService: - """PublicAuthenticationService.""" - - """Not sure where/if this ultimately lives. - It uses a separate public open_id client: spiffworkflow-frontend - Used during development to make testing easy. - """ +class AuthenticationService: + """AuthenticationService.""" @staticmethod def get_open_id_args() -> tuple: @@ -45,8 +43,8 @@ class PublicAuthenticationService: ) @classmethod - def get_user_info_from_id_token(cls, token: str) -> dict: - """This seems to work with basic tokens too.""" + def get_user_info_from_open_id(cls, token: str) -> dict: + """The token is an auth_token.""" ( open_id_server_url, open_id_client_id, @@ -54,10 +52,6 @@ class PublicAuthenticationService: 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 = {"Authorization": f"Bearer {token}"} 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, ) - def get_backend_url(self) -> str: + @staticmethod + def get_backend_url() -> str: """Get_backend_url.""" return str(current_app.config["SPIFFWORKFLOW_BACKEND_URL"]) @@ -99,7 +94,7 @@ class PublicAuthenticationService: open_id_client_id, open_id_realm_name, open_id_client_secret_key, - ) = PublicAuthenticationService.get_open_id_args() + ) = AuthenticationService.get_open_id_args() request_url = ( f"{open_id_server_url}/realms/{open_id_realm_name}/protocol/openid-connect/logout?" + f"post_logout_redirect_uri={return_redirect_url}&" @@ -123,7 +118,7 @@ class PublicAuthenticationService: open_id_client_id, open_id_realm_name, 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}" login_redirect_url = ( f"{open_id_server_url}/realms/{open_id_realm_name}/protocol/openid-connect/auth?" @@ -135,16 +130,16 @@ class PublicAuthenticationService: ) return login_redirect_url - def get_id_token_object( + def get_auth_token_object( self, code: str, redirect_url: str = "/v1.0/login_return" ) -> dict: - """Get_id_token_object.""" + """Get_auth_token_object.""" ( open_id_server_url, open_id_client_id, open_id_realm_name, 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_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" response = requests.post(request_url, data=data, headers=headers) - id_token_object: dict = json.loads(response.text) - return id_token_object + auth_token_object: dict = json.loads(response.text) + return auth_token_object @classmethod def validate_id_token(cls, id_token: str) -> bool: @@ -211,3 +206,65 @@ class PublicAuthenticationService: ) 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 diff --git a/src/spiffworkflow_backend/services/authorization_service.py b/src/spiffworkflow_backend/services/authorization_service.py index f1272ef01..b9353686e 100644 --- a/src/spiffworkflow_backend/services/authorization_service.py +++ b/src/spiffworkflow_backend/services/authorization_service.py @@ -6,6 +6,8 @@ from typing import Union import jwt import yaml from flask import current_app +from flask import g +from flask import request from flask_bpmn.api.api_error import ApiError from flask_bpmn.models.db import db from sqlalchemy import text @@ -21,6 +23,10 @@ from spiffworkflow_backend.models.user_group_assignment import UserGroupAssignme from spiffworkflow_backend.services.user_service import UserService +class PermissionsFileNotSetError(Exception): + """PermissionsFileNotSetError.""" + + class AuthorizationService: """Determine whether a user has permission to perform their request.""" @@ -47,7 +53,9 @@ class AuthorizationService: elif permission_assignment.grant_type == "deny": return False else: - raise Exception("Unknown grant type") + raise Exception( + f"Unknown grant type: {permission_assignment.grant_type}" + ) return False @@ -72,11 +80,31 @@ class AuthorizationService: 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 def import_permissions_from_yaml_file( cls, raise_if_missing_user: bool = False ) -> None: """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 with open(current_app.config["PERMISSIONS_FILE_FULLPATH"]) as file: permission_configs = yaml.safe_load(file) @@ -171,6 +199,88 @@ class AuthorizationService: db.session.commit() 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: # """Refresh_token.""" # # if isinstance(token, str): diff --git a/src/spiffworkflow_backend/services/process_instance_processor.py b/src/spiffworkflow_backend/services/process_instance_processor.py index 435b79014..38d4fe374 100644 --- a/src/spiffworkflow_backend/services/process_instance_processor.py +++ b/src/spiffworkflow_backend/services/process_instance_processor.py @@ -889,9 +889,6 @@ class ProcessInstanceProcessor: self.process_bpmn_messages() self.queue_waiting_receive_messages() - if save: - self.save() - except WorkflowTaskExecException as we: raise ApiError.from_workflow_exception("task_error", str(we), we) from we diff --git a/src/spiffworkflow_backend/services/user_service.py b/src/spiffworkflow_backend/services/user_service.py index 3bf7f0924..c66eb211b 100644 --- a/src/spiffworkflow_backend/services/user_service.py +++ b/src/spiffworkflow_backend/services/user_service.py @@ -299,3 +299,17 @@ class UserService: ugam = UserGroupAssignmentModel(user_id=user.id, group_id=group.id) db.session.add(ugam) 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 diff --git a/tests/spiffworkflow_backend/helpers/base_test.py b/tests/spiffworkflow_backend/helpers/base_test.py index 7e38e45af..b586a2b0f 100644 --- a/tests/spiffworkflow_backend/helpers/base_test.py +++ b/tests/spiffworkflow_backend/helpers/base_test.py @@ -15,6 +15,8 @@ from flask_bpmn.models.db import db from tests.spiffworkflow_backend.helpers.test_data import load_test_spec from werkzeug.test import TestResponse # type: ignore +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 ProcessGroupSchema from spiffworkflow_backend.models.process_instance import ProcessInstanceModel @@ -92,6 +94,7 @@ class BaseTest: exception_notification_addresses: Optional[list] = None, primary_process_id: Optional[str] = None, primary_file_name: Optional[str] = None, + user: Optional[UserModel] = None, ) -> TestResponse: """Create_process_model.""" process_model_service = ProcessModelService() @@ -121,7 +124,9 @@ class BaseTest: fault_or_suspend_on_exception=fault_or_suspend_on_exception, exception_notification_addresses=exception_notification_addresses, ) - user = self.find_or_create_user() + if user is None: + user = self.find_or_create_user() + response = client.post( "/v1.0/process-models", content_type="application/json", @@ -139,6 +144,7 @@ class BaseTest: process_model: Optional[ProcessModelInfo] = None, file_name: str = "random_fact.svg", file_data: bytes = b"abcdef", + user: Optional[UserModel] = None, ) -> Any: """Test_create_spec_file.""" if process_model is None: @@ -146,7 +152,8 @@ class BaseTest: process_model_id, process_group_id=process_group_id ) data = {"file": (io.BytesIO(file_data), file_name)} - user = self.find_or_create_user() + if user is None: + user = self.find_or_create_user() response = client.post( f"/v1.0/process-models/{process_model.process_group_id}/{process_model.id}/files", data=data, @@ -194,7 +201,7 @@ class BaseTest: # @staticmethod # def get_public_access_token(username: str, password: str) -> dict: # """Get_public_access_token.""" - # public_access_token = PublicAuthenticationService().get_public_access_token( + # public_access_token = AuthenticationService().get_public_access_token( # username, password # ) # return public_access_token @@ -218,6 +225,46 @@ class BaseTest: db.session.commit() 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 def logged_in_headers( user: UserModel, _redirect_url: str = "http://some/frontend/url" diff --git a/tests/spiffworkflow_backend/integration/test_authentication.py b/tests/spiffworkflow_backend/integration/test_authentication.py index 934c1b244..34e4d71bc 100644 --- a/tests/spiffworkflow_backend/integration/test_authentication.py +++ b/tests/spiffworkflow_backend/integration/test_authentication.py @@ -5,7 +5,7 @@ import base64 from tests.spiffworkflow_backend.helpers.base_test import BaseTest from spiffworkflow_backend.services.authentication_service import ( - PublicAuthenticationService, + AuthenticationService, ) @@ -15,7 +15,7 @@ class TestAuthentication(BaseTest): def test_get_login_state(self) -> None: """Test_get_login_state.""" 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")) assert isinstance(state_dict, dict) @@ -24,9 +24,9 @@ class TestAuthentication(BaseTest): # def test_get_login_redirect_url(self): # redirect_url = "http://example.com/" - # state = PublicAuthenticationService.generate_state(redirect_url) + # state = AuthenticationService.generate_state(redirect_url) # 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") diff --git a/tests/spiffworkflow_backend/integration/test_authorization.py b/tests/spiffworkflow_backend/integration/test_authorization.py index 912e039a8..51eae72e7 100644 --- a/tests/spiffworkflow_backend/integration/test_authorization.py +++ b/tests/spiffworkflow_backend/integration/test_authorization.py @@ -9,7 +9,7 @@ class TestAuthorization(BaseTest): # """Test_get_bearer_token.""" # for user_id in ("user_1", "user_2", "admin_1", "admin_2"): # 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(bearer_token, dict) # assert "access_token" in bearer_token @@ -25,7 +25,7 @@ class TestAuthorization(BaseTest): # """Test_get_user_info_from_public_access_token.""" # for user_id in ("user_1", "user_2", "admin_1", "admin_2"): # 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 # ) # assert "sub" in user_info @@ -46,7 +46,7 @@ class TestAuthorization(BaseTest): # ) = self.get_keycloak_constants(app) # for user_id in ("user_1", "user_2", "admin_1", "admin_2"): # 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 introspection["typ"] == "Bearer" # 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"): # output[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 # ) # if isinstance(permissions, list): @@ -136,7 +136,7 @@ class TestAuthorization(BaseTest): # for resource in resources: # output[user_id][resource] = {} # 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 # ) # output[user_id][resource][scope] = auth_status @@ -152,7 +152,7 @@ class TestAuthorization(BaseTest): # for resource in resource_names: # output[user_id][resource] = {} # 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 # ) # output[user_id][resource][scope] = permissions diff --git a/tests/spiffworkflow_backend/integration/test_logging_service.py b/tests/spiffworkflow_backend/integration/test_logging_service.py index bb7ec7100..a8d2720f6 100644 --- a/tests/spiffworkflow_backend/integration/test_logging_service.py +++ b/tests/spiffworkflow_backend/integration/test_logging_service.py @@ -3,18 +3,23 @@ from flask.app import Flask from flask.testing import FlaskClient from tests.spiffworkflow_backend.helpers.base_test import BaseTest +from spiffworkflow_backend.models.user import UserModel + class TestLoggingService(BaseTest): """Test logging service.""" 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: """Test_process_instance_run.""" process_group_id = "test_logging_spiff_logger" process_model_id = "simple_script" - user = self.find_or_create_user() - headers = self.logged_in_headers(user) + headers = self.logged_in_headers(with_super_admin_user) response = self.create_process_instance( client, process_group_id, process_model_id, headers ) @@ -22,13 +27,13 @@ class TestLoggingService(BaseTest): process_instance_id = response.json["id"] response = client.post( 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 log_response = client.get( 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.json diff --git a/tests/spiffworkflow_backend/integration/test_process_api.py b/tests/spiffworkflow_backend/integration/test_process_api.py index aff5e5e4e..784954f48 100644 --- a/tests/spiffworkflow_backend/integration/test_process_api.py +++ b/tests/spiffworkflow_backend/integration/test_process_api.py @@ -38,18 +38,86 @@ from spiffworkflow_backend.services.process_model_service import ProcessModelSer class TestProcessApi(BaseTest): """TestProcessAPi.""" + def test_returns_403_if_user_does_not_have_permission( + self, + app: Flask, + client: FlaskClient, + with_db_and_bpmn_file_cleanup: None, + ) -> None: + """Test_returns_403_if_user_does_not_have_permission.""" + user = self.find_or_create_user() + response = client.get( + "/v1.0/process-groups", + headers=self.logged_in_headers(user), + ) + assert response.status_code == 403 + + self.add_permissions_to_user( + user, target_uri="/v1.0/process-groups", permission_names=["read"] + ) + response = client.get( + "/v1.0/process-groups", + headers=self.logged_in_headers(user), + ) + assert response.status_code == 200 + + response = client.post( + "/v1.0/process-groups", + headers=self.logged_in_headers(user), + ) + assert response.status_code == 403 + + def test_permissions_check( + self, + app: Flask, + client: FlaskClient, + with_db_and_bpmn_file_cleanup: None, + ) -> None: + """Test_permissions_check.""" + user = self.find_or_create_user() + self.add_permissions_to_user( + user, target_uri="/v1.0/process-groups", permission_names=["read"] + ) + request_body = { + "requests_to_check": { + "/v1.0/process-groups": ["GET", "POST"], + "/v1.0/process-models": ["GET"], + } + } + expected_response_body = { + "results": { + "/v1.0/process-groups": {"GET": True, "POST": False}, + "/v1.0/process-models": {"GET": False}, + } + } + response = client.post( + "/v1.0/permissions-check", + headers=self.logged_in_headers(user), + content_type="application/json", + data=json.dumps(request_body), + ) + assert response.status_code == 200 + assert response.json is not None + assert response.json == expected_response_body + def test_process_model_add( - 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: """Test_add_new_process_model.""" process_model_identifier = "sample" model_display_name = "Sample" model_description = "The sample" + self.create_process_model_with_api( client, process_model_id=process_model_identifier, process_model_display_name=model_display_name, process_model_description=model_description, + user=with_super_admin_user, ) process_model = ProcessModelService().get_process_model( process_model_identifier @@ -67,6 +135,7 @@ class TestProcessApi(BaseTest): file_name=bpmn_file_name, file_data=bpmn_file_data_bytes, process_model=process_model, + user=with_super_admin_user, ) process_model = ProcessModelService().get_process_model( process_model_identifier @@ -75,7 +144,11 @@ class TestProcessApi(BaseTest): assert process_model.primary_process_id == "sample" def test_primary_process_id_updates_via_xml( - 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: """Test_primary_process_id_updates_via_xml.""" process_model_identifier = "sample" @@ -98,13 +171,12 @@ class TestProcessApi(BaseTest): updated_bpmn_file_data_bytes = bytearray(updated_bpmn_file_data_string, "utf-8") data = {"file": (io.BytesIO(updated_bpmn_file_data_bytes), bpmn_file_name)} - user = self.find_or_create_user() response = client.put( f"/v1.0/process-models/{process_model.process_group_id}/{process_model.id}/files/{bpmn_file_name}", data=data, follow_redirects=True, content_type="multipart/form-data", - headers=self.logged_in_headers(user), + headers=self.logged_in_headers(with_super_admin_user), ) assert response.status_code == 200 process_model = ProcessModelService().get_process_model( @@ -114,10 +186,17 @@ class TestProcessApi(BaseTest): assert process_model.primary_process_id == terminal_primary_process_id def test_process_model_delete( - 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: """Test_process_model_delete.""" - self.create_process_model_with_api(client) + self.create_process_model_with_api( + client, + user=with_super_admin_user, + ) # assert we have a model process_model = ProcessModelService().get_process_model("make_cookies") @@ -125,10 +204,9 @@ class TestProcessApi(BaseTest): assert process_model.id == "make_cookies" # delete the model - user = self.find_or_create_user() response = client.delete( f"/v1.0/process-models/{process_model.process_group_id}/{process_model.id}", - headers=self.logged_in_headers(user), + headers=self.logged_in_headers(with_super_admin_user), ) assert response.status_code == 200 assert response.json is not None @@ -139,13 +217,16 @@ class TestProcessApi(BaseTest): ProcessModelService().get_process_model("make_cookies") def test_process_model_delete_with_instances( - 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: """Test_process_model_delete_with_instances.""" test_process_group_id = "runs_without_input" test_process_model_id = "sample" - user = self.find_or_create_user() - headers = self.logged_in_headers(user) + headers = self.logged_in_headers(with_super_admin_user) # create an instance from a model response = self.create_process_instance( client, test_process_group_id, test_process_model_id, headers @@ -158,7 +239,7 @@ class TestProcessApi(BaseTest): # try to delete the model response = client.delete( f"/v1.0/process-models/{test_process_group_id}/{test_process_model_id}", - headers=self.logged_in_headers(user), + headers=self.logged_in_headers(with_super_admin_user), ) # make sure we get an error in the response @@ -171,10 +252,17 @@ class TestProcessApi(BaseTest): ) def test_process_model_update( - 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: """Test_process_model_update.""" - self.create_process_model_with_api(client) + self.create_process_model_with_api( + client, + user=with_super_admin_user, + ) process_model = ProcessModelService().get_process_model("make_cookies") assert process_model.id == "make_cookies" assert process_model.display_name == "Cooooookies" @@ -187,10 +275,9 @@ class TestProcessApi(BaseTest): process_model.primary_process_id = "superduper" process_model.is_review = True # not in the include list, so get ignored - user = self.find_or_create_user() response = client.put( f"/v1.0/process-models/{process_model.process_group_id}/{process_model.id}", - headers=self.logged_in_headers(user), + headers=self.logged_in_headers(with_super_admin_user), content_type="application/json", data=json.dumps(ProcessModelInfoSchema().dump(process_model)), ) @@ -202,13 +289,16 @@ class TestProcessApi(BaseTest): assert response.json["is_review"] is False def test_process_model_list( - 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: """Test_process_model_list.""" # create a group group_id = "test_group" - user = self.find_or_create_user() - self.create_process_group(client, user, group_id) + self.create_process_group(client, with_super_admin_user, group_id) # add 5 models to the group for i in range(5): @@ -217,16 +307,17 @@ class TestProcessApi(BaseTest): model_description = f"Test Model {i} Description" self.create_process_model_with_api( client, - group_id, - process_model_identifier, - model_display_name, - model_description, + process_group_id=group_id, + process_model_id=process_model_identifier, + process_model_display_name=model_display_name, + process_model_description=model_description, + user=with_super_admin_user, ) # get all models response = client.get( f"/v1.0/process-models?process_group_identifier={group_id}", - headers=self.logged_in_headers(user), + headers=self.logged_in_headers(with_super_admin_user), ) assert response.json is not None assert len(response.json["results"]) == 5 @@ -237,7 +328,7 @@ class TestProcessApi(BaseTest): # get first page, 1 per page response = client.get( f"/v1.0/process-models?page=1&per_page=1&process_group_identifier={group_id}", - headers=self.logged_in_headers(user), + headers=self.logged_in_headers(with_super_admin_user), ) assert response.json is not None assert len(response.json["results"]) == 1 @@ -249,7 +340,7 @@ class TestProcessApi(BaseTest): # get second page, 1 per page response = client.get( f"/v1.0/process-models?page=2&per_page=1&process_group_identifier={group_id}", - headers=self.logged_in_headers(user), + headers=self.logged_in_headers(with_super_admin_user), ) assert response.json is not None assert len(response.json["results"]) == 1 @@ -261,7 +352,7 @@ class TestProcessApi(BaseTest): # get first page, 3 per page response = client.get( f"/v1.0/process-models?page=1&per_page=3&process_group_identifier={group_id}", - headers=self.logged_in_headers(user), + headers=self.logged_in_headers(with_super_admin_user), ) assert response.json is not None assert len(response.json["results"]) == 3 @@ -273,7 +364,7 @@ class TestProcessApi(BaseTest): # get second page, 3 per page response = client.get( f"/v1.0/process-models?page=2&per_page=3&process_group_identifier={group_id}", - headers=self.logged_in_headers(user), + headers=self.logged_in_headers(with_super_admin_user), ) # there should only be 2 left assert response.json is not None @@ -284,7 +375,11 @@ class TestProcessApi(BaseTest): assert response.json["pagination"]["pages"] == 2 def test_process_group_add( - 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: """Test_add_process_group.""" process_group = ProcessGroup( @@ -293,10 +388,9 @@ class TestProcessApi(BaseTest): display_order=0, admin=False, ) - user = self.find_or_create_user() response = client.post( "/v1.0/process-groups", - headers=self.logged_in_headers(user), + headers=self.logged_in_headers(with_super_admin_user), content_type="application/json", data=json.dumps(ProcessGroupSchema().dump(process_group)), ) @@ -314,15 +408,21 @@ class TestProcessApi(BaseTest): assert persisted.id == "test" def test_process_group_delete( - 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: """Test_process_group_delete.""" process_group_id = "test" process_group_display_name = "My Process Group" - user = self.find_or_create_user() self.create_process_group( - client, user, process_group_id, display_name=process_group_display_name + client, + with_super_admin_user, + process_group_id, + display_name=process_group_display_name, ) persisted = ProcessModelService().get_process_group(process_group_id) assert persisted is not None @@ -330,22 +430,25 @@ class TestProcessApi(BaseTest): client.delete( f"/v1.0/process-groups/{process_group_id}", - headers=self.logged_in_headers(user), + headers=self.logged_in_headers(with_super_admin_user), ) with pytest.raises(ProcessEntityNotFoundError): ProcessModelService().get_process_group(process_group_id) def test_process_group_update( - 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: """Test Process Group Update.""" group_id = "test_process_group" group_display_name = "Test Group" - user = self.find_or_create_user() self.create_process_group( - client, user, group_id, display_name=group_display_name + client, with_super_admin_user, group_id, display_name=group_display_name ) process_group = ProcessModelService().get_process_group(group_id) @@ -355,7 +458,7 @@ class TestProcessApi(BaseTest): response = client.put( f"/v1.0/process-groups/{group_id}", - headers=self.logged_in_headers(user), + headers=self.logged_in_headers(with_super_admin_user), content_type="application/json", data=json.dumps(ProcessGroupSchema().dump(process_group)), ) @@ -365,22 +468,25 @@ class TestProcessApi(BaseTest): assert process_group.display_name == "Modified Display Name" def test_process_group_list( - 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: """Test_process_group_list.""" # add 5 groups - user = self.find_or_create_user() for i in range(5): group_id = f"test_process_group_{i}" group_display_name = f"Test Group {i}" self.create_process_group( - client, user, group_id, display_name=group_display_name + client, with_super_admin_user, group_id, display_name=group_display_name ) # get all groups response = client.get( "/v1.0/process-groups", - headers=self.logged_in_headers(user), + headers=self.logged_in_headers(with_super_admin_user), ) assert response.json is not None assert len(response.json["results"]) == 5 @@ -391,7 +497,7 @@ class TestProcessApi(BaseTest): # get first page, one per page response = client.get( "/v1.0/process-groups?page=1&per_page=1", - headers=self.logged_in_headers(user), + headers=self.logged_in_headers(with_super_admin_user), ) assert response.json is not None assert len(response.json["results"]) == 1 @@ -403,7 +509,7 @@ class TestProcessApi(BaseTest): # get second page, one per page response = client.get( "/v1.0/process-groups?page=2&per_page=1", - headers=self.logged_in_headers(user), + headers=self.logged_in_headers(with_super_admin_user), ) assert response.json is not None assert len(response.json["results"]) == 1 @@ -415,7 +521,7 @@ class TestProcessApi(BaseTest): # get first page, 3 per page response = client.get( "/v1.0/process-groups?page=1&per_page=3", - headers=self.logged_in_headers(user), + headers=self.logged_in_headers(with_super_admin_user), ) assert response.json is not None assert len(response.json["results"]) == 3 @@ -429,7 +535,7 @@ class TestProcessApi(BaseTest): # get second page, 3 per page response = client.get( "/v1.0/process-groups?page=2&per_page=3", - headers=self.logged_in_headers(user), + headers=self.logged_in_headers(with_super_admin_user), ) # there should only be 2 left assert response.json is not None @@ -441,20 +547,23 @@ class TestProcessApi(BaseTest): assert response.json["pagination"]["pages"] == 2 def test_process_model_file_update_fails_if_no_file_given( - 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: """Test_process_model_file_update.""" - self.create_spec_file(client) + self.create_spec_file(client, user=with_super_admin_user) process_model = load_test_spec("random_fact") data = {"key1": "THIS DATA"} - user = self.find_or_create_user() response = client.put( f"/v1.0/process-models/{process_model.process_group_id}/{process_model.id}/files/random_fact.svg", data=data, follow_redirects=True, content_type="multipart/form-data", - headers=self.logged_in_headers(user), + headers=self.logged_in_headers(with_super_admin_user), ) assert response.status_code == 400 @@ -462,20 +571,23 @@ class TestProcessApi(BaseTest): assert response.json["error_code"] == "no_file_given" def test_process_model_file_update_fails_if_contents_is_empty( - 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: """Test_process_model_file_update.""" - self.create_spec_file(client) + self.create_spec_file(client, user=with_super_admin_user) process_model = load_test_spec("random_fact") data = {"file": (io.BytesIO(b""), "random_fact.svg")} - user = self.find_or_create_user() response = client.put( f"/v1.0/process-models/{process_model.process_group_id}/{process_model.id}/files/random_fact.svg", data=data, follow_redirects=True, content_type="multipart/form-data", - headers=self.logged_in_headers(user), + headers=self.logged_in_headers(with_super_admin_user), ) assert response.status_code == 400 @@ -483,21 +595,24 @@ class TestProcessApi(BaseTest): assert response.json["error_code"] == "file_contents_empty" def test_process_model_file_update( - 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: """Test_process_model_file_update.""" - original_file = self.create_spec_file(client) + original_file = self.create_spec_file(client, user=with_super_admin_user) process_model = load_test_spec("random_fact") new_file_contents = b"THIS_IS_NEW_DATA" data = {"file": (io.BytesIO(new_file_contents), "random_fact.svg")} - user = self.find_or_create_user() response = client.put( f"/v1.0/process-models/{process_model.process_group_id}/{process_model.id}/files/random_fact.svg", data=data, follow_redirects=True, content_type="multipart/form-data", - headers=self.logged_in_headers(user), + headers=self.logged_in_headers(with_super_admin_user), ) assert response.status_code == 200 @@ -506,7 +621,7 @@ class TestProcessApi(BaseTest): response = client.get( f"/v1.0/process-models/{process_model.process_group_id}/{process_model.id}/files/random_fact.svg", - headers=self.logged_in_headers(user), + headers=self.logged_in_headers(with_super_admin_user), ) assert response.status_code == 200 updated_file = json.loads(response.get_data(as_text=True)) @@ -514,17 +629,20 @@ class TestProcessApi(BaseTest): assert updated_file["file_contents"] == new_file_contents.decode() def test_process_model_file_delete_when_bad_process_model( - 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: """Test_process_model_file_update.""" - self.create_spec_file(client) + self.create_spec_file(client, user=with_super_admin_user) process_model = load_test_spec("random_fact") - user = self.find_or_create_user() response = client.delete( f"/v1.0/process-models/INCORRECT-NON-EXISTENT-GROUP/{process_model.id}/files/random_fact.svg", follow_redirects=True, - headers=self.logged_in_headers(user), + headers=self.logged_in_headers(with_super_admin_user), ) assert response.status_code == 400 @@ -532,17 +650,20 @@ class TestProcessApi(BaseTest): assert response.json["error_code"] == "process_model_cannot_be_found" def test_process_model_file_delete_when_bad_file( - 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: """Test_process_model_file_update.""" - self.create_spec_file(client) + self.create_spec_file(client, user=with_super_admin_user) process_model = load_test_spec("random_fact") - user = self.find_or_create_user() response = client.delete( f"/v1.0/process-models/{process_model.process_group_id}/{process_model.id}/files/random_fact_DOES_NOT_EXIST.svg", follow_redirects=True, - headers=self.logged_in_headers(user), + headers=self.logged_in_headers(with_super_admin_user), ) assert response.status_code == 400 @@ -550,17 +671,20 @@ class TestProcessApi(BaseTest): assert response.json["error_code"] == "process_model_file_cannot_be_found" def test_process_model_file_delete( - 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: """Test_process_model_file_update.""" - self.create_spec_file(client) + self.create_spec_file(client, user=with_super_admin_user) process_model = load_test_spec("random_fact") - user = self.find_or_create_user() response = client.delete( f"/v1.0/process-models/{process_model.process_group_id}/{process_model.id}/files/random_fact.svg", follow_redirects=True, - headers=self.logged_in_headers(user), + headers=self.logged_in_headers(with_super_admin_user), ) assert response.status_code == 200 @@ -569,21 +693,24 @@ class TestProcessApi(BaseTest): response = client.get( f"/v1.0/process-models/{process_model.process_group_id}/{process_model.id}/files/random_fact.svg", - headers=self.logged_in_headers(user), + headers=self.logged_in_headers(with_super_admin_user), ) assert response.status_code == 404 def test_get_file( - 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: """Test_get_file.""" - user = self.find_or_create_user() test_process_group_id = "group_id1" process_model_dir_name = "hello_world" load_test_spec(process_model_dir_name, process_group_id=test_process_group_id) response = client.get( f"/v1.0/process-models/{test_process_group_id}/{process_model_dir_name}/files/hello_world.bpmn", - headers=self.logged_in_headers(user), + headers=self.logged_in_headers(with_super_admin_user), ) assert response.status_code == 200 assert response.json is not None @@ -592,14 +719,17 @@ class TestProcessApi(BaseTest): assert response.json["process_model_id"] == "hello_world" def test_get_workflow_from_workflow_spec( - 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: """Test_get_workflow_from_workflow_spec.""" - user = self.find_or_create_user() process_model = load_test_spec("hello_world") response = client.post( f"/v1.0/process-models/{process_model.process_group_id}/{process_model.id}/process-instances", - headers=self.logged_in_headers(user), + headers=self.logged_in_headers(with_super_admin_user), ) assert response.status_code == 201 assert response.json is not None @@ -607,25 +737,33 @@ class TestProcessApi(BaseTest): # assert('Task_GetName' == response.json['next_task']['name']) def test_get_process_groups_when_none( - 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: """Test_get_process_groups_when_none.""" - user = self.find_or_create_user() response = client.get( - "/v1.0/process-groups", headers=self.logged_in_headers(user) + "/v1.0/process-groups", + headers=self.logged_in_headers(with_super_admin_user), ) assert response.status_code == 200 assert response.json is not None assert response.json["results"] == [] def test_get_process_groups_when_there_are_some( - 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: """Test_get_process_groups_when_there_are_some.""" - user = self.find_or_create_user() load_test_spec("hello_world") response = client.get( - "/v1.0/process-groups", headers=self.logged_in_headers(user) + "/v1.0/process-groups", + headers=self.logged_in_headers(with_super_admin_user), ) assert response.status_code == 200 assert response.json is not None @@ -635,16 +773,19 @@ class TestProcessApi(BaseTest): assert response.json["pagination"]["pages"] == 1 def test_get_process_group_when_found( - 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: """Test_get_process_group_when_found.""" - user = self.find_or_create_user() test_process_group_id = "group_id1" process_model_dir_name = "hello_world" load_test_spec(process_model_dir_name, process_group_id=test_process_group_id) response = client.get( f"/v1.0/process-groups/{test_process_group_id}", - headers=self.logged_in_headers(user), + headers=self.logged_in_headers(with_super_admin_user), ) assert response.status_code == 200 assert response.json is not None @@ -652,16 +793,19 @@ class TestProcessApi(BaseTest): assert response.json["process_models"][0]["id"] == process_model_dir_name def test_get_process_model_when_found( - 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: """Test_get_process_model_when_found.""" - user = self.find_or_create_user() test_process_group_id = "group_id1" process_model_dir_name = "hello_world" load_test_spec(process_model_dir_name, process_group_id=test_process_group_id) response = client.get( f"/v1.0/process-models/{test_process_group_id}/{process_model_dir_name}", - headers=self.logged_in_headers(user), + headers=self.logged_in_headers(with_super_admin_user), ) assert response.status_code == 200 assert response.json is not None @@ -670,28 +814,34 @@ class TestProcessApi(BaseTest): assert response.json["files"][0]["name"] == "hello_world.bpmn" def test_get_process_model_when_not_found( - 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: """Test_get_process_model_when_not_found.""" - user = self.find_or_create_user() process_model_dir_name = "THIS_NO_EXISTS" - group_id = self.create_process_group(client, user, "my_group") + group_id = self.create_process_group(client, with_super_admin_user, "my_group") response = client.get( f"/v1.0/process-models/{group_id}/{process_model_dir_name}", - headers=self.logged_in_headers(user), + headers=self.logged_in_headers(with_super_admin_user), ) assert response.status_code == 400 assert response.json is not None assert response.json["error_code"] == "process_model_cannot_be_found" def test_process_instance_create( - 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: """Test_process_instance_create.""" test_process_group_id = "runs_without_input" test_process_model_id = "sample" - user = self.find_or_create_user() - headers = self.logged_in_headers(user) + headers = self.logged_in_headers(with_super_admin_user) response = self.create_process_instance( client, test_process_group_id, test_process_model_id, headers ) @@ -704,13 +854,16 @@ class TestProcessApi(BaseTest): # assert response.json["bpmn_version_control_identifier"] == current_revision def test_process_instance_run( - 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: """Test_process_instance_run.""" process_group_id = "runs_without_input" process_model_id = "sample" - user = self.find_or_create_user() - headers = self.logged_in_headers(user) + headers = self.logged_in_headers(with_super_admin_user) response = self.create_process_instance( client, process_group_id, process_model_id, headers ) @@ -718,7 +871,7 @@ class TestProcessApi(BaseTest): process_instance_id = response.json["id"] response = client.post( f"/v1.0/process-models/{process_group_id}/{process_model_id}/process-instances/{process_instance_id}/run", - headers=self.logged_in_headers(user), + headers=self.logged_in_headers(with_super_admin_user), ) assert response.json is not None @@ -726,18 +879,24 @@ class TestProcessApi(BaseTest): assert response.json["updated_at_in_seconds"] > 0 assert response.json["status"] == "complete" assert response.json["process_model_identifier"] == process_model_id - assert response.json["data"]["current_user"]["username"] == user.username + assert ( + response.json["data"]["current_user"]["username"] + == with_super_admin_user.username + ) assert response.json["data"]["Mike"] == "Awesome" assert response.json["data"]["person"] == "Kevin" def test_process_instance_show( - 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: """Test_process_instance_show.""" process_group_id = "simple_script" process_model_id = "simple_script" - user = self.find_or_create_user() - headers = self.logged_in_headers(user) + headers = self.logged_in_headers(with_super_admin_user) create_response = self.create_process_instance( client, process_group_id, process_model_id, headers ) @@ -745,11 +904,11 @@ class TestProcessApi(BaseTest): process_instance_id = create_response.json["id"] client.post( f"/v1.0/process-models/{process_group_id}/{process_model_id}/process-instances/{process_instance_id}/run", - headers=self.logged_in_headers(user), + headers=self.logged_in_headers(with_super_admin_user), ) show_response = client.get( f"/v1.0/process-models/{process_group_id}/{process_model_id}/process-instances/{process_instance_id}", - headers=self.logged_in_headers(user), + headers=self.logged_in_headers(with_super_admin_user), ) assert show_response.json is not None file_system_root = FileSystemService.root_path() @@ -759,7 +918,11 @@ class TestProcessApi(BaseTest): assert show_response.json["bpmn_xml_file_contents"] == xml_file_contents def test_message_start_when_starting_process_instance( - 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: """Test_message_start_when_starting_process_instance.""" # ensure process model is loaded in db @@ -768,7 +931,6 @@ class TestProcessApi(BaseTest): process_model_source_directory="message_send_one_conversation", bpmn_file_name="message_receiver", ) - user = self.find_or_create_user() message_model_identifier = "message_send" payload = { "topica": "the_topica_string", @@ -778,7 +940,7 @@ class TestProcessApi(BaseTest): response = client.post( f"/v1.0/messages/{message_model_identifier}", content_type="application/json", - headers=self.logged_in_headers(user), + headers=self.logged_in_headers(with_super_admin_user), data=json.dumps({"payload": payload}), ) assert response.status_code == 200 @@ -797,7 +959,11 @@ class TestProcessApi(BaseTest): assert process_instance_data["the_payload"] == payload def test_message_start_when_providing_message_to_running_process_instance( - 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: """Test_message_start_when_providing_message_to_running_process_instance.""" process_model = load_test_spec( @@ -805,7 +971,6 @@ class TestProcessApi(BaseTest): process_model_source_directory="message_send_one_conversation", bpmn_file_name="message_sender", ) - user = self.find_or_create_user() message_model_identifier = "message_response" payload = { "the_payload": { @@ -818,7 +983,7 @@ class TestProcessApi(BaseTest): client, process_model.process_group_id, process_model.id, - self.logged_in_headers(user), + self.logged_in_headers(with_super_admin_user), ) assert response.json is not None process_instance_id = response.json["id"] @@ -826,7 +991,7 @@ class TestProcessApi(BaseTest): response = client.post( f"/v1.0/process-models/{process_model.process_group_id}/" f"{process_model.id}/process-instances/{process_instance_id}/run", - headers=self.logged_in_headers(user), + headers=self.logged_in_headers(with_super_admin_user), ) assert response.json is not None @@ -834,7 +999,7 @@ class TestProcessApi(BaseTest): response = client.post( f"/v1.0/messages/{message_model_identifier}", content_type="application/json", - headers=self.logged_in_headers(user), + headers=self.logged_in_headers(with_super_admin_user), data=json.dumps( {"payload": payload, "process_instance_id": process_instance_id} ), @@ -855,7 +1020,11 @@ class TestProcessApi(BaseTest): assert process_instance_data["the_payload"] == payload def test_process_instance_can_be_terminated( - 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: """Test_message_start_when_providing_message_to_running_process_instance.""" # this task will wait on a catch event @@ -864,12 +1033,11 @@ class TestProcessApi(BaseTest): process_model_source_directory="message_send_one_conversation", bpmn_file_name="message_sender", ) - user = self.find_or_create_user() response = self.create_process_instance( client, process_model.process_group_id, process_model.id, - self.logged_in_headers(user), + self.logged_in_headers(with_super_admin_user), ) assert response.json is not None process_instance_id = response.json["id"] @@ -877,7 +1045,7 @@ class TestProcessApi(BaseTest): response = client.post( f"/v1.0/process-models/{process_model.process_group_id}/" f"{process_model.id}/process-instances/{process_instance_id}/run", - headers=self.logged_in_headers(user), + headers=self.logged_in_headers(with_super_admin_user), ) assert response.status_code == 200 assert response.json is not None @@ -885,7 +1053,7 @@ class TestProcessApi(BaseTest): response = client.post( f"/v1.0/process-models/{process_model.process_group_id}/" f"{process_model.id}/process-instances/{process_instance_id}/terminate", - headers=self.logged_in_headers(user), + headers=self.logged_in_headers(with_super_admin_user), ) assert response.status_code == 200 assert response.json is not None @@ -897,14 +1065,17 @@ class TestProcessApi(BaseTest): assert process_instance.status == "terminated" def test_process_instance_delete( - 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: """Test_process_instance_delete.""" process_group_id = "my_process_group" process_model_id = "user_task" - user = self.find_or_create_user() - headers = self.logged_in_headers(user) + headers = self.logged_in_headers(with_super_admin_user) response = self.create_process_instance( client, process_group_id, process_model_id, headers ) @@ -913,7 +1084,7 @@ class TestProcessApi(BaseTest): response = client.post( f"/v1.0/process-models/{process_group_id}/{process_model_id}/process-instances/{process_instance_id}/run", - headers=self.logged_in_headers(user), + headers=self.logged_in_headers(with_super_admin_user), ) assert response.json is not None @@ -924,23 +1095,26 @@ class TestProcessApi(BaseTest): ) assert len(task_events) == 1 task_event = task_events[0] - assert task_event.user_id == user.id + assert task_event.user_id == with_super_admin_user.id delete_response = client.delete( f"/v1.0/process-models/{process_group_id}/{process_model_id}/process-instances/{process_instance_id}", - headers=self.logged_in_headers(user), + headers=self.logged_in_headers(with_super_admin_user), ) assert delete_response.status_code == 200 def test_process_instance_run_user_task_creates_task_event( - 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: """Test_process_instance_run_user_task.""" process_group_id = "my_process_group" process_model_id = "user_task" - user = self.find_or_create_user() - headers = self.logged_in_headers(user) + headers = self.logged_in_headers(with_super_admin_user) response = self.create_process_instance( client, process_group_id, process_model_id, headers ) @@ -949,7 +1123,7 @@ class TestProcessApi(BaseTest): response = client.post( f"/v1.0/process-models/{process_group_id}/{process_model_id}/process-instances/{process_instance_id}/run", - headers=self.logged_in_headers(user), + headers=self.logged_in_headers(with_super_admin_user), ) assert response.json is not None @@ -960,18 +1134,21 @@ class TestProcessApi(BaseTest): ) assert len(task_events) == 1 task_event = task_events[0] - assert task_event.user_id == user.id + assert task_event.user_id == with_super_admin_user.id # TODO: When user tasks work, we need to add some more assertions for action, task_state, etc. def test_task_show( - 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: """Test_process_instance_run_user_task.""" process_group_id = "my_process_group" process_model_id = "dynamic_enum_select_fields" - user = self.find_or_create_user() - headers = self.logged_in_headers(user) + headers = self.logged_in_headers(with_super_admin_user) response = self.create_process_instance( client, process_group_id, process_model_id, headers ) @@ -980,7 +1157,7 @@ class TestProcessApi(BaseTest): response = client.post( f"/v1.0/process-models/{process_group_id}/{process_model_id}/process-instances/{process_instance_id}/run", - headers=self.logged_in_headers(user), + headers=self.logged_in_headers(with_super_admin_user), ) assert response.json is not None @@ -993,7 +1170,7 @@ class TestProcessApi(BaseTest): active_task = active_tasks[0] response = client.get( f"/v1.0/tasks/{process_instance_id}/{active_task.task_id}", - headers=self.logged_in_headers(user), + headers=self.logged_in_headers(with_super_admin_user), ) assert response.json is not None assert ( @@ -1002,20 +1179,23 @@ class TestProcessApi(BaseTest): ) def test_process_instance_list_with_default_list( - 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: """Test_process_instance_list_with_default_list.""" test_process_group_id = "runs_without_input" process_model_dir_name = "sample" - user = self.find_or_create_user() - headers = self.logged_in_headers(user) + headers = self.logged_in_headers(with_super_admin_user) self.create_process_instance( client, test_process_group_id, process_model_dir_name, headers ) response = client.get( "/v1.0/process-instances", - headers=self.logged_in_headers(user), + headers=self.logged_in_headers(with_super_admin_user), ) assert response.status_code == 200 assert response.json is not None @@ -1038,13 +1218,16 @@ class TestProcessApi(BaseTest): assert process_instance_dict["status"] == "not_started" def test_process_instance_list_with_paginated_items( - 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: """Test_process_instance_list_with_paginated_items.""" test_process_group_id = "runs_without_input" process_model_dir_name = "sample" - user = self.find_or_create_user() - headers = self.logged_in_headers(user) + headers = self.logged_in_headers(with_super_admin_user) self.create_process_instance( client, test_process_group_id, process_model_dir_name, headers ) @@ -1063,7 +1246,7 @@ class TestProcessApi(BaseTest): response = client.get( "/v1.0/process-instances?per_page=2&page=3", - headers=self.logged_in_headers(user), + headers=self.logged_in_headers(with_super_admin_user), ) assert response.status_code == 200 assert response.json is not None @@ -1074,7 +1257,7 @@ class TestProcessApi(BaseTest): response = client.get( "/v1.0/process-instances?per_page=2&page=1", - headers=self.logged_in_headers(user), + headers=self.logged_in_headers(with_super_admin_user), ) assert response.status_code == 200 assert response.json is not None @@ -1084,12 +1267,15 @@ class TestProcessApi(BaseTest): assert response.json["pagination"]["total"] == 5 def test_process_instance_list_filter( - 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: """Test_process_instance_list_filter.""" test_process_group_id = "runs_without_input" test_process_model_id = "sample" - user = self.find_or_create_user() load_test_spec(test_process_model_id, process_group_id=test_process_group_id) statuses = [status.value for status in ProcessInstanceStatus] @@ -1097,7 +1283,7 @@ class TestProcessApi(BaseTest): for i in range(5): process_instance = ProcessInstanceModel( status=ProcessInstanceStatus[statuses[i]].value, - process_initiator=user, + process_initiator=with_super_admin_user, process_model_identifier=test_process_model_id, process_group_identifier=test_process_group_id, updated_at_in_seconds=round(time.time()), @@ -1111,7 +1297,7 @@ class TestProcessApi(BaseTest): # Without filtering we should get all 5 instances response = client.get( f"/v1.0/process-instances?process_group_identifier={test_process_group_id}&process_model_identifier={test_process_model_id}", - headers=self.logged_in_headers(user), + headers=self.logged_in_headers(with_super_admin_user), ) assert response.json is not None results = response.json["results"] @@ -1122,7 +1308,7 @@ class TestProcessApi(BaseTest): for i in range(5): response = client.get( f"/v1.0/process-instances?process_status={ProcessInstanceStatus[statuses[i]].value}&process_group_identifier={test_process_group_id}&process_model_identifier={test_process_model_id}", - headers=self.logged_in_headers(user), + headers=self.logged_in_headers(with_super_admin_user), ) assert response.json is not None results = response.json["results"] @@ -1131,7 +1317,7 @@ class TestProcessApi(BaseTest): response = client.get( f"/v1.0/process-instances?process_status=not_started,complete&process_group_identifier={test_process_group_id}&process_model_identifier={test_process_model_id}", - headers=self.logged_in_headers(user), + headers=self.logged_in_headers(with_super_admin_user), ) assert response.json is not None results = response.json["results"] @@ -1143,7 +1329,7 @@ class TestProcessApi(BaseTest): # start > 1000 - this should eliminate the first response = client.get( "/v1.0/process-instances?start_from=1001", - headers=self.logged_in_headers(user), + headers=self.logged_in_headers(with_super_admin_user), ) assert response.json is not None results = response.json["results"] @@ -1154,7 +1340,7 @@ class TestProcessApi(BaseTest): # start > 2000, end < 5000 - this should eliminate the first 2 and the last response = client.get( "/v1.0/process-instances?start_from=2001&end_till=5999", - headers=self.logged_in_headers(user), + headers=self.logged_in_headers(with_super_admin_user), ) assert response.json is not None results = response.json["results"] @@ -1165,7 +1351,7 @@ class TestProcessApi(BaseTest): # start > 1000, start < 4000 - this should eliminate the first and the last 2 response = client.get( "/v1.0/process-instances?start_from=1001&start_till=3999", - headers=self.logged_in_headers(user), + headers=self.logged_in_headers(with_super_admin_user), ) assert response.json is not None results = response.json["results"] @@ -1176,7 +1362,7 @@ class TestProcessApi(BaseTest): # end > 2000, end < 6000 - this should eliminate the first and the last response = client.get( "/v1.0/process-instances?end_from=2001&end_till=5999", - headers=self.logged_in_headers(user), + headers=self.logged_in_headers(with_super_admin_user), ) assert response.json is not None results = response.json["results"] @@ -1185,13 +1371,16 @@ class TestProcessApi(BaseTest): assert json.loads(results[i]["bpmn_json"])["i"] in (1, 2, 3) def test_process_instance_report_list( - 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: """Test_process_instance_report_list.""" process_group_identifier = "runs_without_input" process_model_identifier = "sample" - user = self.find_or_create_user() - self.logged_in_headers(user) + self.logged_in_headers(with_super_admin_user) load_test_spec( process_model_identifier, process_group_id=process_group_identifier ) @@ -1202,11 +1391,11 @@ class TestProcessApi(BaseTest): process_group_identifier=process_group_identifier, process_model_identifier=process_model_identifier, report_metadata=report_metadata, - user=user, + user=with_super_admin_user, ) response = client.get( f"/v1.0/process-models/{process_group_identifier}/{process_model_identifier}/process-instances/reports", - headers=self.logged_in_headers(user), + headers=self.logged_in_headers(with_super_admin_user), ) assert response.status_code == 200 assert response.json is not None @@ -1219,12 +1408,12 @@ class TestProcessApi(BaseTest): app: Flask, client: FlaskClient, with_db_and_bpmn_file_cleanup: None, + with_super_admin_user: UserModel, setup_process_instances_for_reports: list[ProcessInstanceModel], ) -> None: """Test_process_instance_report_show_with_default_list.""" test_process_group_id = "runs_without_input" process_model_dir_name = "sample" - user = self.find_or_create_user() report_metadata = { "columns": [ @@ -1250,12 +1439,12 @@ class TestProcessApi(BaseTest): process_group_identifier=test_process_group_id, process_model_identifier=process_model_dir_name, report_metadata=report_metadata, - user=self.find_or_create_user(), + user=with_super_admin_user, ) response = client.get( f"/v1.0/process-models/{test_process_group_id}/{process_model_dir_name}/process-instances/reports/sure", - headers=self.logged_in_headers(user), + headers=self.logged_in_headers(with_super_admin_user), ) assert response.status_code == 200 assert response.json is not None @@ -1281,12 +1470,12 @@ class TestProcessApi(BaseTest): app: Flask, client: FlaskClient, with_db_and_bpmn_file_cleanup: None, + with_super_admin_user: UserModel, setup_process_instances_for_reports: list[ProcessInstanceModel], ) -> None: """Test_process_instance_report_show_with_default_list.""" test_process_group_id = "runs_without_input" process_model_dir_name = "sample" - user = self.find_or_create_user() report_metadata = { "filter_by": [ @@ -1303,12 +1492,12 @@ class TestProcessApi(BaseTest): process_group_identifier=test_process_group_id, process_model_identifier=process_model_dir_name, report_metadata=report_metadata, - user=self.find_or_create_user(), + user=with_super_admin_user, ) response = client.get( f"/v1.0/process-models/{test_process_group_id}/{process_model_dir_name}/process-instances/reports/sure?grade_level=1", - headers=self.logged_in_headers(user), + headers=self.logged_in_headers(with_super_admin_user), ) assert response.status_code == 200 assert response.json is not None @@ -1319,16 +1508,16 @@ class TestProcessApi(BaseTest): app: Flask, client: FlaskClient, with_db_and_bpmn_file_cleanup: None, + with_super_admin_user: UserModel, setup_process_instances_for_reports: list[ProcessInstanceModel], ) -> None: """Test_process_instance_report_show_with_default_list.""" test_process_group_id = "runs_without_input" process_model_dir_name = "sample" - user = self.find_or_create_user() response = client.get( f"/v1.0/process-models/{test_process_group_id}/{process_model_dir_name}/process-instances/reports/sure?grade_level=1", - headers=self.logged_in_headers(user), + headers=self.logged_in_headers(with_super_admin_user), ) assert response.status_code == 404 data = json.loads(response.get_data(as_text=True)) @@ -1339,10 +1528,10 @@ class TestProcessApi(BaseTest): client: FlaskClient, process_group_id: str, process_model_id: str, - user: UserModel, + with_super_admin_user: UserModel, ) -> Any: """Setup_testing_instance.""" - headers = self.logged_in_headers(user) + headers = self.logged_in_headers(with_super_admin_user) response = self.create_process_instance( client, process_group_id, process_model_id, headers ) @@ -1352,15 +1541,18 @@ class TestProcessApi(BaseTest): return process_instance_id def test_error_handler( - 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: """Test_error_handler.""" process_group_id = "data" process_model_id = "error" - user = self.find_or_create_user() process_instance_id = self.setup_testing_instance( - client, process_group_id, process_model_id, user + client, process_group_id, process_model_id, with_super_admin_user ) process = ( @@ -1372,7 +1564,7 @@ class TestProcessApi(BaseTest): response = client.post( f"/v1.0/process-models/{process_group_id}/{process_model_id}/process-instances/{process_instance_id}/run", - headers=self.logged_in_headers(user), + headers=self.logged_in_headers(with_super_admin_user), ) assert response.status_code == 400 @@ -1391,15 +1583,18 @@ class TestProcessApi(BaseTest): assert process.status == "faulted" def test_error_handler_suspend( - 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: """Test_error_handler_suspend.""" process_group_id = "data" process_model_id = "error" - user = self.find_or_create_user() process_instance_id = self.setup_testing_instance( - client, process_group_id, process_model_id, user + client, process_group_id, process_model_id, with_super_admin_user ) process_model = ProcessModelService().get_process_model( process_model_id, process_group_id @@ -1418,7 +1613,7 @@ class TestProcessApi(BaseTest): response = client.post( f"/v1.0/process-models/{process_group_id}/{process_model_id}/process-instances/{process_instance_id}/run", - headers=self.logged_in_headers(user), + headers=self.logged_in_headers(with_super_admin_user), ) assert response.status_code == 400 @@ -1430,22 +1625,26 @@ class TestProcessApi(BaseTest): assert process.status == "suspended" def test_error_handler_with_email( - 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: """Test_error_handler.""" process_group_id = "data" process_model_id = "error" - user = self.find_or_create_user() process_instance_id = self.setup_testing_instance( - client, process_group_id, process_model_id, user + client, process_group_id, process_model_id, with_super_admin_user ) process_model = ProcessModelService().get_process_model( process_model_id, process_group_id ) ProcessModelService().update_spec( - process_model, {"exception_notification_addresses": ["user@example.com"]} + process_model, + {"exception_notification_addresses": ["with_super_admin_user@example.com"]}, ) mail = app.config["MAIL_APP"] @@ -1453,7 +1652,7 @@ class TestProcessApi(BaseTest): response = client.post( f"/v1.0/process-models/{process_group_id}/{process_model_id}/process-instances/{process_instance_id}/run", - headers=self.logged_in_headers(user), + headers=self.logged_in_headers(with_super_admin_user), ) assert response.status_code == 400 assert len(outbox) == 1 @@ -1472,7 +1671,11 @@ class TestProcessApi(BaseTest): assert process.status == "faulted" def test_process_model_file_create( - 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: """Test_process_model_file_create.""" process_group_id = "hello_world" @@ -1486,6 +1689,7 @@ class TestProcessApi(BaseTest): process_model_id=process_model_id, file_name=file_name, file_data=file_data, + user=with_super_admin_user, ) assert result["process_group_id"] == process_group_id assert result["process_model_id"] == process_model_id @@ -1493,7 +1697,11 @@ class TestProcessApi(BaseTest): assert bytes(str(result["file_contents"]), "utf-8") == file_data def test_can_get_message_instances_by_process_instance_id_and_without( - 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: """Test_can_get_message_instances_by_process_instance_id.""" load_test_spec( @@ -1501,7 +1709,6 @@ class TestProcessApi(BaseTest): process_model_source_directory="message_send_one_conversation", bpmn_file_name="message_receiver", ) - user = self.find_or_create_user() message_model_identifier = "message_send" payload = { "topica": "the_topica_string", @@ -1511,7 +1718,7 @@ class TestProcessApi(BaseTest): response = client.post( f"/v1.0/messages/{message_model_identifier}", content_type="application/json", - headers=self.logged_in_headers(user), + headers=self.logged_in_headers(with_super_admin_user), data=json.dumps({"payload": payload}), ) assert response.status_code == 200 @@ -1521,7 +1728,7 @@ class TestProcessApi(BaseTest): response = client.post( f"/v1.0/messages/{message_model_identifier}", content_type="application/json", - headers=self.logged_in_headers(user), + headers=self.logged_in_headers(with_super_admin_user), data=json.dumps({"payload": payload}), ) assert response.status_code == 200 @@ -1530,7 +1737,7 @@ class TestProcessApi(BaseTest): response = client.get( f"/v1.0/messages?process_instance_id={process_instance_id_one}", - headers=self.logged_in_headers(user), + headers=self.logged_in_headers(with_super_admin_user), ) assert response.status_code == 200 assert response.json is not None @@ -1542,7 +1749,7 @@ class TestProcessApi(BaseTest): response = client.get( f"/v1.0/messages?process_instance_id={process_instance_id_two}", - headers=self.logged_in_headers(user), + headers=self.logged_in_headers(with_super_admin_user), ) assert response.status_code == 200 assert response.json is not None @@ -1554,7 +1761,7 @@ class TestProcessApi(BaseTest): response = client.get( "/v1.0/messages", - headers=self.logged_in_headers(user), + headers=self.logged_in_headers(with_super_admin_user), ) assert response.status_code == 200 assert response.json is not None diff --git a/tests/spiffworkflow_backend/integration/test_secret_service.py b/tests/spiffworkflow_backend/integration/test_secret_service.py index a28d0a148..3cfc83a7c 100644 --- a/tests/spiffworkflow_backend/integration/test_secret_service.py +++ b/tests/spiffworkflow_backend/integration/test_secret_service.py @@ -48,6 +48,7 @@ class SecretServiceTestHelpers(BaseTest): process_model_id=self.test_process_model_id, process_model_display_name=self.test_process_model_display_name, process_model_description=self.test_process_model_description, + user=user, ) process_model_info = ProcessModelService().get_process_model( self.test_process_model_id, self.test_process_group_id @@ -58,118 +59,153 @@ class SecretServiceTestHelpers(BaseTest): class TestSecretService(SecretServiceTestHelpers): """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.""" - user = self.find_or_create_user() - test_secret = self.add_test_secret(user) + test_secret = self.add_test_secret(with_super_admin_user) assert test_secret is not None assert test_secret.key == self.test_key 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( - 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: """Test_add_secret_duplicate_key_fails.""" - user = self.find_or_create_user() - self.add_test_secret(user) + self.add_test_secret(with_super_admin_user) 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" - 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.""" - user = self.find_or_create_user() - self.add_test_secret(user) + self.add_test_secret(with_super_admin_user) secret = SecretService().get_secret(self.test_key) assert secret is not None assert secret.value == self.test_value 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: """Test_get_secret_bad_service.""" - user = self.find_or_create_user() - self.add_test_secret(user) + self.add_test_secret(with_super_admin_user) with pytest.raises(ApiError): SecretService().get_secret("bad_key") 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: """Test update secret.""" - user = self.find_or_create_user() - self.add_test_secret(user) + self.add_test_secret(with_super_admin_user) secret = SecretService.get_secret(self.test_key) assert secret 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) assert new_secret assert new_secret.value == "new_secret_value" # noqa: S105 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: """Test_update_secret_bad_user.""" - user = self.find_or_create_user() - self.add_test_secret(user) + self.add_test_secret(with_super_admin_user) with pytest.raises(ApiError) as ae: 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 assert ( 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( - 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: """Test_update_secret_bad_secret_fails.""" - user = self.find_or_create_user() - secret = self.add_test_secret(user) + secret = self.add_test_secret(with_super_admin_user) 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 ae.value.error_code == "update_secret_error" 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: """Test delete secret.""" - user = self.find_or_create_user() - self.add_test_secret(user) + self.add_test_secret(with_super_admin_user) secrets = SecretModel.query.all() assert len(secrets) == 1 - assert secrets[0].creator_user_id == user.id - SecretService.delete_secret(self.test_key, user.id) + assert secrets[0].creator_user_id == with_super_admin_user.id + SecretService.delete_secret(self.test_key, with_super_admin_user.id) secrets = SecretModel.query.all() assert len(secrets) == 0 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: """Test_delete_secret_bad_user.""" - user = self.find_or_create_user() - self.add_test_secret(user) + self.add_test_secret(with_super_admin_user) 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 ( - 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( - 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: """Test_delete_secret_bad_secret_fails.""" - user = self.find_or_create_user() - self.add_test_secret(user) + self.add_test_secret(with_super_admin_user) 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 @@ -177,19 +213,22 @@ class TestSecretServiceApi(SecretServiceTestHelpers): """TestSecretServiceApi.""" 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: """Test_add_secret.""" - user = self.find_or_create_user() secret_model = SecretModel( key=self.test_key, value=self.test_value, - creator_user_id=user.id, + creator_user_id=with_super_admin_user.id, ) data = json.dumps(SecretModelSchema().dump(secret_model)) response: TestResponse = client.post( "/v1.0/secrets", - headers=self.logged_in_headers(user), + headers=self.logged_in_headers(with_super_admin_user), content_type="application/json", data=data, ) @@ -199,17 +238,20 @@ class TestSecretServiceApi(SecretServiceTestHelpers): assert key in secret.keys() assert secret["key"] == self.test_key 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( - 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: """Test get secret.""" - user = self.find_or_create_user() - self.add_test_secret(user) + self.add_test_secret(with_super_admin_user) secret_response = client.get( 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.status_code == 200 @@ -217,20 +259,25 @@ class TestSecretServiceApi(SecretServiceTestHelpers): assert secret_response.json["value"] == self.test_value 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: """Test_update_secret.""" - user = self.find_or_create_user() - self.add_test_secret(user) + self.add_test_secret(with_super_admin_user) secret: Optional[SecretModel] = SecretService.get_secret(self.test_key) assert secret assert secret.value == self.test_value 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( 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", data=json.dumps(SecretModelSchema().dump(secret_model)), ) @@ -242,42 +289,61 @@ class TestSecretServiceApi(SecretServiceTestHelpers): assert secret_model.value == "new_secret_value" 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: """Test delete secret.""" - user = self.find_or_create_user() - self.add_test_secret(user) + self.add_test_secret(with_super_admin_user) secret = SecretService.get_secret(self.test_key) assert secret assert secret.value == self.test_value secret_response = client.delete( 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 with pytest.raises(ApiError): secret = SecretService.get_secret(self.test_key) 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: """Test_delete_secret_bad_user.""" user_1 = self.find_or_create_user() user_2 = self.find_or_create_user("test_user_2") 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( f"/v1.0/secrets/{self.test_key}", headers=self.logged_in_headers(user_2), ) 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( - 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: """Test delete secret.""" - user = self.find_or_create_user() secret_response = client.delete( "/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 diff --git a/tests/spiffworkflow_backend/unit/test_permission_target.py b/tests/spiffworkflow_backend/unit/test_permission_target.py index a2f222a42..567681428 100644 --- a/tests/spiffworkflow_backend/unit/test_permission_target.py +++ b/tests/spiffworkflow_backend/unit/test_permission_target.py @@ -13,10 +13,10 @@ from spiffworkflow_backend.models.permission_target import PermissionTargetModel class TestPermissionTarget(BaseTest): """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 ) -> 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/%") db.session.add(permission_target) db.session.commit() @@ -30,3 +30,13 @@ class TestPermissionTarget(BaseTest): assert ( 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/%"