From dbf6b7ae5a1c3650552002a1209f66bbf2b02f0c Mon Sep 17 00:00:00 2001 From: burnettk Date: Sat, 10 Dec 2022 23:39:01 -0500 Subject: [PATCH] Squashed 'spiffworkflow-backend/' changes from c1ecc9035..ecbe97049 ecbe97049 comment out test not working in CI 942c18fa3 indicate main explicitly for ci 3cd57a1c8 pyl w/ burnettk 5b2917a59 update staging configs to allow env var overrides w/ burnettk 7645a5f8d need to set upstream when git pushing w/ burnettk 0413f3cdf added secret verification to webhook endpoint w/ burnettk e7576d7ee fixed broken test w/ burnettk bf507f8fc fixed mypy issue w/ burnettk 7c6e5c306 Merge branch 'main' of github.com:sartography/spiff-arena 87c986df0 cleaned up the git service and expanded the api git hook w/ burnettk c7322f3c8 Sort primary file to top of files list (#71) 089d7d5e2 note aa6fe8286 add skeleton of endpoint to receive github webhooks 7b9e99ad9 some reorder w/ burnettk cullerton 2081532f4 mypy w/ burnettk cullerton ade7a41e7 added git creds for pushing on publish w/ burnettk cullerton 832d7c5c2 merged in main and resolved conflicts w/ burnettk cullerton 236e0e4ae favor os.path.join over hardcoding slash w/ burnettk 4ce7d6732 pyl passes b525e3523 added new notification component that allows links based on carbons w/ burnettk cullerton 1e9b2e9ab Return URL where they can view changes and open PR c8e8dae95 Clone into unique directory so we don't step on a previous publish that hasn't finished. c5e7e9153 make process metadata saving more resilient dbaa2c37c added frontend url as post redirect url in keycloak w/ burnettk 348595c19 syntax fix w/ burnettk bbcfd5213 remove staging py config file in favor of terraform configs w/ burnettk b150a8a69 moved some configs from deploy scripts to terraform env config w/ burnettk acf641962 rename terraform configs from rb to py w/ burnettk 862937525 Add comment about the new environment variable 655e01a48 Merge branch 'main' into feature/create_containers f580cadb2 Merge branch 'main' into feature/git-integration e241ed5cd updated terraform permissions to match development better w/ burnettk d102a4c84 Merging this unfinished test so I don't lose it. It doesn't test anything yet. Need to deal w/ a bunch of mock stuff b5aecb072 Merge pull request #69 from sartography/feature/api_permission_cleanup a0f351d16 Merge pull request #65 from sartography/new_report 88ad3b99a typing da082e369 Merge branch 'main' into feature/git-integration a1fe0193d First pass at git integration 1e029f427 break process instance log list page into two tabs, simple and detailed 11b994a81 Merge remote-tracking branch 'origin/main' into feature/api_permission_cleanup b1199530d update cors handling to make it more efficient ed6a4d8c9 more api cleanup w/ burnettk b19825e17 cleaned up more api routes for permissions w/ burnettk 4b5f5d369 updated tasks endpoint to task-data for easier permission setting w/ burnettk 460cfb4e4 pyl w/ burnettk 85509c3d4 fixing an untyped method. a7eeaa6d9 running py_pl -- mainly reordering imports. c9e3e8d31 Merge remote-tracking branch 'origin/main' into feature/create_containers 786785693 Setting things up so it's easy to switch databases using a local configuration file (still works with environment variables) Swtiched from a "joinedload" to a "selectinload" which removes a problem with groupby columns in Postgres and sqlite. (https://docs.sqlalchemy.org/en/14/orm/loading_relationships.html#selectin-eager-loading) f3760380e added support to order reports by given column and metadata headers w/ burnettk 0425293fd Fixes based off KB's super kind review. ------- * Remove unnecessary packages from dockerfile for the demo-connect proxy. * Rename an environment variable that mentioned Status.im in what is now a generic connector. * Fixed a spelling mistake. bee4ea107 bump nox stuff and spiff d7ff963da test for automatic saving of process instance metadata on instance save fe7c4c923 remove dup test process model eb2f64518 add order_by to make this query deterministic 9b2a40c87 lint ceec518c9 add extraction, needs test 9af4cf3f3 fix tests 452c64cbf Merge remote-tracking branch 'origin/main' into new_report 809d8bfbf using an array for metadata extraction paths now instead of dictionaries w/ burnettk 49b02a9c2 added some support to add process model metadata. need to fix frontend w/ burnettk d2d0cb0b1 getting the collect hit_policy to work correctly. a3f4c2f48 filtering by metadata works w/ burnettk f87d266a4 favor report id over identifier but support both and ui updates to allow setting a condition value on a metadata field, changing the display name, and fixes for saving and updating a report 52b288bd4 I can't say I love flake8. Removing dependency on rust (monkeytype) 3534b1896 fixing some typing issues, white space, etal... 389069fde added ability to update the display name for perspective columns w/ burnettk d9b36f5e1 fixing some typing issues. 51ecd25fb Reorder config imports so that instance config is dead last - and can override everything else. Updated docker-compose for running a demo. run_pyl fixes d7308ef67 Adding a demo permissions file. 8ade069dd A little cleanup of the ui Don't check authorization on static assets Do not require unique username on user table (uniqueness check is on the service and service id composite.) 1ebe6e108 some updates for process instance reports and metadata w/ burnettk 321fc0e75 Use the "well-known" configuration dictionary from openid to get the url endpoints, rather than trying to configure or guess the correct endpoint urls. 77c79e58b added correlations to message list table w/ burnettk 4df885fd3 Not all open id systems have realms like KeyCloak does -- so removing this in favor of setting just one value - which is the base url of the openid system -- which will work across all openid systems. d4ad18ab9 Adding a blueprint for openid - a very lightweight embedded authentication system to make it eaiser to try out SpiffWorkflow when you don't have openID set up with Google etal. Removing all calls to open id's user_info endpoint - as these are unncessiary. Adding a users section to the permission files -- so we can handle all user/group/permissions in one file when needed. There was a very confusing is_admin function on the user model that needed killin. 9669f2898 removed file named ':' 341fb8cf5 added api to get list of process report columns a6002ebac finished base for metadata reporting w/ burnettk 8e9828661 some cleanup for metadata w/ burnettk 91ea7f4b4 metadat reports work w/ burnettk 2571eb656 WIP more metadata reporting w/ burnettk a2dbee712 WIP more metadata reporting w/ burnettk a0081bdf7 some basics to add metadata to reports w/ burnettk cullerton fcc5bce3e fixed issue ensuring active tasks are up to date w/ burnettk cullerton 3ef318e61 added script to save process instance metadata and fixed permissions issue w/ burnettk cullerton 46038eae3 only delete active tasks if needed w/ burnettk cullerton 3b6255147 Merge pull request #64 from sartography/group_query 3abdf8872 get rid of Project Lead group ded78d9fe Getting ./bin/pyl to pass c51e56e9b Query fix with the gang 38c37b927 everybody gets access to read processes e2a3d67eb put sasha and manuchehr in demo group 61a2d858e upgrade keycloak 47b551b8a make sasha, harmeet, manuchehr, and admin have desired permissions for demo 3ecfe6ae4 add demo group access to the customer-contracts to give us 5 tiles for the dmeo. 327981e63 adding the username to the report tables c90a0a330 Performance improvement in listing all process models. d32575033 Don't create a processor for every thing all the time. 2f639352a Getting ./bin/pyl to pass c8f561159 Merge branch 'main' into task_json fe5f9995b Just save task_json 8f2784828 no global read since that gives configuration as well e351f033e do not allow starting vendor invoice approval process for demo users 2e97efdbb Change permissions per request -- want everyone to have access to 4 process models. ff63418ca Change permissions per request -- want everyone to have access to 4 process models. 6b4bcfd34 Merge branch 'main' of github.com:sartography/spiff-arena into main bc6d80513 Change permissions changed from vendor to core-contributor for everyone. dfed48057 upgrade keycloak 0f0f56e45 display name instead of id, margin under table sections, Download xml to Download 9af377307 lint fe5d2fdec exclude instances you started in with_tasks_completed_by_me bb76eeee5 lint e30f12fab in completed tab, show display name, and filter by all stopped statuses rather than just complete 57a720c97 tests passing dc0665c26 gotta fix usage of is_model 63b654c22 working on tests aa4277d98 oh my god why were these ever instance methods 875bd1576 remove column 6081f6637 renames aa2333f27 start adding display name and removing garbage a9d5e121b add fin1, lead1, and Tasks actioned by me to Tasks completed by me e32c3830b Fix the postgres job (#61) adcfc53f4 Fix for updating xero token in the background (#60) bd4f5d247 updated breadcrumb to use display name w/ burnettk 6b94bb260 update permissions per feedback in demo 71c1f7974 rename process_groups_list to process_group_list and fix lint d4a1d0552 add perm to process instance report list 4f78b1958 some more perm updates for core user w/ burnettk 8fb5fd3c3 Merge branch 'main' of github.com:sartography/spiff-arena b43a06f51 give core user access to instance tasks w/ burnettk 06ba17be1 Start of system report filters (#57) 213221214 Update oauth redirect url (#58) 75d3c0cea filter process models based on user permissions on the backend if specified w/ burnettk acc88b9a9 created new users for keycloak and fixed some permissions for core user w/ burnettk cullerton 6eb24f10c Allow switching between user defined reports (#56) d7cb24a56 fixed broken test 8f9d918ef added recursive option to process model list to recurse or not and fix some ui components 2b47eb7f5 upgrade apscheduler and fix mispelling 2f312e2ab pyl is passing 4fedc046d Merge branch 'main' of https://github.com/sartography/spiff-arena e6230cb79 pyl is passing w/ burnettk e5f7f5096 added a script to add a user to a group w/ burnettk 33897da2c mypy 7a9954d74 sort process groups by display name w/ burnettk cullerton 7e9331dd3 Merge branch 'main' into move-group-or-model d87f47aaa allow getting all process models, process instances should not save when they are initialized, and fixed some cypress tests w/ burnettk 8ffd6203c use correct separator. de7ebb6f5 Merge branch 'main' of github.com:sartography/spiff-arena into main bf6645960 Remove MoneyType from dependencies to avoid a new dependency on a rust complier when upgrading to pytno 11. 9455366f5 lint 7866730f2 see if this solves the windows build problem 98e07cc85 add id_for_file_path helper method for process groups fc476e3cc run_pyl 72da67f00 eliminate security issue by removing py 818bce07e update mysql-connector-python 37444f8bf asst syntax cleanup 7bde5d8cc mypy ea82bf583 removed print statement and unused line of code 9717fb112 test for move model d07114593 move model 38dc4570d process group move api endpoint git-subtree-dir: spiffworkflow-backend git-subtree-split: ecbe97049cbf46d1d7d240cb80051290de56fa7d --- .flake8 | 2 + .github/workflows/constraints.txt | 4 +- bin/import_tickets_for_command_line.py | 2 +- bin/import_tickets_for_script_task.py | 2 +- bin/save_all_bpmn.py | 4 +- bin/spiffworkflow-realm.json | 5977 ++++++++--------- bin/start_keycloak | 2 +- conftest.py | 2 +- keycloak/Dockerfile | 2 +- migrations/env.py | 2 - .../{70223f5c7b98_.py => 4d75421c0af0_.py} | 57 +- poetry.lock | 127 +- pyproject.toml | 7 +- src/spiffworkflow_backend/__init__.py | 13 +- src/spiffworkflow_backend/api.yml | 313 +- src/spiffworkflow_backend/config/__init__.py | 50 +- src/spiffworkflow_backend/config/default.py | 25 +- src/spiffworkflow_backend/config/demo.py | 4 +- src/spiffworkflow_backend/config/dev.py | 8 + .../config/development.py | 7 + .../config/permissions/development.yml | 168 +- .../config/permissions/example.yml | 88 + .../terraform_deployed_environment.yml | 153 +- src/spiffworkflow_backend/config/staging.py | 9 +- .../config/terraform_deployed_environment.py | 29 + .../config/terraform_deployed_environment.rb | 16 - .../load_database_models.py | 3 + .../message_triggerable_process_model.py | 2 - .../models/process_group.py | 9 +- .../models/process_instance.py | 106 +- .../models/process_instance_metadata.py | 30 + .../models/process_instance_report.py | 72 +- .../models/process_model.py | 11 +- .../models/spiff_step_details.py | 5 +- src/spiffworkflow_backend/models/user.py | 7 +- .../routes/admin_blueprint/admin_blueprint.py | 37 +- ...oups_list.html => process_group_list.html} | 0 .../templates/process_group_show.html | 2 +- .../routes/openid_blueprint/__init__.py | 1 + .../openid_blueprint/openid_blueprint.py | 153 + .../routes/openid_blueprint/static/login.css | 112 + .../routes/openid_blueprint/static/logo.png | Bin 0 -> 10138 bytes .../openid_blueprint/static/logo_small.png | Bin 0 -> 5000 bytes .../openid_blueprint/templates/login.html | 36 + .../routes/process_api_blueprint.py | 445 +- src/spiffworkflow_backend/routes/user.py | 40 +- .../scripts/add_user_to_group.py | 43 + .../scripts/save_process_instance_metadata.py | 42 + .../services/acceptance_test_fixtures.py | 22 +- .../services/authentication_service.py | 145 +- .../services/authorization_service.py | 47 +- .../services/background_processing_service.py | 2 +- .../services/data_setup_service.py | 2 +- .../services/error_handling_service.py | 2 +- .../services/file_system_service.py | 28 +- .../services/git_service.py | 250 +- .../services/logging_service.py | 4 +- .../services/message_service.py | 2 +- .../services/process_instance_processor.py | 158 +- .../process_instance_report_service.py | 141 +- .../services/process_instance_service.py | 50 +- .../services/process_model_service.py | 319 +- .../services/secret_service.py | 24 +- .../services/service_task_service.py | 4 +- .../services/spec_file_service.py | 6 +- tests/data/hello_world/hello_world.bpmn | 6 +- .../nested-task-data-structure.bpmn | 56 + .../save_process_instance_metadata.bpmn | 52 + .../helpers/base_test.py | 31 +- .../helpers/example_data.py | 6 +- .../integration/test_logging_service.py | 4 +- .../integration/test_nested_groups.py | 2 +- .../integration/test_openid_blueprint.py | 61 + .../integration/test_process_api.py | 607 +- .../integration/test_secret_service.py | 2 +- .../test_save_process_instance_metadata.py | 45 + .../unit/test_acceptance_test_fixtures.py | 25 + .../unit/test_authorization_service.py | 2 +- .../unit/test_message_instance.py | 8 +- .../unit/test_message_service.py | 4 +- .../unit/test_process_group.py | 5 +- .../unit/test_process_instance_processor.py | 40 + .../unit/test_process_instance_report.py | 10 +- .../unit/test_process_model.py | 51 + .../unit/test_process_model_service.py | 2 +- .../unit/test_spec_file_service.py | 2 +- .../unit/test_various_bpmn_constructs.py | 2 +- 87 files changed, 6218 insertions(+), 4240 deletions(-) rename migrations/versions/{70223f5c7b98_.py => 4d75421c0af0_.py} (92%) create mode 100644 src/spiffworkflow_backend/config/dev.py create mode 100644 src/spiffworkflow_backend/config/permissions/example.yml create mode 100644 src/spiffworkflow_backend/config/terraform_deployed_environment.py delete mode 100644 src/spiffworkflow_backend/config/terraform_deployed_environment.rb create mode 100644 src/spiffworkflow_backend/models/process_instance_metadata.py rename src/spiffworkflow_backend/routes/admin_blueprint/templates/{process_groups_list.html => process_group_list.html} (100%) create mode 100644 src/spiffworkflow_backend/routes/openid_blueprint/__init__.py create mode 100644 src/spiffworkflow_backend/routes/openid_blueprint/openid_blueprint.py create mode 100644 src/spiffworkflow_backend/routes/openid_blueprint/static/login.css create mode 100644 src/spiffworkflow_backend/routes/openid_blueprint/static/logo.png create mode 100644 src/spiffworkflow_backend/routes/openid_blueprint/static/logo_small.png create mode 100644 src/spiffworkflow_backend/routes/openid_blueprint/templates/login.html create mode 100644 src/spiffworkflow_backend/scripts/add_user_to_group.py create mode 100644 src/spiffworkflow_backend/scripts/save_process_instance_metadata.py create mode 100644 tests/data/nested-task-data-structure/nested-task-data-structure.bpmn create mode 100644 tests/data/save_process_instance_metadata/save_process_instance_metadata.bpmn create mode 100644 tests/spiffworkflow_backend/integration/test_openid_blueprint.py create mode 100644 tests/spiffworkflow_backend/scripts/test_save_process_instance_metadata.py diff --git a/.flake8 b/.flake8 index 16f7c559..1cc09c97 100644 --- a/.flake8 +++ b/.flake8 @@ -27,3 +27,5 @@ per-file-ignores = # this file overwrites methods from the logging library so we can't change them # and ignore long comment line src/spiffworkflow_backend/services/logging_service.py:N802,B950 + + tests/spiffworkflow_backend/integration/test_process_api.py:S607,S101,D103,S605 diff --git a/.github/workflows/constraints.txt b/.github/workflows/constraints.txt index 70c8f365..7ccc8711 100644 --- a/.github/workflows/constraints.txt +++ b/.github/workflows/constraints.txt @@ -1,5 +1,5 @@ pip==22.2.2 -nox==2022.8.7 -nox-poetry==1.0.1 +nox==2022.11.21 +nox-poetry==1.0.2 poetry==1.2.2 virtualenv==20.16.5 diff --git a/bin/import_tickets_for_command_line.py b/bin/import_tickets_for_command_line.py index 8b145dc4..e193b599 100644 --- a/bin/import_tickets_for_command_line.py +++ b/bin/import_tickets_for_command_line.py @@ -74,7 +74,7 @@ def main(): print(f"ticket_identifier: {ticket_identifier}") print(f"priority: {priority}") - process_instance = ProcessInstanceService.create_process_instance( + process_instance = ProcessInstanceService.create_process_instance_from_process_model_identifier( process_model_identifier_ticket, user, process_group_identifier="sartography-admin", diff --git a/bin/import_tickets_for_script_task.py b/bin/import_tickets_for_script_task.py index f8977978..6b12699b 100644 --- a/bin/import_tickets_for_script_task.py +++ b/bin/import_tickets_for_script_task.py @@ -68,7 +68,7 @@ def main(): print(f"priority: {priority}") # if there is no month, who cares about it. if month: - process_instance = ProcessInstanceService.create_process_instance( + process_instance = ProcessInstanceService.create_process_instance_from_process_model_identifier( process_model_identifier=process_model_identifier_ticket, user=user, process_group_identifier="sartography-admin", diff --git a/bin/save_all_bpmn.py b/bin/save_all_bpmn.py index 54a40841..fd44bb54 100644 --- a/bin/save_all_bpmn.py +++ b/bin/save_all_bpmn.py @@ -1,13 +1,13 @@ """Grabs tickets from csv and makes process instances.""" import os -from spiffworkflow_backend import get_hacked_up_app_for_script +from spiffworkflow_backend import create_app from spiffworkflow_backend.services.data_setup_service import DataSetupService def main() -> None: """Main.""" - app = get_hacked_up_app_for_script() + app = create_app() with app.app_context(): failing_process_models = DataSetupService.save_all_process_models() for bpmn_errors in failing_process_models: diff --git a/bin/spiffworkflow-realm.json b/bin/spiffworkflow-realm.json index 4b37f289..a30f53c1 100644 --- a/bin/spiffworkflow-realm.json +++ b/bin/spiffworkflow-realm.json @@ -1,3182 +1,2871 @@ { - "id": "spiffworkflow", - "realm": "spiffworkflow", - "notBefore": 0, - "defaultSignatureAlgorithm": "RS256", - "revokeRefreshToken": false, - "refreshTokenMaxReuse": 0, - "accessTokenLifespan": 1800, - "accessTokenLifespanForImplicitFlow": 900, - "ssoSessionIdleTimeout": 86400, - "ssoSessionMaxLifespan": 864000, - "ssoSessionIdleTimeoutRememberMe": 0, - "ssoSessionMaxLifespanRememberMe": 0, - "offlineSessionIdleTimeout": 2592000, - "offlineSessionMaxLifespanEnabled": false, - "offlineSessionMaxLifespan": 5184000, - "clientSessionIdleTimeout": 0, - "clientSessionMaxLifespan": 0, - "clientOfflineSessionIdleTimeout": 0, - "clientOfflineSessionMaxLifespan": 0, - "accessCodeLifespan": 60, - "accessCodeLifespanUserAction": 300, - "accessCodeLifespanLogin": 1800, - "actionTokenGeneratedByAdminLifespan": 43200, - "actionTokenGeneratedByUserLifespan": 300, - "oauth2DeviceCodeLifespan": 600, - "oauth2DevicePollingInterval": 5, - "enabled": true, - "sslRequired": "external", - "registrationAllowed": false, - "registrationEmailAsUsername": false, - "rememberMe": false, - "verifyEmail": false, - "loginWithEmailAllowed": true, - "duplicateEmailsAllowed": false, - "resetPasswordAllowed": false, - "editUsernameAllowed": false, - "bruteForceProtected": false, - "permanentLockout": false, - "maxFailureWaitSeconds": 900, - "minimumQuickLoginWaitSeconds": 60, - "waitIncrementSeconds": 60, - "quickLoginCheckMilliSeconds": 1000, - "maxDeltaTimeSeconds": 43200, - "failureFactor": 30, - "roles": { - "realm": [ - { - "id": "c9f0ff93-642d-402b-965a-04d70719886b", - "name": "default-roles-spiffworkflow", - "description": "${role_default-roles}", - "composite": true, - "composites": { - "realm": ["offline_access", "uma_authorization"], - "client": { - "account": ["view-profile", "manage-account"] + "id" : "spiffworkflow", + "realm" : "spiffworkflow", + "notBefore" : 0, + "defaultSignatureAlgorithm" : "RS256", + "revokeRefreshToken" : false, + "refreshTokenMaxReuse" : 0, + "accessTokenLifespan" : 1800, + "accessTokenLifespanForImplicitFlow" : 900, + "ssoSessionIdleTimeout" : 86400, + "ssoSessionMaxLifespan" : 864000, + "ssoSessionIdleTimeoutRememberMe" : 0, + "ssoSessionMaxLifespanRememberMe" : 0, + "offlineSessionIdleTimeout" : 2592000, + "offlineSessionMaxLifespanEnabled" : false, + "offlineSessionMaxLifespan" : 5184000, + "clientSessionIdleTimeout" : 0, + "clientSessionMaxLifespan" : 0, + "clientOfflineSessionIdleTimeout" : 0, + "clientOfflineSessionMaxLifespan" : 0, + "accessCodeLifespan" : 60, + "accessCodeLifespanUserAction" : 300, + "accessCodeLifespanLogin" : 1800, + "actionTokenGeneratedByAdminLifespan" : 43200, + "actionTokenGeneratedByUserLifespan" : 300, + "oauth2DeviceCodeLifespan" : 600, + "oauth2DevicePollingInterval" : 5, + "enabled" : true, + "sslRequired" : "external", + "registrationAllowed" : false, + "registrationEmailAsUsername" : false, + "rememberMe" : false, + "verifyEmail" : false, + "loginWithEmailAllowed" : true, + "duplicateEmailsAllowed" : false, + "resetPasswordAllowed" : false, + "editUsernameAllowed" : false, + "bruteForceProtected" : false, + "permanentLockout" : false, + "maxFailureWaitSeconds" : 900, + "minimumQuickLoginWaitSeconds" : 60, + "waitIncrementSeconds" : 60, + "quickLoginCheckMilliSeconds" : 1000, + "maxDeltaTimeSeconds" : 43200, + "failureFactor" : 30, + "roles" : { + "realm" : [ { + "id" : "c9f0ff93-642d-402b-965a-04d70719886b", + "name" : "default-roles-spiffworkflow", + "description" : "${role_default-roles}", + "composite" : true, + "composites" : { + "realm" : [ "offline_access", "uma_authorization" ], + "client" : { + "account" : [ "view-profile", "manage-account" ] + } + }, + "clientRole" : false, + "containerId" : "spiffworkflow", + "attributes" : { } + }, { + "id" : "9f474167-5707-4c10-8f9e-bb54ec715cd3", + "name" : "uma_authorization", + "description" : "${role_uma_authorization}", + "composite" : false, + "clientRole" : false, + "containerId" : "spiffworkflow", + "attributes" : { } + }, { + "id" : "6738d143-2d1d-4458-8a98-01ea003fde14", + "name" : "admin", + "composite" : false, + "clientRole" : false, + "containerId" : "spiffworkflow", + "attributes" : { } + }, { + "id" : "6cbcdea5-0083-469d-9576-1d245fb3cdfd", + "name" : "repeat-form-role-realm", + "composite" : false, + "clientRole" : false, + "containerId" : "spiffworkflow", + "attributes" : { } + }, { + "id" : "b5a92aee-82d2-4687-8282-365df4df21a9", + "name" : "offline_access", + "description" : "${role_offline-access}", + "composite" : false, + "clientRole" : false, + "containerId" : "spiffworkflow", + "attributes" : { } + } ], + "client" : { + "realm-management" : [ { + "id" : "257c348c-4b9e-4fea-be39-5fdd28e8bb93", + "name" : "manage-authorization", + "description" : "${role_manage-authorization}", + "composite" : false, + "clientRole" : true, + "containerId" : "4ce68130-aced-4e67-936a-8082dc843cc2", + "attributes" : { } + }, { + "id" : "1d224265-63a8-40ea-9316-47627d0aed8c", + "name" : "view-authorization", + "description" : "${role_view-authorization}", + "composite" : false, + "clientRole" : true, + "containerId" : "4ce68130-aced-4e67-936a-8082dc843cc2", + "attributes" : { } + }, { + "id" : "535d7ca0-0f06-42d8-938b-e6e7aabffb42", + "name" : "query-groups", + "description" : "${role_query-groups}", + "composite" : false, + "clientRole" : true, + "containerId" : "4ce68130-aced-4e67-936a-8082dc843cc2", + "attributes" : { } + }, { + "id" : "9ff52ab5-2558-4cb0-901f-6e6f1469d075", + "name" : "realm-admin", + "description" : "${role_realm-admin}", + "composite" : true, + "composites" : { + "client" : { + "realm-management" : [ "manage-authorization", "view-authorization", "query-groups", "view-clients", "view-realm", "manage-users", "query-users", "impersonation", "manage-clients", "view-identity-providers", "create-client", "query-realms", "view-users", "view-events", "manage-identity-providers", "manage-events", "query-clients", "manage-realm" ] } }, - "clientRole": false, - "containerId": "spiffworkflow", - "attributes": {} - }, - { - "id": "9f474167-5707-4c10-8f9e-bb54ec715cd3", - "name": "uma_authorization", - "description": "${role_uma_authorization}", - "composite": false, - "clientRole": false, - "containerId": "spiffworkflow", - "attributes": {} - }, - { - "id": "6738d143-2d1d-4458-8a98-01ea003fde14", - "name": "admin", - "composite": false, - "clientRole": false, - "containerId": "spiffworkflow", - "attributes": {} - }, - { - "id": "6cbcdea5-0083-469d-9576-1d245fb3cdfd", - "name": "repeat-form-role-realm", - "composite": false, - "clientRole": false, - "containerId": "spiffworkflow", - "attributes": {} - }, - { - "id": "b5a92aee-82d2-4687-8282-365df4df21a9", - "name": "offline_access", - "description": "${role_offline-access}", - "composite": false, - "clientRole": false, - "containerId": "spiffworkflow", - "attributes": {} - } - ], - "client": { - "realm-management": [ - { - "id": "257c348c-4b9e-4fea-be39-5fdd28e8bb93", - "name": "manage-authorization", - "description": "${role_manage-authorization}", - "composite": false, - "clientRole": true, - "containerId": "4ce68130-aced-4e67-936a-8082dc843cc2", - "attributes": {} - }, - { - "id": "1d224265-63a8-40ea-9316-47627d0aed8c", - "name": "view-authorization", - "description": "${role_view-authorization}", - "composite": false, - "clientRole": true, - "containerId": "4ce68130-aced-4e67-936a-8082dc843cc2", - "attributes": {} - }, - { - "id": "535d7ca0-0f06-42d8-938b-e6e7aabffb42", - "name": "query-groups", - "description": "${role_query-groups}", - "composite": false, - "clientRole": true, - "containerId": "4ce68130-aced-4e67-936a-8082dc843cc2", - "attributes": {} - }, - { - "id": "9ff52ab5-2558-4cb0-901f-6e6f1469d075", - "name": "realm-admin", - "description": "${role_realm-admin}", - "composite": true, - "composites": { - "client": { - "realm-management": [ - "manage-authorization", - "view-authorization", - "query-groups", - "view-clients", - "view-realm", - "manage-users", - "query-users", - "impersonation", - "manage-clients", - "view-identity-providers", - "create-client", - "query-realms", - "view-users", - "view-events", - "manage-identity-providers", - "manage-events", - "query-clients", - "manage-realm" - ] - } - }, - "clientRole": true, - "containerId": "4ce68130-aced-4e67-936a-8082dc843cc2", - "attributes": {} - }, - { - "id": "98db35e3-833f-4b61-83af-fc50484fda57", - "name": "view-clients", - "description": "${role_view-clients}", - "composite": true, - "composites": { - "client": { - "realm-management": ["query-clients"] - } - }, - "clientRole": true, - "containerId": "4ce68130-aced-4e67-936a-8082dc843cc2", - "attributes": {} - }, - { - "id": "e0dc0e0c-eba4-4de7-b2eb-2ba095c4c6d4", - "name": "manage-users", - "description": "${role_manage-users}", - "composite": false, - "clientRole": true, - "containerId": "4ce68130-aced-4e67-936a-8082dc843cc2", - "attributes": {} - }, - { - "id": "69ce3805-1897-4291-842b-b8e8e9f29bd7", - "name": "view-realm", - "description": "${role_view-realm}", - "composite": false, - "clientRole": true, - "containerId": "4ce68130-aced-4e67-936a-8082dc843cc2", - "attributes": {} - }, - { - "id": "3e803641-96b1-44d8-9de5-7dee83a0a75b", - "name": "impersonation", - "description": "${role_impersonation}", - "composite": false, - "clientRole": true, - "containerId": "4ce68130-aced-4e67-936a-8082dc843cc2", - "attributes": {} - }, - { - "id": "2c92c3e5-1a0a-4318-9b63-617c5dca0b66", - "name": "query-users", - "description": "${role_query-users}", - "composite": false, - "clientRole": true, - "containerId": "4ce68130-aced-4e67-936a-8082dc843cc2", - "attributes": {} - }, - { - "id": "326a3718-390d-4e41-af00-2197d3ef6858", - "name": "manage-clients", - "description": "${role_manage-clients}", - "composite": false, - "clientRole": true, - "containerId": "4ce68130-aced-4e67-936a-8082dc843cc2", - "attributes": {} - }, - { - "id": "e4c69181-5e0d-484e-ac31-be6beef57c28", - "name": "create-client", - "description": "${role_create-client}", - "composite": false, - "clientRole": true, - "containerId": "4ce68130-aced-4e67-936a-8082dc843cc2", - "attributes": {} - }, - { - "id": "f4ac66cc-97b4-4590-beae-5ff23c9935b3", - "name": "query-realms", - "description": "${role_query-realms}", - "composite": false, - "clientRole": true, - "containerId": "4ce68130-aced-4e67-936a-8082dc843cc2", - "attributes": {} - }, - { - "id": "a24704fe-13fd-40e6-bf2d-29014f63c069", - "name": "view-identity-providers", - "description": "${role_view-identity-providers}", - "composite": false, - "clientRole": true, - "containerId": "4ce68130-aced-4e67-936a-8082dc843cc2", - "attributes": {} - }, - { - "id": "7deec87c-2716-40c1-a115-2a0fe840b119", - "name": "view-users", - "description": "${role_view-users}", - "composite": true, - "composites": { - "client": { - "realm-management": ["query-groups", "query-users"] - } - }, - "clientRole": true, - "containerId": "4ce68130-aced-4e67-936a-8082dc843cc2", - "attributes": {} - }, - { - "id": "827c40ae-b4c2-4574-9f34-db33925cd19c", - "name": "view-events", - "description": "${role_view-events}", - "composite": false, - "clientRole": true, - "containerId": "4ce68130-aced-4e67-936a-8082dc843cc2", - "attributes": {} - }, - { - "id": "cbe05c62-2b07-4ac7-a33a-ffca7c176252", - "name": "manage-events", - "description": "${role_manage-events}", - "composite": false, - "clientRole": true, - "containerId": "4ce68130-aced-4e67-936a-8082dc843cc2", - "attributes": {} - }, - { - "id": "8ca56814-a817-4849-a515-45399eb1dcc1", - "name": "manage-identity-providers", - "description": "${role_manage-identity-providers}", - "composite": false, - "clientRole": true, - "containerId": "4ce68130-aced-4e67-936a-8082dc843cc2", - "attributes": {} - }, - { - "id": "1134c6df-d0ff-498d-9dc4-ad989f7cfe93", - "name": "query-clients", - "description": "${role_query-clients}", - "composite": false, - "clientRole": true, - "containerId": "4ce68130-aced-4e67-936a-8082dc843cc2", - "attributes": {} - }, - { - "id": "3bb14549-60f6-4078-8f4e-47a1162412f2", - "name": "manage-realm", - "description": "${role_manage-realm}", - "composite": false, - "clientRole": true, - "containerId": "4ce68130-aced-4e67-936a-8082dc843cc2", - "attributes": {} - } - ], - "spiffworkflow-frontend": [], - "security-admin-console": [], - "admin-cli": [], - "spiffworkflow-backend": [ - { - "id": "4d71d1bb-d627-43c8-bc07-d542f816e04b", - "name": "spiffworkflow-admin", - "composite": false, - "clientRole": true, - "containerId": "f44558af-3601-4e54-b854-08396a247544", - "attributes": {} - }, - { - "id": "2341ca1c-24c8-4ddf-874c-7153c9408068", - "name": "uma_protection", - "composite": false, - "clientRole": true, - "containerId": "f44558af-3601-4e54-b854-08396a247544", - "attributes": {} - }, - { - "id": "cf88054e-4bdc-491c-bf93-c660cdaad72d", - "name": "repeat-form-role-2", - "composite": false, - "clientRole": true, - "containerId": "f44558af-3601-4e54-b854-08396a247544", - "attributes": { - "repeat-form-role-2-att-key": ["repeat-form-role-2-att-value"] + "clientRole" : true, + "containerId" : "4ce68130-aced-4e67-936a-8082dc843cc2", + "attributes" : { } + }, { + "id" : "98db35e3-833f-4b61-83af-fc50484fda57", + "name" : "view-clients", + "description" : "${role_view-clients}", + "composite" : true, + "composites" : { + "client" : { + "realm-management" : [ "query-clients" ] } + }, + "clientRole" : true, + "containerId" : "4ce68130-aced-4e67-936a-8082dc843cc2", + "attributes" : { } + }, { + "id" : "e0dc0e0c-eba4-4de7-b2eb-2ba095c4c6d4", + "name" : "manage-users", + "description" : "${role_manage-users}", + "composite" : false, + "clientRole" : true, + "containerId" : "4ce68130-aced-4e67-936a-8082dc843cc2", + "attributes" : { } + }, { + "id" : "69ce3805-1897-4291-842b-b8e8e9f29bd7", + "name" : "view-realm", + "description" : "${role_view-realm}", + "composite" : false, + "clientRole" : true, + "containerId" : "4ce68130-aced-4e67-936a-8082dc843cc2", + "attributes" : { } + }, { + "id" : "3e803641-96b1-44d8-9de5-7dee83a0a75b", + "name" : "impersonation", + "description" : "${role_impersonation}", + "composite" : false, + "clientRole" : true, + "containerId" : "4ce68130-aced-4e67-936a-8082dc843cc2", + "attributes" : { } + }, { + "id" : "2c92c3e5-1a0a-4318-9b63-617c5dca0b66", + "name" : "query-users", + "description" : "${role_query-users}", + "composite" : false, + "clientRole" : true, + "containerId" : "4ce68130-aced-4e67-936a-8082dc843cc2", + "attributes" : { } + }, { + "id" : "326a3718-390d-4e41-af00-2197d3ef6858", + "name" : "manage-clients", + "description" : "${role_manage-clients}", + "composite" : false, + "clientRole" : true, + "containerId" : "4ce68130-aced-4e67-936a-8082dc843cc2", + "attributes" : { } + }, { + "id" : "e4c69181-5e0d-484e-ac31-be6beef57c28", + "name" : "create-client", + "description" : "${role_create-client}", + "composite" : false, + "clientRole" : true, + "containerId" : "4ce68130-aced-4e67-936a-8082dc843cc2", + "attributes" : { } + }, { + "id" : "f4ac66cc-97b4-4590-beae-5ff23c9935b3", + "name" : "query-realms", + "description" : "${role_query-realms}", + "composite" : false, + "clientRole" : true, + "containerId" : "4ce68130-aced-4e67-936a-8082dc843cc2", + "attributes" : { } + }, { + "id" : "a24704fe-13fd-40e6-bf2d-29014f63c069", + "name" : "view-identity-providers", + "description" : "${role_view-identity-providers}", + "composite" : false, + "clientRole" : true, + "containerId" : "4ce68130-aced-4e67-936a-8082dc843cc2", + "attributes" : { } + }, { + "id" : "7deec87c-2716-40c1-a115-2a0fe840b119", + "name" : "view-users", + "description" : "${role_view-users}", + "composite" : true, + "composites" : { + "client" : { + "realm-management" : [ "query-groups", "query-users" ] + } + }, + "clientRole" : true, + "containerId" : "4ce68130-aced-4e67-936a-8082dc843cc2", + "attributes" : { } + }, { + "id" : "827c40ae-b4c2-4574-9f34-db33925cd19c", + "name" : "view-events", + "description" : "${role_view-events}", + "composite" : false, + "clientRole" : true, + "containerId" : "4ce68130-aced-4e67-936a-8082dc843cc2", + "attributes" : { } + }, { + "id" : "cbe05c62-2b07-4ac7-a33a-ffca7c176252", + "name" : "manage-events", + "description" : "${role_manage-events}", + "composite" : false, + "clientRole" : true, + "containerId" : "4ce68130-aced-4e67-936a-8082dc843cc2", + "attributes" : { } + }, { + "id" : "8ca56814-a817-4849-a515-45399eb1dcc1", + "name" : "manage-identity-providers", + "description" : "${role_manage-identity-providers}", + "composite" : false, + "clientRole" : true, + "containerId" : "4ce68130-aced-4e67-936a-8082dc843cc2", + "attributes" : { } + }, { + "id" : "1134c6df-d0ff-498d-9dc4-ad989f7cfe93", + "name" : "query-clients", + "description" : "${role_query-clients}", + "composite" : false, + "clientRole" : true, + "containerId" : "4ce68130-aced-4e67-936a-8082dc843cc2", + "attributes" : { } + }, { + "id" : "3bb14549-60f6-4078-8f4e-47a1162412f2", + "name" : "manage-realm", + "description" : "${role_manage-realm}", + "composite" : false, + "clientRole" : true, + "containerId" : "4ce68130-aced-4e67-936a-8082dc843cc2", + "attributes" : { } + } ], + "spiffworkflow-frontend" : [ ], + "security-admin-console" : [ ], + "admin-cli" : [ ], + "spiffworkflow-backend" : [ { + "id" : "4d71d1bb-d627-43c8-bc07-d542f816e04b", + "name" : "spiffworkflow-admin", + "composite" : false, + "clientRole" : true, + "containerId" : "f44558af-3601-4e54-b854-08396a247544", + "attributes" : { } + }, { + "id" : "2341ca1c-24c8-4ddf-874c-7153c9408068", + "name" : "uma_protection", + "composite" : false, + "clientRole" : true, + "containerId" : "f44558af-3601-4e54-b854-08396a247544", + "attributes" : { } + }, { + "id" : "cf88054e-4bdc-491c-bf93-c660cdaad72d", + "name" : "repeat-form-role-2", + "composite" : false, + "clientRole" : true, + "containerId" : "f44558af-3601-4e54-b854-08396a247544", + "attributes" : { + "repeat-form-role-2-att-key" : [ "repeat-form-role-2-att-value" ] } - ], - "withAuth": [ - { - "id": "87673823-6a5a-4cb2-baa7-6c8b5da5d402", - "name": "uma_protection", - "composite": false, - "clientRole": true, - "containerId": "5d94a8c3-f56b-4eff-ac39-8580053a7fbe", - "attributes": {} - } - ], - "broker": [ - { - "id": "6d688d72-cf5b-4450-a902-cb2d41f0e04c", - "name": "read-token", - "description": "${role_read-token}", - "composite": false, - "clientRole": true, - "containerId": "55d75754-cf1b-4875-bf3e-15add4be8c99", - "attributes": {} - } - ], - "account": [ - { - "id": "9c51c3e1-028d-4a0d-96dc-6619196b49f0", - "name": "delete-account", - "description": "${role_delete-account}", - "composite": false, - "clientRole": true, - "containerId": "e39b3c85-bb9d-4c73-8250-be087c82ae48", - "attributes": {} + } ], + "withAuth" : [ { + "id" : "87673823-6a5a-4cb2-baa7-6c8b5da5d402", + "name" : "uma_protection", + "composite" : false, + "clientRole" : true, + "containerId" : "5d94a8c3-f56b-4eff-ac39-8580053a7fbe", + "attributes" : { } + } ], + "broker" : [ { + "id" : "6d688d72-cf5b-4450-a902-cb2d41f0e04c", + "name" : "read-token", + "description" : "${role_read-token}", + "composite" : false, + "clientRole" : true, + "containerId" : "55d75754-cf1b-4875-bf3e-15add4be8c99", + "attributes" : { } + } ], + "account" : [ { + "id" : "9c51c3e1-028d-4a0d-96dc-6619196b49f0", + "name" : "delete-account", + "description" : "${role_delete-account}", + "composite" : false, + "clientRole" : true, + "containerId" : "e39b3c85-bb9d-4c73-8250-be087c82ae48", + "attributes" : { } + }, { + "id" : "f395d221-7f80-4fcf-90ac-0a89c8b15a9b", + "name" : "manage-consent", + "description" : "${role_manage-consent}", + "composite" : true, + "composites" : { + "client" : { + "account" : [ "view-consent" ] + } }, - { - "id": "f395d221-7f80-4fcf-90ac-0a89c8b15a9b", - "name": "manage-consent", - "description": "${role_manage-consent}", - "composite": true, - "composites": { - "client": { - "account": ["view-consent"] - } - }, - "clientRole": true, - "containerId": "e39b3c85-bb9d-4c73-8250-be087c82ae48", - "attributes": {} + "clientRole" : true, + "containerId" : "e39b3c85-bb9d-4c73-8250-be087c82ae48", + "attributes" : { } + }, { + "id" : "7abb4169-1960-4b4d-b5ae-6ea45cf91ee4", + "name" : "view-consent", + "description" : "${role_view-consent}", + "composite" : false, + "clientRole" : true, + "containerId" : "e39b3c85-bb9d-4c73-8250-be087c82ae48", + "attributes" : { } + }, { + "id" : "4d3c24ed-cc61-4a6e-ac78-47af4545b415", + "name" : "manage-account-links", + "description" : "${role_manage-account-links}", + "composite" : false, + "clientRole" : true, + "containerId" : "e39b3c85-bb9d-4c73-8250-be087c82ae48", + "attributes" : { } + }, { + "id" : "a4954091-9be9-4b7c-a196-1af934917ff7", + "name" : "view-profile", + "description" : "${role_view-profile}", + "composite" : false, + "clientRole" : true, + "containerId" : "e39b3c85-bb9d-4c73-8250-be087c82ae48", + "attributes" : { } + }, { + "id" : "0810773c-a57d-449e-a31f-1344e1eb4b9b", + "name" : "manage-account", + "description" : "${role_manage-account}", + "composite" : true, + "composites" : { + "client" : { + "account" : [ "manage-account-links" ] + } }, - { - "id": "7abb4169-1960-4b4d-b5ae-6ea45cf91ee4", - "name": "view-consent", - "description": "${role_view-consent}", - "composite": false, - "clientRole": true, - "containerId": "e39b3c85-bb9d-4c73-8250-be087c82ae48", - "attributes": {} - }, - { - "id": "4d3c24ed-cc61-4a6e-ac78-47af4545b415", - "name": "manage-account-links", - "description": "${role_manage-account-links}", - "composite": false, - "clientRole": true, - "containerId": "e39b3c85-bb9d-4c73-8250-be087c82ae48", - "attributes": {} - }, - { - "id": "a4954091-9be9-4b7c-a196-1af934917ff7", - "name": "view-profile", - "description": "${role_view-profile}", - "composite": false, - "clientRole": true, - "containerId": "e39b3c85-bb9d-4c73-8250-be087c82ae48", - "attributes": {} - }, - { - "id": "0810773c-a57d-449e-a31f-1344e1eb4b9b", - "name": "manage-account", - "description": "${role_manage-account}", - "composite": true, - "composites": { - "client": { - "account": ["manage-account-links"] - } - }, - "clientRole": true, - "containerId": "e39b3c85-bb9d-4c73-8250-be087c82ae48", - "attributes": {} - }, - { - "id": "ae774a41-a274-4f99-9d7f-f4a0d5dbc085", - "name": "view-applications", - "description": "${role_view-applications}", - "composite": false, - "clientRole": true, - "containerId": "e39b3c85-bb9d-4c73-8250-be087c82ae48", - "attributes": {} - } - ] + "clientRole" : true, + "containerId" : "e39b3c85-bb9d-4c73-8250-be087c82ae48", + "attributes" : { } + }, { + "id" : "f75e4973-b9b6-4ff0-a691-5f900199b17a", + "name" : "view-groups", + "description" : "${role_view-groups}", + "composite" : false, + "clientRole" : true, + "containerId" : "e39b3c85-bb9d-4c73-8250-be087c82ae48", + "attributes" : { } + }, { + "id" : "ae774a41-a274-4f99-9d7f-f4a0d5dbc085", + "name" : "view-applications", + "description" : "${role_view-applications}", + "composite" : false, + "clientRole" : true, + "containerId" : "e39b3c85-bb9d-4c73-8250-be087c82ae48", + "attributes" : { } + } ] } }, - "groups": [], - "defaultRole": { - "id": "c9f0ff93-642d-402b-965a-04d70719886b", - "name": "default-roles-spiffworkflow", - "description": "${role_default-roles}", - "composite": true, - "clientRole": false, - "containerId": "spiffworkflow" + "groups" : [ ], + "defaultRole" : { + "id" : "c9f0ff93-642d-402b-965a-04d70719886b", + "name" : "default-roles-spiffworkflow", + "description" : "${role_default-roles}", + "composite" : true, + "clientRole" : false, + "containerId" : "spiffworkflow" }, - "requiredCredentials": ["password"], - "otpPolicyType": "totp", - "otpPolicyAlgorithm": "HmacSHA1", - "otpPolicyInitialCounter": 0, - "otpPolicyDigits": 6, - "otpPolicyLookAheadWindow": 1, - "otpPolicyPeriod": 30, - "otpSupportedApplications": ["FreeOTP", "Google Authenticator"], - "webAuthnPolicyRpEntityName": "keycloak", - "webAuthnPolicySignatureAlgorithms": ["ES256"], - "webAuthnPolicyRpId": "", - "webAuthnPolicyAttestationConveyancePreference": "not specified", - "webAuthnPolicyAuthenticatorAttachment": "not specified", - "webAuthnPolicyRequireResidentKey": "not specified", - "webAuthnPolicyUserVerificationRequirement": "not specified", - "webAuthnPolicyCreateTimeout": 0, - "webAuthnPolicyAvoidSameAuthenticatorRegister": false, - "webAuthnPolicyAcceptableAaguids": [], - "webAuthnPolicyPasswordlessRpEntityName": "keycloak", - "webAuthnPolicyPasswordlessSignatureAlgorithms": ["ES256"], - "webAuthnPolicyPasswordlessRpId": "", - "webAuthnPolicyPasswordlessAttestationConveyancePreference": "not specified", - "webAuthnPolicyPasswordlessAuthenticatorAttachment": "not specified", - "webAuthnPolicyPasswordlessRequireResidentKey": "not specified", - "webAuthnPolicyPasswordlessUserVerificationRequirement": "not specified", - "webAuthnPolicyPasswordlessCreateTimeout": 0, - "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister": false, - "webAuthnPolicyPasswordlessAcceptableAaguids": [], - "users": [ - { - "id": "4048e9a7-8afa-4e69-9904-389657221abe", - "createdTimestamp": 1665517741516, - "username": "alex", - "enabled": true, - "totp": false, - "emailVerified": false, - "credentials": [ - { - "id": "81a61a3b-228d-42b3-b39a-f62d8e7f57ca", - "type": "password", - "createdDate": 1665517748308, - "secretData": "{\"value\":\"13OdXlB1S1EqHL+3/0y4LYp/LGCn0UW8/Wh9ykgpUbRrwdX6dY3iiMlKePfTy5nXoH/ISmPlxNKOe5z7FWXsgg==\",\"salt\":\"pv0SEb7Ctk5tpu2y32L2kw==\",\"additionalParameters\":{}}", - "credentialData": "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" - } - ], - "disableableCredentialTypes": [], - "requiredActions": [], - "realmRoles": ["default-roles-spiffworkflow"], - "notBefore": 0, - "groups": [] + "requiredCredentials" : [ "password" ], + "otpPolicyType" : "totp", + "otpPolicyAlgorithm" : "HmacSHA1", + "otpPolicyInitialCounter" : 0, + "otpPolicyDigits" : 6, + "otpPolicyLookAheadWindow" : 1, + "otpPolicyPeriod" : 30, + "otpPolicyCodeReusable" : false, + "otpSupportedApplications" : [ "totpAppGoogleName", "totpAppFreeOTPName" ], + "webAuthnPolicyRpEntityName" : "keycloak", + "webAuthnPolicySignatureAlgorithms" : [ "ES256" ], + "webAuthnPolicyRpId" : "", + "webAuthnPolicyAttestationConveyancePreference" : "not specified", + "webAuthnPolicyAuthenticatorAttachment" : "not specified", + "webAuthnPolicyRequireResidentKey" : "not specified", + "webAuthnPolicyUserVerificationRequirement" : "not specified", + "webAuthnPolicyCreateTimeout" : 0, + "webAuthnPolicyAvoidSameAuthenticatorRegister" : false, + "webAuthnPolicyAcceptableAaguids" : [ ], + "webAuthnPolicyPasswordlessRpEntityName" : "keycloak", + "webAuthnPolicyPasswordlessSignatureAlgorithms" : [ "ES256" ], + "webAuthnPolicyPasswordlessRpId" : "", + "webAuthnPolicyPasswordlessAttestationConveyancePreference" : "not specified", + "webAuthnPolicyPasswordlessAuthenticatorAttachment" : "not specified", + "webAuthnPolicyPasswordlessRequireResidentKey" : "not specified", + "webAuthnPolicyPasswordlessUserVerificationRequirement" : "not specified", + "webAuthnPolicyPasswordlessCreateTimeout" : 0, + "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister" : false, + "webAuthnPolicyPasswordlessAcceptableAaguids" : [ ], + "users" : [ { + "id" : "5a97144d-4f59-4a8c-b365-463d0577a740", + "createdTimestamp" : 1669600821350, + "username" : "admin", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "firstName" : "", + "lastName" : "", + "credentials" : [ { + "id" : "ef435043-ef0c-407a-af5b-ced13182a408", + "type" : "password", + "userLabel" : "My password", + "createdDate" : 1669600831704, + "secretData" : "{\"value\":\"4D4JRvE7kR5nfGiIdrwzK+0drmy3kX++TlT1BTvYix8N83c9FGTPWvxR1Hl4ggEKuCCAEYZnTzVJZY0DcUcN+A==\",\"salt\":\"yI7UkD+mCuq0H35AnNV/KA==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "default-roles-spiffworkflow" ], + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "4048e9a7-8afa-4e69-9904-389657221abe", + "createdTimestamp" : 1665517741516, + "username" : "alex", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "credentials" : [ { + "id" : "81a61a3b-228d-42b3-b39a-f62d8e7f57ca", + "type" : "password", + "createdDate" : 1665517748308, + "secretData" : "{\"value\":\"13OdXlB1S1EqHL+3/0y4LYp/LGCn0UW8/Wh9ykgpUbRrwdX6dY3iiMlKePfTy5nXoH/ISmPlxNKOe5z7FWXsgg==\",\"salt\":\"pv0SEb7Ctk5tpu2y32L2kw==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "default-roles-spiffworkflow" ], + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "b4dc5a30-4bd7-44fc-88b5-839fbb8567ea", + "createdTimestamp" : 1665518311550, + "username" : "amir", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "credentials" : [ { + "id" : "e589f3ad-bf7b-4756-89f7-7894c03c2831", + "type" : "password", + "createdDate" : 1665518319210, + "secretData" : "{\"value\":\"mamd7Hi6nV5suylSrUgwWon3Gw3WeOIvAJu9g39Mq1iYoXWj2rI870bGHiSITLaFBpdjLOEmlu9feKkULOXNpQ==\",\"salt\":\"wG7tkMQfPKRW9ymu4ekujQ==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "default-roles-spiffworkflow" ], + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "4c436296-8471-4105-b551-80eee96b43bb", + "createdTimestamp" : 1657139858075, + "username" : "ciadmin1", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "credentials" : [ { + "id" : "111b5ea1-c2ab-470a-a16b-2373bc94de7a", + "type" : "password", + "createdDate" : 1657139904275, + "secretData" : "{\"value\":\"e5MjWAk7RPspQIh9gEOKyv3AV/DHNoWk8w1tf+MRLh2oxrKmnnizOj0eFtIadT/q/i5JRfUq5IYBPLL/4nEJDw==\",\"salt\":\"5inqMqqTR6+PBYriy3RPjA==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "default-roles-spiffworkflow", "admin" ], + "clientRoles" : { + "spiffworkflow-backend" : [ "spiffworkflow-admin", "uma_protection" ] }, - { - "id": "b4dc5a30-4bd7-44fc-88b5-839fbb8567ea", - "createdTimestamp": 1665518311550, - "username": "amir", - "enabled": true, - "totp": false, - "emailVerified": false, - "credentials": [ - { - "id": "e589f3ad-bf7b-4756-89f7-7894c03c2831", - "type": "password", - "createdDate": 1665518319210, - "secretData": "{\"value\":\"mamd7Hi6nV5suylSrUgwWon3Gw3WeOIvAJu9g39Mq1iYoXWj2rI870bGHiSITLaFBpdjLOEmlu9feKkULOXNpQ==\",\"salt\":\"wG7tkMQfPKRW9ymu4ekujQ==\",\"additionalParameters\":{}}", - "credentialData": "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" - } - ], - "disableableCredentialTypes": [], - "requiredActions": [], - "realmRoles": ["default-roles-spiffworkflow"], - "notBefore": 0, - "groups": [] + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "56457e8f-47c6-4f9f-a72b-473dea5edfeb", + "createdTimestamp" : 1657139955336, + "username" : "ciuser1", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "credentials" : [ { + "id" : "762f36e9-47af-44da-8520-cf09d752497a", + "type" : "password", + "createdDate" : 1657139966468, + "secretData" : "{\"value\":\"Dpn9QBJSxvl54b0Fu+OKrKRwmDJbk28FQ3xhlOdJPvZVJU/SpdrcsH7ktYAIkVLkRC5qILSZuNPQ3vDGzE2r1Q==\",\"salt\":\"yXd7N8XIQBkJ7swHDeRzXw==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "default-roles-spiffworkflow" ], + "clientRoles" : { + "spiffworkflow-backend" : [ "uma_protection" ] }, - { - "id": "4c436296-8471-4105-b551-80eee96b43bb", - "createdTimestamp": 1657139858075, - "username": "ciadmin1", - "enabled": true, - "totp": false, - "emailVerified": false, - "credentials": [ - { - "id": "111b5ea1-c2ab-470a-a16b-2373bc94de7a", - "type": "password", - "createdDate": 1657139904275, - "secretData": "{\"value\":\"e5MjWAk7RPspQIh9gEOKyv3AV/DHNoWk8w1tf+MRLh2oxrKmnnizOj0eFtIadT/q/i5JRfUq5IYBPLL/4nEJDw==\",\"salt\":\"5inqMqqTR6+PBYriy3RPjA==\",\"additionalParameters\":{}}", - "credentialData": "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" - } - ], - "disableableCredentialTypes": [], - "requiredActions": [], - "realmRoles": ["default-roles-spiffworkflow", "admin"], - "clientRoles": { - "spiffworkflow-backend": ["spiffworkflow-admin", "uma_protection"] - }, - "notBefore": 0, - "groups": [] + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "d58b61cc-a77e-488f-a427-05f4e0572e20", + "createdTimestamp" : 1669132945413, + "username" : "core", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "firstName" : "", + "lastName" : "", + "credentials" : [ { + "id" : "ee80092b-8ee6-4699-8492-566e088b48f5", + "type" : "password", + "userLabel" : "My password", + "createdDate" : 1669132955862, + "secretData" : "{\"value\":\"x0f/IvOAsMmbQzgc1LXJ9O7dDepeFURi7lD4Wy0NZBrFRyQ3pMXM6FHNNjhVDeZMsTr2tesYYQ2BK3z9xIPPrA==\",\"salt\":\"vx4/Z41MiUnLqaVt+vMmOQ==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "default-roles-spiffworkflow" ], + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "99e7e4ea-d4ae-4944-bd31-873dac7b004c", + "createdTimestamp" : 1665517024483, + "username" : "dan", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "credentials" : [ { + "id" : "d517c520-f500-4542-80e5-7144daef1e32", + "type" : "password", + "createdDate" : 1665517033429, + "secretData" : "{\"value\":\"rgWPI1YobMfDaaT3di2+af3gHU8bkreRElAHgYFA+dXHw0skiGVd1t57kNLEP49M6zKYjZzlOKr0qvAxQF0oSg==\",\"salt\":\"usMZebZnPYXhD6ID95bizg==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "default-roles-spiffworkflow" ], + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "1834a79d-917f-4e4c-ab38-8ec376179fe9", + "createdTimestamp" : 1665517805115, + "username" : "daniel", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "credentials" : [ { + "id" : "f240495c-265b-42fc-99db-46928580d07d", + "type" : "password", + "createdDate" : 1665517812636, + "secretData" : "{\"value\":\"sRCF3tFOZrUbEW220cVHhQ7e89iKqjgAMyO0BaYCPZZw1tEjZ+drGj+bfwRbuuK0Nps3t//YGVELsejRogWkcw==\",\"salt\":\"XQtLR9oZctkyRTi2Be+Z0g==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "default-roles-spiffworkflow" ], + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "72d32cba-e2e2-489d-9141-4d94e3bb2cda", + "createdTimestamp" : 1665517787787, + "username" : "elizabeth", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "credentials" : [ { + "id" : "ae951ec8-9fc9-4f1b-b340-bbbe463ae5c2", + "type" : "password", + "createdDate" : 1665517794484, + "secretData" : "{\"value\":\"oudGUsbh8utUavZ8OmoUvggCYxr+RHCgwcqpub5AgbITsK4DgY01X0SlDGRTdNGOIqoHse8zGBNmcyBNPWjC0w==\",\"salt\":\"auHilaAS2Lo7oa0UaA7L6A==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "default-roles-spiffworkflow" ], + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "faf29027-dc54-4804-a408-4989a8c9c243", + "createdTimestamp" : 1669132994561, + "username" : "fin", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "firstName" : "", + "lastName" : "", + "credentials" : [ { + "id" : "2379940c-98b4-481a-b629-0bd1a4e91acf", + "type" : "password", + "userLabel" : "My password", + "createdDate" : 1669133003955, + "secretData" : "{\"value\":\"Wb9XtkrxJ9YdW7faHcWgQ+WK3JqBYCQ5wTn9rJa7Uo47I2TrniH+7/CBODIaiF3ipYAEZBkiCJDnPqg2qbZ+aA==\",\"salt\":\"bY1gRb+5sjbmrYWvxfl9CQ==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "default-roles-spiffworkflow" ], + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "13e009b2-e96f-43b7-a227-465675ece81d", + "createdTimestamp" : 1669303701625, + "username" : "fin1", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "firstName" : "", + "lastName" : "", + "credentials" : [ { + "id" : "96216746-ff72-454e-8288-232428d10b42", + "type" : "password", + "userLabel" : "My password", + "createdDate" : 1669303725352, + "secretData" : "{\"value\":\"ukPIO1rlfpzbxb+FXHAwCdNQ4cq3yX+Ke11uFPpGy7xBNT5UgLzO3oIK34Cw1Ma3+gFqK6/OsT4Q5fZd/AsVJQ==\",\"salt\":\"iSIY1gAdz7wkAwnGer95Lw==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "default-roles-spiffworkflow" ], + "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, + "username" : "harmeet", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "credentials" : [ { + "id" : "89c26090-9bd3-46ac-b038-883d02e3f125", + "type" : "password", + "createdDate" : 1665516905862, + "secretData" : "{\"value\":\"vDzTFQhjg8l8XgQ/YFYZSMLxQovFc/wflVBiRtAk/UWRKhJwuz3XInFbQ64wbYppBlXDYSmYis3luKv6YyUWjQ==\",\"salt\":\"58OQLETS0sM9VpXWoNa6rQ==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "default-roles-spiffworkflow" ], + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "1561518b-c327-491e-9db3-23c2b5394104", + "createdTimestamp" : 1669303773974, + "username" : "j", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "firstName" : "", + "lastName" : "", + "credentials" : [ { + "id" : "e71ec785-9133-4b7d-8015-1978379af0bb", + "type" : "password", + "userLabel" : "My password", + "createdDate" : 1669303786522, + "secretData" : "{\"value\":\"g/nsCceqGWoU7thzq21RFSNUB8WP6l9/x2ghKFAKC1Xrqcf2At+u0r8GglqM6WmLthOTtrwICs98tS4ZPLmsbA==\",\"salt\":\"Na/OfJ9itENgaLPsIntzUQ==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "default-roles-spiffworkflow" ], + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "13f5481e-c6b5-450d-8aaf-e13c1c1f5914", + "createdTimestamp" : 1665518332327, + "username" : "jakub", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "credentials" : [ { + "id" : "ce141fa5-b8d5-4bbe-93e7-22e7119f97c2", + "type" : "password", + "createdDate" : 1665518338651, + "secretData" : "{\"value\":\"+L4TmIGURzFtyRMFyKbPmQ8iYSC639K0GLNHXM+T/cLiMGxVr/wvWj5j435c1V9P+kwO2CnGtd09IsSN8cXuXg==\",\"salt\":\"a2eNeYyoci5fpkPJJy735g==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "default-roles-spiffworkflow" ], + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "3965a6c8-31df-474f-9a45-c268ed98e3fd", + "createdTimestamp" : 1665518284693, + "username" : "jarrad", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "credentials" : [ { + "id" : "113e0343-1069-476d-83f9-21d98edb9cfa", + "type" : "password", + "createdDate" : 1665518292234, + "secretData" : "{\"value\":\"1CeBMYC3yiJ/cmIxHs/bSea3kxItLNnaIkPNRk2HefZiCdfUKcJ/QLI0O9QO108G2Lzg9McR33EB72zbFAfYUw==\",\"salt\":\"2kWgItvYvzJkgJU9ICWMAw==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "default-roles-spiffworkflow" ], + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "58bcce19-41ec-4ae7-b930-b37be7ad4ba3", + "createdTimestamp" : 1665516949583, + "username" : "jason", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "credentials" : [ { + "id" : "40abf32e-f0cc-4a17-8231-1a69a02c1b0b", + "type" : "password", + "createdDate" : 1665516957192, + "secretData" : "{\"value\":\"nCnRYH5rLRMu1E7C260SowAdvJfQCSdf4LigcIzSkoPwT+qfLT5ut5m99zakNLeHLoCtGhO2lSVGUQWhdCUYJw==\",\"salt\":\"mW5QN/RSr55I04VI6FTERA==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "default-roles-spiffworkflow" ], + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "29c11638-3b32-4024-8594-91c8b09e713c", + "createdTimestamp" : 1665518366585, + "username" : "jon", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "credentials" : [ { + "id" : "8b520e01-5b9b-44ab-9ee8-505bd0831a45", + "type" : "password", + "createdDate" : 1665518373016, + "secretData" : "{\"value\":\"lZBDnz49zW6EkT2t7JSQjOzBlYhjhkw3hHefcOC4tmet+h/dAuxSGRuLibJHBap2j6G9Z2SoRqtyS8bwGbR42g==\",\"salt\":\"MI90jmxbLAno0g5O4BCeHw==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "default-roles-spiffworkflow" ], + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "af15c167-d0e7-4a41-ac2c-109188dd7166", + "createdTimestamp" : 1665516966482, + "username" : "kb", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "credentials" : [ { + "id" : "2c0be363-038f-48f1-86d6-91fdd28657cf", + "type" : "password", + "createdDate" : 1665516982394, + "secretData" : "{\"value\":\"yvliX8Mn+lgpxfMpkjfsV8CASgghEgPA2P1/DR1GP5LSFoGwGCEwj0SmeQAo+MQjBsn3nfvtL9asQvmIYdNZwQ==\",\"salt\":\"kFr1K94QCEx9eGD25rZR9g==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "default-roles-spiffworkflow" ], + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "23c464ea-6a98-462c-a8b9-e8e561804361", + "createdTimestamp" : 1669132970114, + "username" : "lead", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "firstName" : "", + "lastName" : "", + "credentials" : [ { + "id" : "96e836a4-1a84-45c5-a9ed-651b0c90195e", + "type" : "password", + "userLabel" : "My password", + "createdDate" : 1669132979516, + "secretData" : "{\"value\":\"DsOkyXBHcwY0HAGta+m+E5jXDZwxGl/fgROCR7ph23oJ3j9833UVH5VLHfYcZ3YZixUIfskYMlcwW91uqO0oxQ==\",\"salt\":\"zOLZMvNnOIEB0t32DghWiQ==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "default-roles-spiffworkflow" ], + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "fef2c863-be05-49f2-94d0-702238505a4d", + "createdTimestamp" : 1669303745591, + "username" : "lead1", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "firstName" : "", + "lastName" : "", + "credentials" : [ { + "id" : "4e17388b-6c44-44e1-b20a-a873c0feb9a8", + "type" : "password", + "userLabel" : "My password", + "createdDate" : 1669303762736, + "secretData" : "{\"value\":\"NNPFZcVk47adUPH1q3L27uPkULy9OocZkOzi4qUVvO+tvZJVH5sMrSUYqM8S71AqdHNZD1a8ge6amF6k6dDIkQ==\",\"salt\":\"7e48fZJBAeVferVYA4gNVw==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "default-roles-spiffworkflow" ], + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "6f5bfa09-7494-4a2f-b871-cf327048cac7", + "createdTimestamp" : 1665517010600, + "username" : "manuchehr", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "credentials" : [ { + "id" : "07dabf55-b5d3-4f98-abba-3334086ecf5e", + "type" : "password", + "createdDate" : 1665517017682, + "secretData" : "{\"value\":\"1btDXHraz9l0Gp4g1xxdcuZffLsuKsW0tHwQGzoEtTlI/iZdrKPG9WFlCEFd84qtpdYPJD/tvzn6ZK6zU4/GlQ==\",\"salt\":\"jHtMiO+4jMv9GqLhC9wg4w==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "default-roles-spiffworkflow" ], + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "d1c46b47-67c4-4d07-9cf4-6b1ceac88fc1", + "createdTimestamp" : 1665517760255, + "username" : "mike", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "credentials" : [ { + "id" : "1ed375fb-0f1a-4c2a-9243-2477242cf7bd", + "type" : "password", + "createdDate" : 1665517768715, + "secretData" : "{\"value\":\"S1cxZ3dgNB+A6yfMchDWEGP8OyZaaAOU/IUKn+QWFt255yoFqs28pfmwCsevdzuh0YfygO9GBgBv7qZQ2pknNQ==\",\"salt\":\"i+Q9zEHNxfi8TAHw17Dv6w==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "default-roles-spiffworkflow" ], + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "cecacfd3-2f59-4ce2-87d9-bea91ef13c5b", + "createdTimestamp" : 1666102618518, + "username" : "natalia", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "credentials" : [ { + "id" : "b6aa9936-39cc-4931-bfeb-60e6753de5ba", + "type" : "password", + "createdDate" : 1666102626704, + "secretData" : "{\"value\":\"kGyQIqZM6n9rjGZkNScJbkFjLvRJ2I+ZzCtjQ80e+zX7QaXtIF3CEeSY6KTXVjE8Z74oyVBWTIibpiTblm5Ztw==\",\"salt\":\"0k+Y+QJiW0YhxuxxYigasg==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "default-roles-spiffworkflow" ], + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "f3852a7d-8adf-494f-b39d-96ad4c899ee5", + "createdTimestamp" : 1665516926300, + "username" : "sasha", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "credentials" : [ { + "id" : "4a170af4-6f0c-4e7b-b70c-e674edf619df", + "type" : "password", + "createdDate" : 1665516934662, + "secretData" : "{\"value\":\"/cimS+PL6p+YnOCF9ZSA6UuwmmLZ7aVUZUthiFDqp/sn0c8GTpWmAdDIbJy2Ut+D4Rx605kRFQaekzRgSYPxcg==\",\"salt\":\"0dmUnLfqK745YHVSz6HOZg==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "default-roles-spiffworkflow" ], + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "487d3a85-89dd-4839-957a-c3f6d70551f6", + "createdTimestamp" : 1657115173081, + "username" : "service-account-spiffworkflow-backend", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "serviceAccountClientId" : "spiffworkflow-backend", + "credentials" : [ ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "default-roles-spiffworkflow" ], + "clientRoles" : { + "spiffworkflow-backend" : [ "uma_protection" ] }, - { - "id": "56457e8f-47c6-4f9f-a72b-473dea5edfeb", - "createdTimestamp": 1657139955336, - "username": "ciuser1", - "enabled": true, - "totp": false, - "emailVerified": false, - "credentials": [ - { - "id": "762f36e9-47af-44da-8520-cf09d752497a", - "type": "password", - "createdDate": 1657139966468, - "secretData": "{\"value\":\"Dpn9QBJSxvl54b0Fu+OKrKRwmDJbk28FQ3xhlOdJPvZVJU/SpdrcsH7ktYAIkVLkRC5qILSZuNPQ3vDGzE2r1Q==\",\"salt\":\"yXd7N8XIQBkJ7swHDeRzXw==\",\"additionalParameters\":{}}", - "credentialData": "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" - } - ], - "disableableCredentialTypes": [], - "requiredActions": [], - "realmRoles": ["default-roles-spiffworkflow"], - "clientRoles": { - "spiffworkflow-backend": ["uma_protection"] - }, - "notBefore": 0, - "groups": [] + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "22de68b1-4b06-4bc2-8da6-0c577e7e62ad", + "createdTimestamp" : 1657055472800, + "username" : "service-account-withauth", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "serviceAccountClientId" : "withAuth", + "credentials" : [ ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "default-roles-spiffworkflow" ], + "clientRoles" : { + "withAuth" : [ "uma_protection" ] }, - { - "id": "99e7e4ea-d4ae-4944-bd31-873dac7b004c", - "createdTimestamp": 1665517024483, - "username": "dan", - "enabled": true, - "totp": false, - "emailVerified": false, - "credentials": [ - { - "id": "d517c520-f500-4542-80e5-7144daef1e32", - "type": "password", - "createdDate": 1665517033429, - "secretData": "{\"value\":\"rgWPI1YobMfDaaT3di2+af3gHU8bkreRElAHgYFA+dXHw0skiGVd1t57kNLEP49M6zKYjZzlOKr0qvAxQF0oSg==\",\"salt\":\"usMZebZnPYXhD6ID95bizg==\",\"additionalParameters\":{}}", - "credentialData": "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" - } - ], - "disableableCredentialTypes": [], - "requiredActions": [], - "realmRoles": ["default-roles-spiffworkflow"], - "notBefore": 0, - "groups": [] + "notBefore" : 0, + "groups" : [ ] + } ], + "scopeMappings" : [ { + "clientScope" : "offline_access", + "roles" : [ "offline_access" ] + } ], + "clients" : [ { + "id" : "e39b3c85-bb9d-4c73-8250-be087c82ae48", + "clientId" : "account", + "name" : "${client_account}", + "rootUrl" : "${authBaseUrl}", + "baseUrl" : "/realms/spiffworkflow/account/", + "surrogateAuthRequired" : false, + "enabled" : false, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ "/realms/spiffworkflow/account/*" ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "saml.force.post.binding" : "false", + "saml.multivalued.roles" : "false", + "frontchannel.logout.session.required" : "false", + "post.logout.redirect.uris" : "+", + "oauth2.device.authorization.grant.enabled" : "false", + "backchannel.logout.revoke.offline.tokens" : "false", + "saml.server.signature.keyinfo.ext" : "false", + "use.refresh.tokens" : "true", + "oidc.ciba.grant.enabled" : "false", + "backchannel.logout.session.required" : "false", + "client_credentials.use_refresh_token" : "false", + "require.pushed.authorization.requests" : "false", + "saml.client.signature" : "false", + "saml.allow.ecp.flow" : "false", + "id.token.as.detached.signature" : "false", + "saml.assertion.signature" : "false", + "saml.encrypt" : "false", + "saml.server.signature" : "false", + "exclude.session.state.from.auth.response" : "false", + "saml.artifact.binding" : "false", + "saml_force_name_id_format" : "false", + "acr.loa.map" : "{}", + "tls.client.certificate.bound.access.tokens" : "false", + "saml.authnstatement" : "false", + "display.on.consent.screen" : "false", + "token.response.type.bearer.lower-case" : "false", + "saml.onetimeuse.condition" : "false" }, - { - "id": "1834a79d-917f-4e4c-ab38-8ec376179fe9", - "createdTimestamp": 1665517805115, - "username": "daniel", - "enabled": true, - "totp": false, - "emailVerified": false, - "credentials": [ - { - "id": "f240495c-265b-42fc-99db-46928580d07d", - "type": "password", - "createdDate": 1665517812636, - "secretData": "{\"value\":\"sRCF3tFOZrUbEW220cVHhQ7e89iKqjgAMyO0BaYCPZZw1tEjZ+drGj+bfwRbuuK0Nps3t//YGVELsejRogWkcw==\",\"salt\":\"XQtLR9oZctkyRTi2Be+Z0g==\",\"additionalParameters\":{}}", - "credentialData": "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" - } - ], - "disableableCredentialTypes": [], - "requiredActions": [], - "realmRoles": ["default-roles-spiffworkflow"], - "notBefore": 0, - "groups": [] + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "02fa6179-9399-4bb1-970f-c4d8e8b5f99f", + "clientId" : "admin-cli", + "name" : "${client_admin-cli}", + "surrogateAuthRequired" : false, + "enabled" : false, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : false, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : true, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "saml.force.post.binding" : "false", + "saml.multivalued.roles" : "false", + "frontchannel.logout.session.required" : "false", + "post.logout.redirect.uris" : "+", + "oauth2.device.authorization.grant.enabled" : "false", + "backchannel.logout.revoke.offline.tokens" : "false", + "saml.server.signature.keyinfo.ext" : "false", + "use.refresh.tokens" : "true", + "oidc.ciba.grant.enabled" : "false", + "backchannel.logout.session.required" : "false", + "client_credentials.use_refresh_token" : "false", + "require.pushed.authorization.requests" : "false", + "saml.client.signature" : "false", + "saml.allow.ecp.flow" : "false", + "id.token.as.detached.signature" : "false", + "saml.assertion.signature" : "false", + "saml.encrypt" : "false", + "saml.server.signature" : "false", + "exclude.session.state.from.auth.response" : "false", + "saml.artifact.binding" : "false", + "saml_force_name_id_format" : "false", + "acr.loa.map" : "{}", + "tls.client.certificate.bound.access.tokens" : "false", + "saml.authnstatement" : "false", + "display.on.consent.screen" : "false", + "token.response.type.bearer.lower-case" : "false", + "saml.onetimeuse.condition" : "false" }, - { - "id": "72d32cba-e2e2-489d-9141-4d94e3bb2cda", - "createdTimestamp": 1665517787787, - "username": "elizabeth", - "enabled": true, - "totp": false, - "emailVerified": false, - "credentials": [ - { - "id": "ae951ec8-9fc9-4f1b-b340-bbbe463ae5c2", - "type": "password", - "createdDate": 1665517794484, - "secretData": "{\"value\":\"oudGUsbh8utUavZ8OmoUvggCYxr+RHCgwcqpub5AgbITsK4DgY01X0SlDGRTdNGOIqoHse8zGBNmcyBNPWjC0w==\",\"salt\":\"auHilaAS2Lo7oa0UaA7L6A==\",\"additionalParameters\":{}}", - "credentialData": "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" - } - ], - "disableableCredentialTypes": [], - "requiredActions": [], - "realmRoles": ["default-roles-spiffworkflow"], - "notBefore": 0, - "groups": [] + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "55d75754-cf1b-4875-bf3e-15add4be8c99", + "clientId" : "broker", + "name" : "${client_broker}", + "surrogateAuthRequired" : false, + "enabled" : false, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : true, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : false, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "saml.force.post.binding" : "false", + "saml.multivalued.roles" : "false", + "frontchannel.logout.session.required" : "false", + "post.logout.redirect.uris" : "+", + "oauth2.device.authorization.grant.enabled" : "false", + "backchannel.logout.revoke.offline.tokens" : "false", + "saml.server.signature.keyinfo.ext" : "false", + "use.refresh.tokens" : "true", + "oidc.ciba.grant.enabled" : "false", + "backchannel.logout.session.required" : "false", + "client_credentials.use_refresh_token" : "false", + "require.pushed.authorization.requests" : "false", + "saml.client.signature" : "false", + "saml.allow.ecp.flow" : "false", + "id.token.as.detached.signature" : "false", + "saml.assertion.signature" : "false", + "saml.encrypt" : "false", + "saml.server.signature" : "false", + "exclude.session.state.from.auth.response" : "false", + "saml.artifact.binding" : "false", + "saml_force_name_id_format" : "false", + "acr.loa.map" : "{}", + "tls.client.certificate.bound.access.tokens" : "false", + "saml.authnstatement" : "false", + "display.on.consent.screen" : "false", + "token.response.type.bearer.lower-case" : "false", + "saml.onetimeuse.condition" : "false" }, - { - "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": [] + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "4ce68130-aced-4e67-936a-8082dc843cc2", + "clientId" : "realm-management", + "name" : "${client_realm-management}", + "surrogateAuthRequired" : false, + "enabled" : false, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : true, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : false, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "saml.force.post.binding" : "false", + "saml.multivalued.roles" : "false", + "frontchannel.logout.session.required" : "false", + "post.logout.redirect.uris" : "+", + "oauth2.device.authorization.grant.enabled" : "false", + "backchannel.logout.revoke.offline.tokens" : "false", + "saml.server.signature.keyinfo.ext" : "false", + "use.refresh.tokens" : "true", + "oidc.ciba.grant.enabled" : "false", + "backchannel.logout.session.required" : "false", + "client_credentials.use_refresh_token" : "false", + "require.pushed.authorization.requests" : "false", + "saml.client.signature" : "false", + "saml.allow.ecp.flow" : "false", + "id.token.as.detached.signature" : "false", + "saml.assertion.signature" : "false", + "saml.encrypt" : "false", + "saml.server.signature" : "false", + "exclude.session.state.from.auth.response" : "false", + "saml.artifact.binding" : "false", + "saml_force_name_id_format" : "false", + "acr.loa.map" : "{}", + "tls.client.certificate.bound.access.tokens" : "false", + "saml.authnstatement" : "false", + "display.on.consent.screen" : "false", + "token.response.type.bearer.lower-case" : "false", + "saml.onetimeuse.condition" : "false" }, - { - "id": "087bdc16-e362-4340-aa60-1ff71a45f844", - "createdTimestamp": 1665516884829, - "username": "harmeet", - "enabled": true, - "totp": false, - "emailVerified": false, - "credentials": [ - { - "id": "89c26090-9bd3-46ac-b038-883d02e3f125", - "type": "password", - "createdDate": 1665516905862, - "secretData": "{\"value\":\"vDzTFQhjg8l8XgQ/YFYZSMLxQovFc/wflVBiRtAk/UWRKhJwuz3XInFbQ64wbYppBlXDYSmYis3luKv6YyUWjQ==\",\"salt\":\"58OQLETS0sM9VpXWoNa6rQ==\",\"additionalParameters\":{}}", - "credentialData": "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" - } - ], - "disableableCredentialTypes": [], - "requiredActions": [], - "realmRoles": ["default-roles-spiffworkflow"], - "notBefore": 0, - "groups": [] + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "7c82344d-d4ae-4599-bbce-583cc8848199", + "clientId" : "security-admin-console", + "name" : "${client_security-admin-console}", + "rootUrl" : "${authAdminUrl}", + "baseUrl" : "/admin/spiffworkflow/console/", + "surrogateAuthRequired" : false, + "enabled" : false, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ "/admin/spiffworkflow/console/*" ], + "webOrigins" : [ "+" ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "saml.force.post.binding" : "false", + "saml.multivalued.roles" : "false", + "frontchannel.logout.session.required" : "false", + "post.logout.redirect.uris" : "+", + "oauth2.device.authorization.grant.enabled" : "false", + "backchannel.logout.revoke.offline.tokens" : "false", + "saml.server.signature.keyinfo.ext" : "false", + "use.refresh.tokens" : "true", + "oidc.ciba.grant.enabled" : "false", + "backchannel.logout.session.required" : "false", + "client_credentials.use_refresh_token" : "false", + "require.pushed.authorization.requests" : "false", + "saml.client.signature" : "false", + "pkce.code.challenge.method" : "S256", + "saml.allow.ecp.flow" : "false", + "id.token.as.detached.signature" : "false", + "saml.assertion.signature" : "false", + "saml.encrypt" : "false", + "saml.server.signature" : "false", + "exclude.session.state.from.auth.response" : "false", + "saml.artifact.binding" : "false", + "saml_force_name_id_format" : "false", + "acr.loa.map" : "{}", + "tls.client.certificate.bound.access.tokens" : "false", + "saml.authnstatement" : "false", + "display.on.consent.screen" : "false", + "token.response.type.bearer.lower-case" : "false", + "saml.onetimeuse.condition" : "false" }, - { - "id": "13f5481e-c6b5-450d-8aaf-e13c1c1f5914", - "createdTimestamp": 1665518332327, - "username": "jakub", - "enabled": true, - "totp": false, - "emailVerified": false, - "credentials": [ - { - "id": "ce141fa5-b8d5-4bbe-93e7-22e7119f97c2", - "type": "password", - "createdDate": 1665518338651, - "secretData": "{\"value\":\"+L4TmIGURzFtyRMFyKbPmQ8iYSC639K0GLNHXM+T/cLiMGxVr/wvWj5j435c1V9P+kwO2CnGtd09IsSN8cXuXg==\",\"salt\":\"a2eNeYyoci5fpkPJJy735g==\",\"additionalParameters\":{}}", - "credentialData": "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" - } - ], - "disableableCredentialTypes": [], - "requiredActions": [], - "realmRoles": ["default-roles-spiffworkflow"], - "notBefore": 0, - "groups": [] - }, - { - "id": "3965a6c8-31df-474f-9a45-c268ed98e3fd", - "createdTimestamp": 1665518284693, - "username": "jarrad", - "enabled": true, - "totp": false, - "emailVerified": false, - "credentials": [ - { - "id": "113e0343-1069-476d-83f9-21d98edb9cfa", - "type": "password", - "createdDate": 1665518292234, - "secretData": "{\"value\":\"1CeBMYC3yiJ/cmIxHs/bSea3kxItLNnaIkPNRk2HefZiCdfUKcJ/QLI0O9QO108G2Lzg9McR33EB72zbFAfYUw==\",\"salt\":\"2kWgItvYvzJkgJU9ICWMAw==\",\"additionalParameters\":{}}", - "credentialData": "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" - } - ], - "disableableCredentialTypes": [], - "requiredActions": [], - "realmRoles": ["default-roles-spiffworkflow"], - "notBefore": 0, - "groups": [] - }, - { - "id": "58bcce19-41ec-4ae7-b930-b37be7ad4ba3", - "createdTimestamp": 1665516949583, - "username": "jason", - "enabled": true, - "totp": false, - "emailVerified": false, - "credentials": [ - { - "id": "40abf32e-f0cc-4a17-8231-1a69a02c1b0b", - "type": "password", - "createdDate": 1665516957192, - "secretData": "{\"value\":\"nCnRYH5rLRMu1E7C260SowAdvJfQCSdf4LigcIzSkoPwT+qfLT5ut5m99zakNLeHLoCtGhO2lSVGUQWhdCUYJw==\",\"salt\":\"mW5QN/RSr55I04VI6FTERA==\",\"additionalParameters\":{}}", - "credentialData": "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" - } - ], - "disableableCredentialTypes": [], - "requiredActions": [], - "realmRoles": ["default-roles-spiffworkflow"], - "notBefore": 0, - "groups": [] - }, - { - "id": "29c11638-3b32-4024-8594-91c8b09e713c", - "createdTimestamp": 1665518366585, - "username": "jon", - "enabled": true, - "totp": false, - "emailVerified": false, - "credentials": [ - { - "id": "8b520e01-5b9b-44ab-9ee8-505bd0831a45", - "type": "password", - "createdDate": 1665518373016, - "secretData": "{\"value\":\"lZBDnz49zW6EkT2t7JSQjOzBlYhjhkw3hHefcOC4tmet+h/dAuxSGRuLibJHBap2j6G9Z2SoRqtyS8bwGbR42g==\",\"salt\":\"MI90jmxbLAno0g5O4BCeHw==\",\"additionalParameters\":{}}", - "credentialData": "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" - } - ], - "disableableCredentialTypes": [], - "requiredActions": [], - "realmRoles": ["default-roles-spiffworkflow"], - "notBefore": 0, - "groups": [] - }, - { - "id": "af15c167-d0e7-4a41-ac2c-109188dd7166", - "createdTimestamp": 1665516966482, - "username": "kb", - "enabled": true, - "totp": false, - "emailVerified": false, - "credentials": [ - { - "id": "2c0be363-038f-48f1-86d6-91fdd28657cf", - "type": "password", - "createdDate": 1665516982394, - "secretData": "{\"value\":\"yvliX8Mn+lgpxfMpkjfsV8CASgghEgPA2P1/DR1GP5LSFoGwGCEwj0SmeQAo+MQjBsn3nfvtL9asQvmIYdNZwQ==\",\"salt\":\"kFr1K94QCEx9eGD25rZR9g==\",\"additionalParameters\":{}}", - "credentialData": "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" - } - ], - "disableableCredentialTypes": [], - "requiredActions": [], - "realmRoles": ["default-roles-spiffworkflow"], - "notBefore": 0, - "groups": [] - }, - { - "id": "6f5bfa09-7494-4a2f-b871-cf327048cac7", - "createdTimestamp": 1665517010600, - "username": "manuchehr", - "enabled": true, - "totp": false, - "emailVerified": false, - "credentials": [ - { - "id": "07dabf55-b5d3-4f98-abba-3334086ecf5e", - "type": "password", - "createdDate": 1665517017682, - "secretData": "{\"value\":\"1btDXHraz9l0Gp4g1xxdcuZffLsuKsW0tHwQGzoEtTlI/iZdrKPG9WFlCEFd84qtpdYPJD/tvzn6ZK6zU4/GlQ==\",\"salt\":\"jHtMiO+4jMv9GqLhC9wg4w==\",\"additionalParameters\":{}}", - "credentialData": "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" - } - ], - "disableableCredentialTypes": [], - "requiredActions": [], - "realmRoles": ["default-roles-spiffworkflow"], - "notBefore": 0, - "groups": [] - }, - { - "id": "d1c46b47-67c4-4d07-9cf4-6b1ceac88fc1", - "createdTimestamp": 1665517760255, - "username": "mike", - "enabled": true, - "totp": false, - "emailVerified": false, - "credentials": [ - { - "id": "1ed375fb-0f1a-4c2a-9243-2477242cf7bd", - "type": "password", - "createdDate": 1665517768715, - "secretData": "{\"value\":\"S1cxZ3dgNB+A6yfMchDWEGP8OyZaaAOU/IUKn+QWFt255yoFqs28pfmwCsevdzuh0YfygO9GBgBv7qZQ2pknNQ==\",\"salt\":\"i+Q9zEHNxfi8TAHw17Dv6w==\",\"additionalParameters\":{}}", - "credentialData": "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" - } - ], - "disableableCredentialTypes": [], - "requiredActions": [], - "realmRoles": ["default-roles-spiffworkflow"], - "notBefore": 0, - "groups": [] - }, - { - "id": "cecacfd3-2f59-4ce2-87d9-bea91ef13c5b", - "createdTimestamp": 1666102618518, - "username": "natalia", - "enabled": true, - "totp": false, - "emailVerified": false, - "credentials": [ - { - "id": "b6aa9936-39cc-4931-bfeb-60e6753de5ba", - "type": "password", - "createdDate": 1666102626704, - "secretData": "{\"value\":\"kGyQIqZM6n9rjGZkNScJbkFjLvRJ2I+ZzCtjQ80e+zX7QaXtIF3CEeSY6KTXVjE8Z74oyVBWTIibpiTblm5Ztw==\",\"salt\":\"0k+Y+QJiW0YhxuxxYigasg==\",\"additionalParameters\":{}}", - "credentialData": "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" - } - ], - "disableableCredentialTypes": [], - "requiredActions": [], - "realmRoles": ["default-roles-spiffworkflow"], - "notBefore": 0, - "groups": [] - }, - { - "id": "f3852a7d-8adf-494f-b39d-96ad4c899ee5", - "createdTimestamp": 1665516926300, - "username": "sasha", - "enabled": true, - "totp": false, - "emailVerified": false, - "credentials": [ - { - "id": "4a170af4-6f0c-4e7b-b70c-e674edf619df", - "type": "password", - "createdDate": 1665516934662, - "secretData": "{\"value\":\"/cimS+PL6p+YnOCF9ZSA6UuwmmLZ7aVUZUthiFDqp/sn0c8GTpWmAdDIbJy2Ut+D4Rx605kRFQaekzRgSYPxcg==\",\"salt\":\"0dmUnLfqK745YHVSz6HOZg==\",\"additionalParameters\":{}}", - "credentialData": "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" - } - ], - "disableableCredentialTypes": [], - "requiredActions": [], - "realmRoles": ["default-roles-spiffworkflow"], - "notBefore": 0, - "groups": [] - }, - { - "id": "487d3a85-89dd-4839-957a-c3f6d70551f6", - "createdTimestamp": 1657115173081, - "username": "service-account-spiffworkflow-backend", - "enabled": true, - "totp": false, - "emailVerified": false, - "serviceAccountClientId": "spiffworkflow-backend", - "credentials": [], - "disableableCredentialTypes": [], - "requiredActions": [], - "realmRoles": ["default-roles-spiffworkflow"], - "clientRoles": { - "spiffworkflow-backend": ["uma_protection"] - }, - "notBefore": 0, - "groups": [] - }, - { - "id": "22de68b1-4b06-4bc2-8da6-0c577e7e62ad", - "createdTimestamp": 1657055472800, - "username": "service-account-withauth", - "enabled": true, - "totp": false, - "emailVerified": false, - "serviceAccountClientId": "withAuth", - "credentials": [], - "disableableCredentialTypes": [], - "requiredActions": [], - "realmRoles": ["default-roles-spiffworkflow"], - "clientRoles": { - "withAuth": ["uma_protection"] - }, - "notBefore": 0, - "groups": [] - } - ], - "scopeMappings": [ - { - "clientScope": "offline_access", - "roles": ["offline_access"] - } - ], - "clients": [ - { - "id": "e39b3c85-bb9d-4c73-8250-be087c82ae48", - "clientId": "account", - "name": "${client_account}", - "rootUrl": "${authBaseUrl}", - "baseUrl": "/realms/spiffworkflow/account/", - "surrogateAuthRequired": false, - "enabled": false, - "alwaysDisplayInConsole": false, - "clientAuthenticatorType": "client-secret", - "redirectUris": ["/realms/spiffworkflow/account/*"], - "webOrigins": [], - "notBefore": 0, - "bearerOnly": false, - "consentRequired": false, - "standardFlowEnabled": true, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": false, - "serviceAccountsEnabled": false, - "publicClient": true, - "frontchannelLogout": false, - "protocol": "openid-connect", - "attributes": { - "saml.force.post.binding": "false", - "saml.multivalued.roles": "false", - "frontchannel.logout.session.required": "false", - "post.logout.redirect.uris": "+", - "oauth2.device.authorization.grant.enabled": "false", - "backchannel.logout.revoke.offline.tokens": "false", - "saml.server.signature.keyinfo.ext": "false", - "use.refresh.tokens": "true", - "oidc.ciba.grant.enabled": "false", - "backchannel.logout.session.required": "false", - "client_credentials.use_refresh_token": "false", - "require.pushed.authorization.requests": "false", - "saml.client.signature": "false", - "saml.allow.ecp.flow": "false", - "id.token.as.detached.signature": "false", - "saml.assertion.signature": "false", - "saml.encrypt": "false", - "saml.server.signature": "false", - "exclude.session.state.from.auth.response": "false", - "saml.artifact.binding": "false", - "saml_force_name_id_format": "false", - "acr.loa.map": "{}", - "tls.client.certificate.bound.access.tokens": "false", - "saml.authnstatement": "false", - "display.on.consent.screen": "false", - "token.response.type.bearer.lower-case": "false", - "saml.onetimeuse.condition": "false" - }, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": false, - "nodeReRegistrationTimeout": 0, - "defaultClientScopes": [ - "web-origins", - "acr", - "profile", - "roles", - "email" - ], - "optionalClientScopes": [ - "address", - "phone", - "offline_access", - "microprofile-jwt" - ] - }, - { - "id": "02fa6179-9399-4bb1-970f-c4d8e8b5f99f", - "clientId": "admin-cli", - "name": "${client_admin-cli}", - "surrogateAuthRequired": false, - "enabled": false, - "alwaysDisplayInConsole": false, - "clientAuthenticatorType": "client-secret", - "redirectUris": [], - "webOrigins": [], - "notBefore": 0, - "bearerOnly": false, - "consentRequired": false, - "standardFlowEnabled": false, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": true, - "serviceAccountsEnabled": false, - "publicClient": true, - "frontchannelLogout": false, - "protocol": "openid-connect", - "attributes": { - "saml.force.post.binding": "false", - "saml.multivalued.roles": "false", - "frontchannel.logout.session.required": "false", - "post.logout.redirect.uris": "+", - "oauth2.device.authorization.grant.enabled": "false", - "backchannel.logout.revoke.offline.tokens": "false", - "saml.server.signature.keyinfo.ext": "false", - "use.refresh.tokens": "true", - "oidc.ciba.grant.enabled": "false", - "backchannel.logout.session.required": "false", - "client_credentials.use_refresh_token": "false", - "require.pushed.authorization.requests": "false", - "saml.client.signature": "false", - "saml.allow.ecp.flow": "false", - "id.token.as.detached.signature": "false", - "saml.assertion.signature": "false", - "saml.encrypt": "false", - "saml.server.signature": "false", - "exclude.session.state.from.auth.response": "false", - "saml.artifact.binding": "false", - "saml_force_name_id_format": "false", - "acr.loa.map": "{}", - "tls.client.certificate.bound.access.tokens": "false", - "saml.authnstatement": "false", - "display.on.consent.screen": "false", - "token.response.type.bearer.lower-case": "false", - "saml.onetimeuse.condition": "false" - }, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": false, - "nodeReRegistrationTimeout": 0, - "defaultClientScopes": [ - "web-origins", - "acr", - "profile", - "roles", - "email" - ], - "optionalClientScopes": [ - "address", - "phone", - "offline_access", - "microprofile-jwt" - ] - }, - { - "id": "55d75754-cf1b-4875-bf3e-15add4be8c99", - "clientId": "broker", - "name": "${client_broker}", - "surrogateAuthRequired": false, - "enabled": false, - "alwaysDisplayInConsole": false, - "clientAuthenticatorType": "client-secret", - "redirectUris": [], - "webOrigins": [], - "notBefore": 0, - "bearerOnly": true, - "consentRequired": false, - "standardFlowEnabled": true, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": false, - "serviceAccountsEnabled": false, - "publicClient": false, - "frontchannelLogout": false, - "protocol": "openid-connect", - "attributes": { - "saml.force.post.binding": "false", - "saml.multivalued.roles": "false", - "frontchannel.logout.session.required": "false", - "post.logout.redirect.uris": "+", - "oauth2.device.authorization.grant.enabled": "false", - "backchannel.logout.revoke.offline.tokens": "false", - "saml.server.signature.keyinfo.ext": "false", - "use.refresh.tokens": "true", - "oidc.ciba.grant.enabled": "false", - "backchannel.logout.session.required": "false", - "client_credentials.use_refresh_token": "false", - "require.pushed.authorization.requests": "false", - "saml.client.signature": "false", - "saml.allow.ecp.flow": "false", - "id.token.as.detached.signature": "false", - "saml.assertion.signature": "false", - "saml.encrypt": "false", - "saml.server.signature": "false", - "exclude.session.state.from.auth.response": "false", - "saml.artifact.binding": "false", - "saml_force_name_id_format": "false", - "acr.loa.map": "{}", - "tls.client.certificate.bound.access.tokens": "false", - "saml.authnstatement": "false", - "display.on.consent.screen": "false", - "token.response.type.bearer.lower-case": "false", - "saml.onetimeuse.condition": "false" - }, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": false, - "nodeReRegistrationTimeout": 0, - "defaultClientScopes": [ - "web-origins", - "acr", - "profile", - "roles", - "email" - ], - "optionalClientScopes": [ - "address", - "phone", - "offline_access", - "microprofile-jwt" - ] - }, - { - "id": "4ce68130-aced-4e67-936a-8082dc843cc2", - "clientId": "realm-management", - "name": "${client_realm-management}", - "surrogateAuthRequired": false, - "enabled": false, - "alwaysDisplayInConsole": false, - "clientAuthenticatorType": "client-secret", - "redirectUris": [], - "webOrigins": [], - "notBefore": 0, - "bearerOnly": true, - "consentRequired": false, - "standardFlowEnabled": true, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": false, - "serviceAccountsEnabled": false, - "publicClient": false, - "frontchannelLogout": false, - "protocol": "openid-connect", - "attributes": { - "saml.force.post.binding": "false", - "saml.multivalued.roles": "false", - "frontchannel.logout.session.required": "false", - "post.logout.redirect.uris": "+", - "oauth2.device.authorization.grant.enabled": "false", - "backchannel.logout.revoke.offline.tokens": "false", - "saml.server.signature.keyinfo.ext": "false", - "use.refresh.tokens": "true", - "oidc.ciba.grant.enabled": "false", - "backchannel.logout.session.required": "false", - "client_credentials.use_refresh_token": "false", - "require.pushed.authorization.requests": "false", - "saml.client.signature": "false", - "saml.allow.ecp.flow": "false", - "id.token.as.detached.signature": "false", - "saml.assertion.signature": "false", - "saml.encrypt": "false", - "saml.server.signature": "false", - "exclude.session.state.from.auth.response": "false", - "saml.artifact.binding": "false", - "saml_force_name_id_format": "false", - "acr.loa.map": "{}", - "tls.client.certificate.bound.access.tokens": "false", - "saml.authnstatement": "false", - "display.on.consent.screen": "false", - "token.response.type.bearer.lower-case": "false", - "saml.onetimeuse.condition": "false" - }, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": false, - "nodeReRegistrationTimeout": 0, - "defaultClientScopes": [ - "web-origins", - "acr", - "profile", - "roles", - "email" - ], - "optionalClientScopes": [ - "address", - "phone", - "offline_access", - "microprofile-jwt" - ] - }, - { - "id": "7c82344d-d4ae-4599-bbce-583cc8848199", - "clientId": "security-admin-console", - "name": "${client_security-admin-console}", - "rootUrl": "${authAdminUrl}", - "baseUrl": "/admin/spiffworkflow/console/", - "surrogateAuthRequired": false, - "enabled": false, - "alwaysDisplayInConsole": false, - "clientAuthenticatorType": "client-secret", - "redirectUris": ["/admin/spiffworkflow/console/*"], - "webOrigins": ["+"], - "notBefore": 0, - "bearerOnly": false, - "consentRequired": false, - "standardFlowEnabled": true, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": false, - "serviceAccountsEnabled": false, - "publicClient": true, - "frontchannelLogout": false, - "protocol": "openid-connect", - "attributes": { - "saml.force.post.binding": "false", - "saml.multivalued.roles": "false", - "frontchannel.logout.session.required": "false", - "post.logout.redirect.uris": "+", - "oauth2.device.authorization.grant.enabled": "false", - "backchannel.logout.revoke.offline.tokens": "false", - "saml.server.signature.keyinfo.ext": "false", - "use.refresh.tokens": "true", - "oidc.ciba.grant.enabled": "false", - "backchannel.logout.session.required": "false", - "client_credentials.use_refresh_token": "false", - "require.pushed.authorization.requests": "false", - "saml.client.signature": "false", - "pkce.code.challenge.method": "S256", - "saml.allow.ecp.flow": "false", - "id.token.as.detached.signature": "false", - "saml.assertion.signature": "false", - "saml.encrypt": "false", - "saml.server.signature": "false", - "exclude.session.state.from.auth.response": "false", - "saml.artifact.binding": "false", - "saml_force_name_id_format": "false", - "acr.loa.map": "{}", - "tls.client.certificate.bound.access.tokens": "false", - "saml.authnstatement": "false", - "display.on.consent.screen": "false", - "token.response.type.bearer.lower-case": "false", - "saml.onetimeuse.condition": "false" - }, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": false, - "nodeReRegistrationTimeout": 0, - "protocolMappers": [ - { - "id": "949c8afa-a06e-4a86-9260-6f477fc9ad9d", - "name": "locale", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "locale", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "locale", - "jsonType.label": "String" - } - } - ], - "defaultClientScopes": [ - "web-origins", - "acr", - "profile", - "roles", - "email" - ], - "optionalClientScopes": [ - "address", - "phone", - "offline_access", - "microprofile-jwt" - ] - }, - { - "id": "f44558af-3601-4e54-b854-08396a247544", - "clientId": "spiffworkflow-backend", - "surrogateAuthRequired": false, - "enabled": true, - "alwaysDisplayInConsole": false, - "clientAuthenticatorType": "client-secret", - "secret": "JXeQExm0JhQPLumgHtIIqf52bDalHz0q", - "redirectUris": [ - "http://localhost:7000/*", - "http://67.205.133.116:7000/*", - "http://167.172.242.138:7000/*", - "https://api.{{SPIFF_SUBDOMAIN}}.spiffworkflow.org/*", - "https://api.demo.spiffworkflow.org/*" - ], - "webOrigins": [], - "notBefore": 0, - "bearerOnly": false, - "consentRequired": false, - "standardFlowEnabled": true, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": true, - "serviceAccountsEnabled": true, - "authorizationServicesEnabled": true, - "publicClient": false, - "frontchannelLogout": false, - "protocol": "openid-connect", - "attributes": { - "saml.force.post.binding": "false", - "saml.multivalued.roles": "false", - "frontchannel.logout.session.required": "false", - "post.logout.redirect.uris": "+", - "oauth2.device.authorization.grant.enabled": "false", - "backchannel.logout.revoke.offline.tokens": "false", - "saml.server.signature.keyinfo.ext": "false", - "use.refresh.tokens": "true", - "oidc.ciba.grant.enabled": "false", - "backchannel.logout.session.required": "true", - "client_credentials.use_refresh_token": "false", - "require.pushed.authorization.requests": "false", - "saml.client.signature": "false", - "saml.allow.ecp.flow": "false", - "id.token.as.detached.signature": "false", - "saml.assertion.signature": "false", - "client.secret.creation.time": "1657115173", - "saml.encrypt": "false", - "saml.server.signature": "false", - "exclude.session.state.from.auth.response": "false", - "saml.artifact.binding": "false", - "saml_force_name_id_format": "false", - "acr.loa.map": "{}", - "tls.client.certificate.bound.access.tokens": "false", - "saml.authnstatement": "false", - "display.on.consent.screen": "false", - "token.response.type.bearer.lower-case": "false", - "saml.onetimeuse.condition": "false" - }, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": true, - "nodeReRegistrationTimeout": -1, - "protocolMappers": [ - { - "id": "af3598ab-74a9-48ba-956f-431b14acd896", - "name": "Client IP Address", - "protocol": "openid-connect", - "protocolMapper": "oidc-usersessionmodel-note-mapper", - "consentRequired": false, - "config": { - "user.session.note": "clientAddress", - "userinfo.token.claim": "true", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "clientAddress", - "jsonType.label": "String" - } - }, - { - "id": "87369cf7-2a77-40fd-a926-a26d689831a0", - "name": "Client Host", - "protocol": "openid-connect", - "protocolMapper": "oidc-usersessionmodel-note-mapper", - "consentRequired": false, - "config": { - "user.session.note": "clientHost", - "userinfo.token.claim": "true", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "clientHost", - "jsonType.label": "String" - } - }, - { - "id": "2c78d7e8-0a99-43bd-bc29-0ba062ed8750", - "name": "Client ID", - "protocol": "openid-connect", - "protocolMapper": "oidc-usersessionmodel-note-mapper", - "consentRequired": false, - "config": { - "user.session.note": "clientId", - "userinfo.token.claim": "true", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "clientId", - "jsonType.label": "String" - } - } - ], - "defaultClientScopes": [ - "web-origins", - "acr", - "profile", - "roles", - "email" - ], - "optionalClientScopes": [ - "address", - "phone", - "offline_access", - "microprofile-jwt" - ], - "authorizationSettings": { - "allowRemoteResourceManagement": true, - "policyEnforcementMode": "ENFORCING", - "resources": [ - { - "name": "everything", - "ownerManagedAccess": false, - "attributes": {}, - "_id": "446bdcf4-a3bd-41c7-a0f8-67a225ba6b57", - "uris": ["/*"], - "scopes": [ - { - "name": "read" - }, - { - "name": "update" - }, - { - "name": "delete" - }, - { - "name": "instantiate" - } - ] - }, - { - "name": "Default Resource", - "type": "urn:spiffworkflow-backend:resources:default", - "ownerManagedAccess": false, - "attributes": {}, - "_id": "8e00e4a3-3fff-4521-b7f0-95f66c2f79d2", - "uris": ["/*"] - }, - { - "name": "process-model-with-repeating-form-crud", - "type": "process-model", - "ownerManagedAccess": false, - "displayName": "process-model-with-repeating-form-crud", - "attributes": { - "test_resource_att1": ["this_is_the_value"] - }, - "_id": "e294304c-796e-4c56-bdf2-8c854f65db59", - "uris": [ - "/process-models/category_number_one/process-model-with-repeating-form" - ], - "scopes": [ - { - "name": "read" - }, - { - "name": "update" - }, - { - "name": "delete" - }, - { - "name": "instantiate" - } - ] - } - ], - "policies": [ - { - "id": "048d043e-d98c-44d8-8c85-656ba117053e", - "name": "repeat-form-role-policy", - "type": "role", - "logic": "POSITIVE", - "decisionStrategy": "UNANIMOUS", - "config": { - "roles": "[{\"id\":\"spiffworkflow-backend/repeat-form-role-2\",\"required\":false}]" - } - }, - { - "id": "ac55237b-6ec9-4f66-bb8e-bee94a5bb5e9", - "name": "admins have everything", - "type": "role", - "logic": "POSITIVE", - "decisionStrategy": "UNANIMOUS", - "config": { - "roles": "[{\"id\":\"spiffworkflow-backend/spiffworkflow-admin\",\"required\":false}]" - } - }, - { - "id": "7dac9bea-d415-4bc4-8817-7a71c2b3ce32", - "name": "Default Policy", - "description": "A policy that grants access only for users within this realm", - "type": "role", - "logic": "POSITIVE", - "decisionStrategy": "AFFIRMATIVE", - "config": { - "roles": "[{\"id\":\"spiffworkflow-backend/repeat-form-role-2\",\"required\":false}]" - } - }, - { - "id": "5133ae0b-5e90-48a6-bdd9-3f323e10c44d", - "name": "repeat-form-read", - "type": "scope", - "logic": "POSITIVE", - "decisionStrategy": "UNANIMOUS", - "config": { - "resources": "[\"process-model-with-repeating-form-crud\"]", - "scopes": "[\"read\"]", - "applyPolicies": "[\"repeat-form-role-policy\"]" - } - }, - { - "id": "0a86ae38-7460-4bc2-b1f9-f933531303ac", - "name": "all_permissions", - "type": "resource", - "logic": "POSITIVE", - "decisionStrategy": "UNANIMOUS", - "config": { - "resources": "[\"everything\"]", - "applyPolicies": "[\"admins have everything\"]" - } - }, - { - "id": "4b634627-51d9-4257-91d9-29503490e4fb", - "name": "Default Permission", - "description": "A permission that applies to the default resource type", - "type": "resource", - "logic": "POSITIVE", - "decisionStrategy": "UNANIMOUS", - "config": { - "defaultResourceType": "urn:spiffworkflow-backend:resources:default", - "applyPolicies": "[\"Default Policy\"]" - } - } - ], - "scopes": [ - { - "id": "c03b5c4e-f1bb-4066-8666-3c8a6f44ddb3", - "name": "read", - "displayName": "read" - }, - { - "id": "f55c3e81-9257-4618-9acb-32c57fc561a6", - "name": "update", - "displayName": "update" - }, - { - "id": "c8628417-7ffa-4675-9cda-955df62ea1db", - "name": "delete", - "displayName": "delete" - }, - { - "id": "50ef4129-aa88-4ecd-9afe-c7e5a1b66142", - "name": "instantiate", - "displayName": "instantiate" - } - ], - "decisionStrategy": "UNANIMOUS" + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "protocolMappers" : [ { + "id" : "949c8afa-a06e-4a86-9260-6f477fc9ad9d", + "name" : "locale", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "locale", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "locale", + "jsonType.label" : "String" } + } ], + "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "f44558af-3601-4e54-b854-08396a247544", + "clientId" : "spiffworkflow-backend", + "name" : "", + "description" : "", + "rootUrl" : "", + "adminUrl" : "", + "baseUrl" : "", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "secret" : "JXeQExm0JhQPLumgHtIIqf52bDalHz0q", + "redirectUris" : [ "http://localhost:7000/*", "https://api.unused-for-local-dev.spiffworkflow.org/*", "https://api.replace-me-with-spiff-subdomain.spiffworkflow.org/*", "http://67.205.133.116:7000/*", "http://167.172.242.138:7000/*" ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : true, + "serviceAccountsEnabled" : true, + "authorizationServicesEnabled" : true, + "publicClient" : false, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "saml.force.post.binding" : "false", + "saml.multivalued.roles" : "false", + "frontchannel.logout.session.required" : "false", + "post.logout.redirect.uris" : "https://replace-me-with-spiff-subdomain.spiffworkflow.org/*##http://localhost:7001/*", + "oauth2.device.authorization.grant.enabled" : "false", + "backchannel.logout.revoke.offline.tokens" : "false", + "saml.server.signature.keyinfo.ext" : "false", + "use.refresh.tokens" : "true", + "oidc.ciba.grant.enabled" : "false", + "backchannel.logout.session.required" : "true", + "client_credentials.use_refresh_token" : "false", + "require.pushed.authorization.requests" : "false", + "saml.client.signature" : "false", + "saml.allow.ecp.flow" : "false", + "id.token.as.detached.signature" : "false", + "saml.assertion.signature" : "false", + "client.secret.creation.time" : "1657115173", + "saml.encrypt" : "false", + "saml.server.signature" : "false", + "exclude.session.state.from.auth.response" : "false", + "saml.artifact.binding" : "false", + "saml_force_name_id_format" : "false", + "acr.loa.map" : "{}", + "tls.client.certificate.bound.access.tokens" : "false", + "saml.authnstatement" : "false", + "display.on.consent.screen" : "false", + "token.response.type.bearer.lower-case" : "false", + "saml.onetimeuse.condition" : "false" }, - { - "id": "9f340eba-2b84-43d0-a976-010e270e3981", - "clientId": "spiffworkflow-frontend", - "surrogateAuthRequired": false, - "enabled": true, - "alwaysDisplayInConsole": false, - "clientAuthenticatorType": "client-secret", - "redirectUris": [ - "http://localhost:7001/*", - "http://67.205.133.116:7000/*", - "http://167.172.242.138:7001/*", - "https://api.{{SPIFF_SUBDOMAIN}}.spiffworkflow.org/*", - "https://api.demo.spiffworkflow.org/*" - ], - "webOrigins": ["*"], - "notBefore": 0, - "bearerOnly": false, - "consentRequired": false, - "standardFlowEnabled": true, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": true, - "serviceAccountsEnabled": false, - "publicClient": true, - "frontchannelLogout": false, - "protocol": "openid-connect", - "attributes": { - "saml.force.post.binding": "false", - "saml.multivalued.roles": "false", - "frontchannel.logout.session.required": "false", - "post.logout.redirect.uris": "+", - "oauth2.device.authorization.grant.enabled": "false", - "backchannel.logout.revoke.offline.tokens": "false", - "saml.server.signature.keyinfo.ext": "false", - "use.refresh.tokens": "true", - "oidc.ciba.grant.enabled": "false", - "backchannel.logout.session.required": "true", - "client_credentials.use_refresh_token": "false", - "require.pushed.authorization.requests": "false", - "saml.client.signature": "false", - "saml.allow.ecp.flow": "false", - "id.token.as.detached.signature": "false", - "saml.assertion.signature": "false", - "saml.encrypt": "false", - "saml.server.signature": "false", - "exclude.session.state.from.auth.response": "false", - "saml.artifact.binding": "false", - "saml_force_name_id_format": "false", - "acr.loa.map": "{}", - "tls.client.certificate.bound.access.tokens": "false", - "saml.authnstatement": "false", - "display.on.consent.screen": "false", - "token.response.type.bearer.lower-case": "false", - "saml.onetimeuse.condition": "false" - }, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": true, - "nodeReRegistrationTimeout": -1, - "defaultClientScopes": [ - "web-origins", - "acr", - "profile", - "roles", - "email" - ], - "optionalClientScopes": [ - "address", - "phone", - "offline_access", - "microprofile-jwt" - ] - }, - { - "id": "5d94a8c3-f56b-4eff-ac39-8580053a7fbe", - "clientId": "withAuth", - "surrogateAuthRequired": false, - "enabled": true, - "alwaysDisplayInConsole": false, - "clientAuthenticatorType": "client-secret", - "secret": "6o8kIKQznQtejHOdRhWeKorBJclMGcgA", - "redirectUris": [ - "http://localhost:7001/*", - "http://67.205.133.116:7000/*", - "http://167.172.242.138:7001/*", - "https://api.{{SPIFF_SUBDOMAIN}}.spiffworkflow.org/*", - "https://api.demo.spiffworkflow.org/*" - ], - "webOrigins": [], - "notBefore": 0, - "bearerOnly": false, - "consentRequired": false, - "standardFlowEnabled": true, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": true, - "serviceAccountsEnabled": true, - "authorizationServicesEnabled": true, - "publicClient": false, - "frontchannelLogout": false, - "protocol": "openid-connect", - "attributes": { - "saml.force.post.binding": "false", - "saml.multivalued.roles": "false", - "frontchannel.logout.session.required": "false", - "post.logout.redirect.uris": "+", - "oauth2.device.authorization.grant.enabled": "false", - "backchannel.logout.revoke.offline.tokens": "false", - "saml.server.signature.keyinfo.ext": "false", - "use.refresh.tokens": "true", - "oidc.ciba.grant.enabled": "false", - "backchannel.logout.session.required": "true", - "client_credentials.use_refresh_token": "false", - "require.pushed.authorization.requests": "false", - "saml.client.signature": "false", - "saml.allow.ecp.flow": "false", - "id.token.as.detached.signature": "false", - "saml.assertion.signature": "false", - "client.secret.creation.time": "1657055472", - "saml.encrypt": "false", - "saml.server.signature": "false", - "exclude.session.state.from.auth.response": "false", - "saml.artifact.binding": "false", - "saml_force_name_id_format": "false", - "acr.loa.map": "{}", - "tls.client.certificate.bound.access.tokens": "false", - "saml.authnstatement": "false", - "display.on.consent.screen": "false", - "token.response.type.bearer.lower-case": "false", - "saml.onetimeuse.condition": "false" - }, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": true, - "nodeReRegistrationTimeout": -1, - "protocolMappers": [ - { - "id": "abfc756f-fc57-45b4-8a40-0cd0f8081f0c", - "name": "Client ID", - "protocol": "openid-connect", - "protocolMapper": "oidc-usersessionmodel-note-mapper", - "consentRequired": false, - "config": { - "user.session.note": "clientId", - "userinfo.token.claim": "true", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "clientId", - "jsonType.label": "String" - } - }, - { - "id": "c05d38b7-9b4d-4286-b40c-f48b3cca42e3", - "name": "Client Host", - "protocol": "openid-connect", - "protocolMapper": "oidc-usersessionmodel-note-mapper", - "consentRequired": false, - "config": { - "user.session.note": "clientHost", - "userinfo.token.claim": "true", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "clientHost", - "jsonType.label": "String" - } - }, - { - "id": "b27d0bd8-b8d9-43cb-a07a-3ec4bdc818dc", - "name": "Client IP Address", - "protocol": "openid-connect", - "protocolMapper": "oidc-usersessionmodel-note-mapper", - "consentRequired": false, - "config": { - "user.session.note": "clientAddress", - "userinfo.token.claim": "true", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "clientAddress", - "jsonType.label": "String" - } - } - ], - "defaultClientScopes": [ - "web-origins", - "acr", - "profile", - "roles", - "email" - ], - "optionalClientScopes": [ - "address", - "phone", - "offline_access", - "microprofile-jwt" - ], - "authorizationSettings": { - "allowRemoteResourceManagement": true, - "policyEnforcementMode": "ENFORCING", - "resources": [ - { - "name": "Default Resource", - "type": "urn:withAuth:resources:default", - "ownerManagedAccess": false, - "attributes": {}, - "_id": "c882ad40-c15d-4f88-ad60-c2ea2f486ce2", - "uris": ["/*"] - } - ], - "policies": [ - { - "id": "b8b338bc-884d-43cf-96d8-3776f2b220f3", - "name": "Default Policy", - "description": "A policy that grants access only for users within this realm", - "type": "role", - "logic": "POSITIVE", - "decisionStrategy": "AFFIRMATIVE", - "config": { - "roles": "[{\"id\":\"spiffworkflow-backend/repeat-form-role-2\",\"required\":false}]" - } - }, - { - "id": "4f5afa22-0fdf-4ed7-97b9-35400591bf6f", - "name": "Default Permission", - "description": "A permission that applies to the default resource type", - "type": "resource", - "logic": "POSITIVE", - "decisionStrategy": "UNANIMOUS", - "config": { - "defaultResourceType": "urn:withAuth:resources:default", - "applyPolicies": "[\"Default Policy\"]" - } - } - ], - "scopes": [], - "decisionStrategy": "UNANIMOUS" + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : true, + "nodeReRegistrationTimeout" : -1, + "protocolMappers" : [ { + "id" : "af3598ab-74a9-48ba-956f-431b14acd896", + "name" : "Client IP Address", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usersessionmodel-note-mapper", + "consentRequired" : false, + "config" : { + "user.session.note" : "clientAddress", + "userinfo.token.claim" : "true", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "clientAddress", + "jsonType.label" : "String" } - } - ], - "clientScopes": [ - { - "id": "fa3d9944-cf66-4af9-b931-1f3b02943e5b", - "name": "acr", - "description": "OpenID Connect scope for add acr (authentication context class reference) to the token", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "false", - "display.on.consent.screen": "false" - }, - "protocolMappers": [ - { - "id": "12ad0a69-d414-4b5b-9f5f-b647db5f8959", - "name": "acr loa level", - "protocol": "openid-connect", - "protocolMapper": "oidc-acr-mapper", - "consentRequired": false, - "config": { - "id.token.claim": "true", - "access.token.claim": "true", - "userinfo.token.claim": "true" - } - } - ] - }, - { - "id": "4e69d058-1229-4704-9411-decf25da0a49", - "name": "profile", - "description": "OpenID Connect built-in scope: profile", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "true", - "display.on.consent.screen": "true", - "consent.screen.text": "${profileScopeConsentText}" - }, - "protocolMappers": [ - { - "id": "d0d7334e-3f11-45d2-9670-46dbc1977cb2", - "name": "full name", - "protocol": "openid-connect", - "protocolMapper": "oidc-full-name-mapper", - "consentRequired": false, - "config": { - "id.token.claim": "true", - "access.token.claim": "true", - "userinfo.token.claim": "true" - } - }, - { - "id": "4efcf169-4df2-4cdb-b331-005aff1cee28", - "name": "website", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "website", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "website", - "jsonType.label": "String" - } - }, - { - "id": "3f639f2f-cf0e-4651-ab93-15a77023b5a0", - "name": "given name", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-property-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "firstName", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "given_name", - "jsonType.label": "String" - } - }, - { - "id": "16e93663-bf6a-4f6d-b5ab-8e68bf118f72", - "name": "nickname", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "nickname", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "nickname", - "jsonType.label": "String" - } - }, - { - "id": "b9c97283-8153-4c4d-b8d8-dd1bde17823b", - "name": "username", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-property-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "username", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "preferred_username", - "jsonType.label": "String" - } - }, - { - "id": "eeead6c7-1dae-4be1-9eca-988ffb38aaf4", - "name": "zoneinfo", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "zoneinfo", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "zoneinfo", - "jsonType.label": "String" - } - }, - { - "id": "d62991bc-2583-42be-bb08-8d1527c4f162", - "name": "family name", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-property-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "lastName", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "family_name", - "jsonType.label": "String" - } - }, - { - "id": "9f761222-f84d-4a25-a53f-13e196d38a46", - "name": "profile", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "profile", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "profile", - "jsonType.label": "String" - } - }, - { - "id": "ec866e3c-582f-4c99-920f-d57cf03d772d", - "name": "gender", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "gender", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "gender", - "jsonType.label": "String" - } - }, - { - "id": "b05e679c-e00e-427e-8e47-0a4fd411c7a6", - "name": "updated at", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "updatedAt", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "updated_at", - "jsonType.label": "long" - } - }, - { - "id": "505ff402-5533-48ea-91f9-ab4804c3826b", - "name": "middle name", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "middleName", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "middle_name", - "jsonType.label": "String" - } - }, - { - "id": "d546af31-b669-442b-9a9d-8a6478364002", - "name": "picture", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "picture", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "picture", - "jsonType.label": "String" - } - }, - { - "id": "5a75c993-290f-4bfb-9044-5d7d269378b2", - "name": "birthdate", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "birthdate", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "birthdate", - "jsonType.label": "String" - } - }, - { - "id": "2d387240-0f2f-4f30-8464-0e7c57946743", - "name": "locale", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "locale", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "locale", - "jsonType.label": "String" - } - } - ] - }, - { - "id": "2efee39d-723c-44af-9eb1-4dde9635b249", - "name": "email", - "description": "OpenID Connect built-in scope: email", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "true", - "display.on.consent.screen": "true", - "consent.screen.text": "${emailScopeConsentText}" - }, - "protocolMappers": [ - { - "id": "5bf7db0f-a915-43c2-bff4-475ee5c3259b", - "name": "email", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-property-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "email", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "email", - "jsonType.label": "String" - } - }, - { - "id": "687a8c7d-c93f-47d9-a176-78b0954429c7", - "name": "email verified", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-property-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "emailVerified", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "email_verified", - "jsonType.label": "boolean" - } - } - ] - }, - { - "id": "4a7737cf-83e3-40e1-b36d-9566b34e4148", - "name": "phone", - "description": "OpenID Connect built-in scope: phone", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "true", - "display.on.consent.screen": "true", - "consent.screen.text": "${phoneScopeConsentText}" - }, - "protocolMappers": [ - { - "id": "14bd2816-a2f3-4fde-9ac2-452dea2e9e58", - "name": "phone number", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "phoneNumber", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "phone_number", - "jsonType.label": "String" - } - }, - { - "id": "6172e315-8999-4df8-89fa-75ffd1981793", - "name": "phone number verified", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "phoneNumberVerified", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "phone_number_verified", - "jsonType.label": "boolean" - } - } - ] - }, - { - "id": "5ad0c621-d3ec-4018-98c8-d6fb630d661f", - "name": "microprofile-jwt", - "description": "Microprofile - JWT built-in scope", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "true", - "display.on.consent.screen": "false" - }, - "protocolMappers": [ - { - "id": "252fdd9f-cc91-4ca3-aaab-cdf053360e94", - "name": "groups", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-realm-role-mapper", - "consentRequired": false, - "config": { - "multivalued": "true", - "userinfo.token.claim": "true", - "user.attribute": "foo", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "groups", - "jsonType.label": "String" - } - }, - { - "id": "8e9b880e-6dd8-4e2f-ade2-77fc8fd0bc6d", - "name": "upn", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-property-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "username", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "upn", - "jsonType.label": "String" - } - } - ] - }, - { - "id": "77ca4f26-3777-451b-a907-e258f46f7b95", - "name": "roles", - "description": "OpenID Connect scope for add user roles to the access token", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "false", - "display.on.consent.screen": "true", - "consent.screen.text": "${rolesScopeConsentText}" - }, - "protocolMappers": [ - { - "id": "e7ebb9c0-5ed3-4c6f-bb69-22e01d26b49f", - "name": "audience resolve", - "protocol": "openid-connect", - "protocolMapper": "oidc-audience-resolve-mapper", - "consentRequired": false, - "config": {} - }, - { - "id": "66fd470f-419e-44cd-822e-43df8ee5fe1b", - "name": "realm roles", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-realm-role-mapper", - "consentRequired": false, - "config": { - "user.attribute": "foo", - "access.token.claim": "true", - "claim.name": "realm_access.roles", - "jsonType.label": "String", - "multivalued": "true" - } - }, - { - "id": "f3c313bc-7da7-4cf6-a0df-b62e77209b7c", - "name": "client roles", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-client-role-mapper", - "consentRequired": false, - "config": { - "user.attribute": "foo", - "access.token.claim": "true", - "claim.name": "resource_access.${client_id}.roles", - "jsonType.label": "String", - "multivalued": "true" - } - } - ] - }, - { - "id": "3e9849f5-15ff-43c6-b929-40f26fda2c05", - "name": "offline_access", - "description": "OpenID Connect built-in scope: offline_access", - "protocol": "openid-connect", - "attributes": { - "consent.screen.text": "${offlineAccessScopeConsentText}", - "display.on.consent.screen": "true" + }, { + "id" : "87369cf7-2a77-40fd-a926-a26d689831a0", + "name" : "Client Host", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usersessionmodel-note-mapper", + "consentRequired" : false, + "config" : { + "user.session.note" : "clientHost", + "userinfo.token.claim" : "true", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "clientHost", + "jsonType.label" : "String" } - }, - { - "id": "ffda6ea6-8add-4c7e-9754-66d00c6735a1", - "name": "web-origins", - "description": "OpenID Connect scope for add allowed web origins to the access token", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "false", - "display.on.consent.screen": "false", - "consent.screen.text": "" - }, - "protocolMappers": [ - { - "id": "05635d42-8bb3-440b-b871-b64c97f524da", - "name": "allowed web origins", - "protocol": "openid-connect", - "protocolMapper": "oidc-allowed-origins-mapper", - "consentRequired": false, - "config": {} + }, { + "id" : "2c78d7e8-0a99-43bd-bc29-0ba062ed8750", + "name" : "Client ID", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usersessionmodel-note-mapper", + "consentRequired" : false, + "config" : { + "user.session.note" : "clientId", + "userinfo.token.claim" : "true", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "clientId", + "jsonType.label" : "String" + } + } ], + "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ], + "authorizationSettings" : { + "allowRemoteResourceManagement" : true, + "policyEnforcementMode" : "ENFORCING", + "resources" : [ { + "name" : "everything", + "ownerManagedAccess" : false, + "attributes" : { }, + "_id" : "446bdcf4-a3bd-41c7-a0f8-67a225ba6b57", + "uris" : [ "/*" ], + "scopes" : [ { + "name" : "read" + }, { + "name" : "update" + }, { + "name" : "delete" + }, { + "name" : "instantiate" + } ] + }, { + "name" : "Default Resource", + "type" : "urn:spiffworkflow-backend:resources:default", + "ownerManagedAccess" : false, + "attributes" : { }, + "_id" : "8e00e4a3-3fff-4521-b7f0-95f66c2f79d2", + "uris" : [ "/*" ] + }, { + "name" : "process-model-with-repeating-form-crud", + "type" : "process-model", + "ownerManagedAccess" : false, + "displayName" : "process-model-with-repeating-form-crud", + "attributes" : { + "test_resource_att1" : [ "this_is_the_value" ] + }, + "_id" : "e294304c-796e-4c56-bdf2-8c854f65db59", + "uris" : [ "/process-models/category_number_one/process-model-with-repeating-form" ], + "scopes" : [ { + "name" : "read" + }, { + "name" : "update" + }, { + "name" : "delete" + }, { + "name" : "instantiate" + } ] + } ], + "policies" : [ { + "id" : "048d043e-d98c-44d8-8c85-656ba117053e", + "name" : "repeat-form-role-policy", + "type" : "role", + "logic" : "POSITIVE", + "decisionStrategy" : "UNANIMOUS", + "config" : { + "roles" : "[{\"id\":\"spiffworkflow-backend/repeat-form-role-2\",\"required\":false}]" } - ] - }, - { - "id": "6f56ae2b-253f-40f7-ba99-e8c5bbc71423", - "name": "role_list", - "description": "SAML role list", - "protocol": "saml", - "attributes": { - "consent.screen.text": "${samlRoleListScopeConsentText}", - "display.on.consent.screen": "true" - }, - "protocolMappers": [ - { - "id": "7036c17a-9306-4481-82a1-d8d9d77077e5", - "name": "role list", - "protocol": "saml", - "protocolMapper": "saml-role-list-mapper", - "consentRequired": false, - "config": { - "single": "false", - "attribute.nameformat": "Basic", - "attribute.name": "Role" - } + }, { + "id" : "ac55237b-6ec9-4f66-bb8e-bee94a5bb5e9", + "name" : "admins have everything", + "type" : "role", + "logic" : "POSITIVE", + "decisionStrategy" : "UNANIMOUS", + "config" : { + "roles" : "[{\"id\":\"spiffworkflow-backend/spiffworkflow-admin\",\"required\":false}]" } - ] - }, - { - "id": "ce4493c0-ccb4-45f9-a46e-a40cc3f6d4b2", - "name": "address", - "description": "OpenID Connect built-in scope: address", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "true", - "display.on.consent.screen": "true", - "consent.screen.text": "${addressScopeConsentText}" - }, - "protocolMappers": [ - { - "id": "8a0d3248-d231-40b2-9b8e-3d63bd5a5d12", - "name": "address", - "protocol": "openid-connect", - "protocolMapper": "oidc-address-mapper", - "consentRequired": false, - "config": { - "user.attribute.formatted": "formatted", - "user.attribute.country": "country", - "user.attribute.postal_code": "postal_code", - "userinfo.token.claim": "true", - "user.attribute.street": "street", - "id.token.claim": "true", - "user.attribute.region": "region", - "access.token.claim": "true", - "user.attribute.locality": "locality" - } + }, { + "id" : "7dac9bea-d415-4bc4-8817-7a71c2b3ce32", + "name" : "Default Policy", + "description" : "A policy that grants access only for users within this realm", + "type" : "role", + "logic" : "POSITIVE", + "decisionStrategy" : "AFFIRMATIVE", + "config" : { + "roles" : "[{\"id\":\"spiffworkflow-backend/repeat-form-role-2\",\"required\":false}]" } - ] + }, { + "id" : "5133ae0b-5e90-48a6-bdd9-3f323e10c44d", + "name" : "repeat-form-read", + "type" : "scope", + "logic" : "POSITIVE", + "decisionStrategy" : "UNANIMOUS", + "config" : { + "resources" : "[\"process-model-with-repeating-form-crud\"]", + "scopes" : "[\"read\"]", + "applyPolicies" : "[\"repeat-form-role-policy\"]" + } + }, { + "id" : "0a86ae38-7460-4bc2-b1f9-f933531303ac", + "name" : "all_permissions", + "type" : "resource", + "logic" : "POSITIVE", + "decisionStrategy" : "UNANIMOUS", + "config" : { + "resources" : "[\"everything\"]", + "applyPolicies" : "[\"admins have everything\"]" + } + }, { + "id" : "4b634627-51d9-4257-91d9-29503490e4fb", + "name" : "Default Permission", + "description" : "A permission that applies to the default resource type", + "type" : "resource", + "logic" : "POSITIVE", + "decisionStrategy" : "UNANIMOUS", + "config" : { + "defaultResourceType" : "urn:spiffworkflow-backend:resources:default", + "applyPolicies" : "[\"Default Policy\"]" + } + } ], + "scopes" : [ { + "id" : "c03b5c4e-f1bb-4066-8666-3c8a6f44ddb3", + "name" : "read", + "displayName" : "read" + }, { + "id" : "f55c3e81-9257-4618-9acb-32c57fc561a6", + "name" : "update", + "displayName" : "update" + }, { + "id" : "c8628417-7ffa-4675-9cda-955df62ea1db", + "name" : "delete", + "displayName" : "delete" + }, { + "id" : "50ef4129-aa88-4ecd-9afe-c7e5a1b66142", + "name" : "instantiate", + "displayName" : "instantiate" + } ], + "decisionStrategy" : "UNANIMOUS" } - ], - "defaultDefaultClientScopes": [ - "email", - "profile", - "role_list", - "roles", - "acr", - "web-origins" - ], - "defaultOptionalClientScopes": [ - "offline_access", - "phone", - "microprofile-jwt", - "address" - ], - "browserSecurityHeaders": { - "contentSecurityPolicyReportOnly": "", - "xContentTypeOptions": "nosniff", - "xRobotsTag": "none", - "xFrameOptions": "SAMEORIGIN", - "contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", - "xXSSProtection": "1; mode=block", - "strictTransportSecurity": "max-age=31536000; includeSubDomains" + }, { + "id" : "9f340eba-2b84-43d0-a976-010e270e3981", + "clientId" : "spiffworkflow-frontend", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ "https://api.unused-for-local-dev.spiffworkflow.org/*", "http://localhost:7001/*", "http://67.205.133.116:7000/*", "http://167.172.242.138:7001/*", "https://api.demo.spiffworkflow.org/*" ], + "webOrigins" : [ "*" ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : true, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "saml.force.post.binding" : "false", + "saml.multivalued.roles" : "false", + "frontchannel.logout.session.required" : "false", + "post.logout.redirect.uris" : "+", + "oauth2.device.authorization.grant.enabled" : "false", + "backchannel.logout.revoke.offline.tokens" : "false", + "saml.server.signature.keyinfo.ext" : "false", + "use.refresh.tokens" : "true", + "oidc.ciba.grant.enabled" : "false", + "backchannel.logout.session.required" : "true", + "client_credentials.use_refresh_token" : "false", + "require.pushed.authorization.requests" : "false", + "saml.client.signature" : "false", + "saml.allow.ecp.flow" : "false", + "id.token.as.detached.signature" : "false", + "saml.assertion.signature" : "false", + "saml.encrypt" : "false", + "saml.server.signature" : "false", + "exclude.session.state.from.auth.response" : "false", + "saml.artifact.binding" : "false", + "saml_force_name_id_format" : "false", + "acr.loa.map" : "{}", + "tls.client.certificate.bound.access.tokens" : "false", + "saml.authnstatement" : "false", + "display.on.consent.screen" : "false", + "token.response.type.bearer.lower-case" : "false", + "saml.onetimeuse.condition" : "false" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : true, + "nodeReRegistrationTimeout" : -1, + "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "5d94a8c3-f56b-4eff-ac39-8580053a7fbe", + "clientId" : "withAuth", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "secret" : "6o8kIKQznQtejHOdRhWeKorBJclMGcgA", + "redirectUris" : [ "https://api.unused-for-local-dev.spiffworkflow.org/*", "http://localhost:7001/*", "http://67.205.133.116:7000/*", "http://167.172.242.138:7001/*", "https://api.demo.spiffworkflow.org/*" ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : true, + "serviceAccountsEnabled" : true, + "authorizationServicesEnabled" : true, + "publicClient" : false, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "saml.force.post.binding" : "false", + "saml.multivalued.roles" : "false", + "frontchannel.logout.session.required" : "false", + "post.logout.redirect.uris" : "+", + "oauth2.device.authorization.grant.enabled" : "false", + "backchannel.logout.revoke.offline.tokens" : "false", + "saml.server.signature.keyinfo.ext" : "false", + "use.refresh.tokens" : "true", + "oidc.ciba.grant.enabled" : "false", + "backchannel.logout.session.required" : "true", + "client_credentials.use_refresh_token" : "false", + "require.pushed.authorization.requests" : "false", + "saml.client.signature" : "false", + "saml.allow.ecp.flow" : "false", + "id.token.as.detached.signature" : "false", + "saml.assertion.signature" : "false", + "client.secret.creation.time" : "1657055472", + "saml.encrypt" : "false", + "saml.server.signature" : "false", + "exclude.session.state.from.auth.response" : "false", + "saml.artifact.binding" : "false", + "saml_force_name_id_format" : "false", + "acr.loa.map" : "{}", + "tls.client.certificate.bound.access.tokens" : "false", + "saml.authnstatement" : "false", + "display.on.consent.screen" : "false", + "token.response.type.bearer.lower-case" : "false", + "saml.onetimeuse.condition" : "false" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : true, + "nodeReRegistrationTimeout" : -1, + "protocolMappers" : [ { + "id" : "abfc756f-fc57-45b4-8a40-0cd0f8081f0c", + "name" : "Client ID", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usersessionmodel-note-mapper", + "consentRequired" : false, + "config" : { + "user.session.note" : "clientId", + "userinfo.token.claim" : "true", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "clientId", + "jsonType.label" : "String" + } + }, { + "id" : "c05d38b7-9b4d-4286-b40c-f48b3cca42e3", + "name" : "Client Host", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usersessionmodel-note-mapper", + "consentRequired" : false, + "config" : { + "user.session.note" : "clientHost", + "userinfo.token.claim" : "true", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "clientHost", + "jsonType.label" : "String" + } + }, { + "id" : "b27d0bd8-b8d9-43cb-a07a-3ec4bdc818dc", + "name" : "Client IP Address", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usersessionmodel-note-mapper", + "consentRequired" : false, + "config" : { + "user.session.note" : "clientAddress", + "userinfo.token.claim" : "true", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "clientAddress", + "jsonType.label" : "String" + } + } ], + "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ], + "authorizationSettings" : { + "allowRemoteResourceManagement" : true, + "policyEnforcementMode" : "ENFORCING", + "resources" : [ { + "name" : "Default Resource", + "type" : "urn:withAuth:resources:default", + "ownerManagedAccess" : false, + "attributes" : { }, + "_id" : "c882ad40-c15d-4f88-ad60-c2ea2f486ce2", + "uris" : [ "/*" ] + } ], + "policies" : [ { + "id" : "b8b338bc-884d-43cf-96d8-3776f2b220f3", + "name" : "Default Policy", + "description" : "A policy that grants access only for users within this realm", + "type" : "role", + "logic" : "POSITIVE", + "decisionStrategy" : "AFFIRMATIVE", + "config" : { + "roles" : "[{\"id\":\"spiffworkflow-backend/repeat-form-role-2\",\"required\":false}]" + } + }, { + "id" : "4f5afa22-0fdf-4ed7-97b9-35400591bf6f", + "name" : "Default Permission", + "description" : "A permission that applies to the default resource type", + "type" : "resource", + "logic" : "POSITIVE", + "decisionStrategy" : "UNANIMOUS", + "config" : { + "defaultResourceType" : "urn:withAuth:resources:default", + "applyPolicies" : "[\"Default Policy\"]" + } + } ], + "scopes" : [ ], + "decisionStrategy" : "UNANIMOUS" + } + } ], + "clientScopes" : [ { + "id" : "fa3d9944-cf66-4af9-b931-1f3b02943e5b", + "name" : "acr", + "description" : "OpenID Connect scope for add acr (authentication context class reference) to the token", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "false", + "display.on.consent.screen" : "false" + }, + "protocolMappers" : [ { + "id" : "12ad0a69-d414-4b5b-9f5f-b647db5f8959", + "name" : "acr loa level", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-acr-mapper", + "consentRequired" : false, + "config" : { + "id.token.claim" : "true", + "access.token.claim" : "true", + "userinfo.token.claim" : "true" + } + } ] + }, { + "id" : "4e69d058-1229-4704-9411-decf25da0a49", + "name" : "profile", + "description" : "OpenID Connect built-in scope: profile", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "true", + "consent.screen.text" : "${profileScopeConsentText}" + }, + "protocolMappers" : [ { + "id" : "d0d7334e-3f11-45d2-9670-46dbc1977cb2", + "name" : "full name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-full-name-mapper", + "consentRequired" : false, + "config" : { + "id.token.claim" : "true", + "access.token.claim" : "true", + "userinfo.token.claim" : "true" + } + }, { + "id" : "4efcf169-4df2-4cdb-b331-005aff1cee28", + "name" : "website", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "website", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "website", + "jsonType.label" : "String" + } + }, { + "id" : "3f639f2f-cf0e-4651-ab93-15a77023b5a0", + "name" : "given name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "firstName", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "given_name", + "jsonType.label" : "String" + } + }, { + "id" : "16e93663-bf6a-4f6d-b5ab-8e68bf118f72", + "name" : "nickname", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "nickname", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "nickname", + "jsonType.label" : "String" + } + }, { + "id" : "b9c97283-8153-4c4d-b8d8-dd1bde17823b", + "name" : "username", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "username", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "preferred_username", + "jsonType.label" : "String" + } + }, { + "id" : "eeead6c7-1dae-4be1-9eca-988ffb38aaf4", + "name" : "zoneinfo", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "zoneinfo", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "zoneinfo", + "jsonType.label" : "String" + } + }, { + "id" : "d62991bc-2583-42be-bb08-8d1527c4f162", + "name" : "family name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "lastName", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "family_name", + "jsonType.label" : "String" + } + }, { + "id" : "9f761222-f84d-4a25-a53f-13e196d38a46", + "name" : "profile", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "profile", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "profile", + "jsonType.label" : "String" + } + }, { + "id" : "ec866e3c-582f-4c99-920f-d57cf03d772d", + "name" : "gender", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "gender", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "gender", + "jsonType.label" : "String" + } + }, { + "id" : "b05e679c-e00e-427e-8e47-0a4fd411c7a6", + "name" : "updated at", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "updatedAt", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "updated_at", + "jsonType.label" : "long" + } + }, { + "id" : "505ff402-5533-48ea-91f9-ab4804c3826b", + "name" : "middle name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "middleName", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "middle_name", + "jsonType.label" : "String" + } + }, { + "id" : "d546af31-b669-442b-9a9d-8a6478364002", + "name" : "picture", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "picture", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "picture", + "jsonType.label" : "String" + } + }, { + "id" : "5a75c993-290f-4bfb-9044-5d7d269378b2", + "name" : "birthdate", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "birthdate", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "birthdate", + "jsonType.label" : "String" + } + }, { + "id" : "2d387240-0f2f-4f30-8464-0e7c57946743", + "name" : "locale", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "locale", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "locale", + "jsonType.label" : "String" + } + } ] + }, { + "id" : "2efee39d-723c-44af-9eb1-4dde9635b249", + "name" : "email", + "description" : "OpenID Connect built-in scope: email", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "true", + "consent.screen.text" : "${emailScopeConsentText}" + }, + "protocolMappers" : [ { + "id" : "5bf7db0f-a915-43c2-bff4-475ee5c3259b", + "name" : "email", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "email", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "email", + "jsonType.label" : "String" + } + }, { + "id" : "687a8c7d-c93f-47d9-a176-78b0954429c7", + "name" : "email verified", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "emailVerified", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "email_verified", + "jsonType.label" : "boolean" + } + } ] + }, { + "id" : "4a7737cf-83e3-40e1-b36d-9566b34e4148", + "name" : "phone", + "description" : "OpenID Connect built-in scope: phone", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "true", + "consent.screen.text" : "${phoneScopeConsentText}" + }, + "protocolMappers" : [ { + "id" : "14bd2816-a2f3-4fde-9ac2-452dea2e9e58", + "name" : "phone number", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "phoneNumber", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "phone_number", + "jsonType.label" : "String" + } + }, { + "id" : "6172e315-8999-4df8-89fa-75ffd1981793", + "name" : "phone number verified", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "phoneNumberVerified", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "phone_number_verified", + "jsonType.label" : "boolean" + } + } ] + }, { + "id" : "5ad0c621-d3ec-4018-98c8-d6fb630d661f", + "name" : "microprofile-jwt", + "description" : "Microprofile - JWT built-in scope", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "false" + }, + "protocolMappers" : [ { + "id" : "252fdd9f-cc91-4ca3-aaab-cdf053360e94", + "name" : "groups", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-realm-role-mapper", + "consentRequired" : false, + "config" : { + "multivalued" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "foo", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "groups", + "jsonType.label" : "String" + } + }, { + "id" : "8e9b880e-6dd8-4e2f-ade2-77fc8fd0bc6d", + "name" : "upn", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "username", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "upn", + "jsonType.label" : "String" + } + } ] + }, { + "id" : "77ca4f26-3777-451b-a907-e258f46f7b95", + "name" : "roles", + "description" : "OpenID Connect scope for add user roles to the access token", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "false", + "display.on.consent.screen" : "true", + "consent.screen.text" : "${rolesScopeConsentText}" + }, + "protocolMappers" : [ { + "id" : "e7ebb9c0-5ed3-4c6f-bb69-22e01d26b49f", + "name" : "audience resolve", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-audience-resolve-mapper", + "consentRequired" : false, + "config" : { } + }, { + "id" : "66fd470f-419e-44cd-822e-43df8ee5fe1b", + "name" : "realm roles", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-realm-role-mapper", + "consentRequired" : false, + "config" : { + "user.attribute" : "foo", + "access.token.claim" : "true", + "claim.name" : "realm_access.roles", + "jsonType.label" : "String", + "multivalued" : "true" + } + }, { + "id" : "f3c313bc-7da7-4cf6-a0df-b62e77209b7c", + "name" : "client roles", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-client-role-mapper", + "consentRequired" : false, + "config" : { + "user.attribute" : "foo", + "access.token.claim" : "true", + "claim.name" : "resource_access.${client_id}.roles", + "jsonType.label" : "String", + "multivalued" : "true" + } + } ] + }, { + "id" : "3e9849f5-15ff-43c6-b929-40f26fda2c05", + "name" : "offline_access", + "description" : "OpenID Connect built-in scope: offline_access", + "protocol" : "openid-connect", + "attributes" : { + "consent.screen.text" : "${offlineAccessScopeConsentText}", + "display.on.consent.screen" : "true" + } + }, { + "id" : "ffda6ea6-8add-4c7e-9754-66d00c6735a1", + "name" : "web-origins", + "description" : "OpenID Connect scope for add allowed web origins to the access token", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "false", + "display.on.consent.screen" : "false", + "consent.screen.text" : "" + }, + "protocolMappers" : [ { + "id" : "05635d42-8bb3-440b-b871-b64c97f524da", + "name" : "allowed web origins", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-allowed-origins-mapper", + "consentRequired" : false, + "config" : { } + } ] + }, { + "id" : "6f56ae2b-253f-40f7-ba99-e8c5bbc71423", + "name" : "role_list", + "description" : "SAML role list", + "protocol" : "saml", + "attributes" : { + "consent.screen.text" : "${samlRoleListScopeConsentText}", + "display.on.consent.screen" : "true" + }, + "protocolMappers" : [ { + "id" : "7036c17a-9306-4481-82a1-d8d9d77077e5", + "name" : "role list", + "protocol" : "saml", + "protocolMapper" : "saml-role-list-mapper", + "consentRequired" : false, + "config" : { + "single" : "false", + "attribute.nameformat" : "Basic", + "attribute.name" : "Role" + } + } ] + }, { + "id" : "ce4493c0-ccb4-45f9-a46e-a40cc3f6d4b2", + "name" : "address", + "description" : "OpenID Connect built-in scope: address", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "true", + "consent.screen.text" : "${addressScopeConsentText}" + }, + "protocolMappers" : [ { + "id" : "8a0d3248-d231-40b2-9b8e-3d63bd5a5d12", + "name" : "address", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-address-mapper", + "consentRequired" : false, + "config" : { + "user.attribute.formatted" : "formatted", + "user.attribute.country" : "country", + "user.attribute.postal_code" : "postal_code", + "userinfo.token.claim" : "true", + "user.attribute.street" : "street", + "id.token.claim" : "true", + "user.attribute.region" : "region", + "access.token.claim" : "true", + "user.attribute.locality" : "locality" + } + } ] + } ], + "defaultDefaultClientScopes" : [ "email", "profile", "role_list", "roles", "acr", "web-origins" ], + "defaultOptionalClientScopes" : [ "offline_access", "phone", "microprofile-jwt", "address" ], + "browserSecurityHeaders" : { + "contentSecurityPolicyReportOnly" : "", + "xContentTypeOptions" : "nosniff", + "xRobotsTag" : "none", + "xFrameOptions" : "SAMEORIGIN", + "contentSecurityPolicy" : "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", + "xXSSProtection" : "1; mode=block", + "strictTransportSecurity" : "max-age=31536000; includeSubDomains" }, - "smtpServer": {}, - "eventsEnabled": false, - "eventsListeners": ["jboss-logging"], - "enabledEventTypes": [], - "adminEventsEnabled": false, - "adminEventsDetailsEnabled": false, - "identityProviders": [], - "identityProviderMappers": [], - "components": { - "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy": [ - { - "id": "b8617465-1c84-4a5f-a16f-a6f10f0f66b1", - "name": "Trusted Hosts", - "providerId": "trusted-hosts", - "subType": "anonymous", - "subComponents": {}, - "config": { - "host-sending-registration-request-must-match": ["true"], - "client-uris-must-match": ["true"] - } - }, - { - "id": "6061713a-c1f5-46e1-adfb-762b8768976a", - "name": "Allowed Protocol Mapper Types", - "providerId": "allowed-protocol-mappers", - "subType": "authenticated", - "subComponents": {}, - "config": { - "allowed-protocol-mapper-types": [ - "oidc-full-name-mapper", - "oidc-usermodel-property-mapper", - "oidc-sha256-pairwise-sub-mapper", - "saml-user-property-mapper", - "oidc-usermodel-attribute-mapper", - "oidc-address-mapper", - "saml-role-list-mapper", - "saml-user-attribute-mapper" - ] - } - }, - { - "id": "d68e938d-dde6-47d9-bdc8-8e8523eb08cd", - "name": "Max Clients Limit", - "providerId": "max-clients", - "subType": "anonymous", - "subComponents": {}, - "config": { - "max-clients": ["200"] - } - }, - { - "id": "1209fa5d-37df-4f9a-b4fa-4a3cd94e21fe", - "name": "Allowed Protocol Mapper Types", - "providerId": "allowed-protocol-mappers", - "subType": "anonymous", - "subComponents": {}, - "config": { - "allowed-protocol-mapper-types": [ - "oidc-full-name-mapper", - "saml-user-property-mapper", - "oidc-usermodel-attribute-mapper", - "saml-role-list-mapper", - "oidc-sha256-pairwise-sub-mapper", - "oidc-usermodel-property-mapper", - "saml-user-attribute-mapper", - "oidc-address-mapper" - ] - } - }, - { - "id": "3854361d-3fe5-47fb-9417-a99592e3dc5c", - "name": "Allowed Client Scopes", - "providerId": "allowed-client-templates", - "subType": "authenticated", - "subComponents": {}, - "config": { - "allow-default-scopes": ["true"] - } - }, - { - "id": "4c4076ec-68ed-46c1-b0a5-3c8ed08dd4f6", - "name": "Consent Required", - "providerId": "consent-required", - "subType": "anonymous", - "subComponents": {}, - "config": {} - }, - { - "id": "bbbe2ea2-2a36-494b-b57f-8b202740ebf4", - "name": "Full Scope Disabled", - "providerId": "scope", - "subType": "anonymous", - "subComponents": {}, - "config": {} - }, - { - "id": "41eef3e1-bf71-4e8a-b729-fea8eb16b5d8", - "name": "Allowed Client Scopes", - "providerId": "allowed-client-templates", - "subType": "anonymous", - "subComponents": {}, - "config": { - "allow-default-scopes": ["true"] - } + "smtpServer" : { }, + "eventsEnabled" : false, + "eventsListeners" : [ "jboss-logging" ], + "enabledEventTypes" : [ ], + "adminEventsEnabled" : false, + "adminEventsDetailsEnabled" : false, + "identityProviders" : [ ], + "identityProviderMappers" : [ ], + "components" : { + "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy" : [ { + "id" : "b8617465-1c84-4a5f-a16f-a6f10f0f66b1", + "name" : "Trusted Hosts", + "providerId" : "trusted-hosts", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "host-sending-registration-request-must-match" : [ "true" ], + "client-uris-must-match" : [ "true" ] } - ], - "org.keycloak.userprofile.UserProfileProvider": [ - { - "id": "576f8c6a-00e6-45dd-a63d-614100fb2cc4", - "providerId": "declarative-user-profile", - "subComponents": {}, - "config": {} + }, { + "id" : "6061713a-c1f5-46e1-adfb-762b8768976a", + "name" : "Allowed Protocol Mapper Types", + "providerId" : "allowed-protocol-mappers", + "subType" : "authenticated", + "subComponents" : { }, + "config" : { + "allowed-protocol-mapper-types" : [ "oidc-usermodel-attribute-mapper", "oidc-sha256-pairwise-sub-mapper", "saml-user-property-mapper", "oidc-usermodel-property-mapper", "saml-role-list-mapper", "saml-user-attribute-mapper", "oidc-full-name-mapper", "oidc-address-mapper" ] } - ], - "org.keycloak.keys.KeyProvider": [ - { - "id": "1f9958a4-b3ac-4a1b-af95-fd8e6053864a", - "name": "hmac-generated", - "providerId": "hmac-generated", - "subComponents": {}, - "config": { - "kid": ["4e99c641-0494-49d5-979f-45cb5126f6f1"], - "secret": [ - "4wV4voiQmFajEegv83Ugd8DxFoy3JpN4YzO5qMx4XfB7Abq8NKU4Az5AkSpxYBSdb5GJEQypA4aLmnaDyCWLIw" - ], - "priority": ["100"], - "algorithm": ["HS256"] - } - }, - { - "id": "70fe0720-f3b7-47b4-a625-ae8fb6635da1", - "name": "aes-generated", - "providerId": "aes-generated", - "subComponents": {}, - "config": { - "kid": ["76118b54-fc74-4149-9028-fab1fdc07860"], - "secret": ["DvxTn0KA4TEUPqSFBw8qAw"], - "priority": ["100"] - } - }, - { - "id": "a12fdd97-1d72-4d9e-9e6a-f9e0b5d4e5f0", - "name": "rsa-generated", - "providerId": "rsa-generated", - "subComponents": {}, - "config": { - "privateKey": [ - "MIIEpAIBAAKCAQEAimbfmG2pL3qesWhUrQayRyYBbRFE0Ul5Ii/AW8Kq6Kad9R2n2sT2BvXWnsWBH6KuINUFJz3Tb+gWy235Jy0Idmekwx63JR20//ZJ7dyQ+b1iadmYPpqyixGL7NrVxQYT0AEGLcD/Fwsh869F3jgfQt7N15q2arRnOrW5NMwi+IvtHxZRZ3UluxShut2577ef8cakwCv4zoTV29y+Z3XhtlKZ4WOCuqIHL3SRHwNkb+k8cY0Gwc88FHl/ihFR0fX/lc7W2AHRd98ex8il4kBFfShBZur8ZLE7QWQdXRY2EYYr3D/W6/5wf/R2fAvbVmGzcYGZ2qm6d+K1XH8VU3X84wIDAQABAoIBABXXrHwa+nOCz57CD3MLNoGiDuGOsySwisyJartQmraC7TTtDDurkASDMe72zq0WeJK368tIp6DmqQpL/eFf6xD8xHUC2PajnJg033AJuluftvNroupmcb0e9M1ZsBkbH29Zagc4iUmyuRYDWGx8wPpFvYjEYvuuIwiR+3vIp9A/0ZbcBwdtml3Of5gYTXChPj28PrA4K7oFib2Zu1aYCBEdF8h9bKRF/UlvyWeSajjddexSQ6gkEjzAEMpliCDbOGSFGwNu1pY7FF4EpyJbalzdpn44m5v9bqfS9/CDrIOOUus88Nn5wCD2OAmAQnWn0Hnh7at4A5fw3VBUmEt70ckCgYEAx0Fg8Gp3SuMaytrf9HJHJcltyDRsdSxysF1ZvDV9cDUsD28QOa/wFJRVsABxqElU+W6QEc20NMgOHVyPFed5UhQA6WfmydzGIcF5C6T5IbE/5Uk3ptGuPdI0aR7rlRfefQOnUBr28dz5UDBTb93t9+Klxcss+nLGRbugnFBAtTUCgYEAsdD+92nuF/GfET97vbHxtJ6+epHddttWlsa5PVeVOZBE/LUsOZRxmxm4afvZGOkhUrvmA1+U0arcp9crS5+Ol2LUGh/9efqLvoBImBxLwB37VcIYLJi0EVPrhVPh+9r3vah1YMBhtapS0VtuEZOr47Yz7asBg1s1Z06l+bD1JLcCgYA+3YS9NYn/qZl5aQcBs9B4vo2RfeC+M1DYDgvS0rmJ3mzRTcQ7vyOrCoXiarFxW/mgXN69jz4M7RVu9BX83jQrzj3fZjWteKdWXRlYsCseEzNKnwgc7MjhnmGEzQmc15QNs0plfqxs8MAEKcsZX1bGP873kbvWJMIjnCf3SWaxBQKBgQCh9zt2w19jIewA+vFMbXw7SGk6Hgk6zTlG50YtkMxU/YtJIAFjhUohu8DVkNhDr35x7MLribF1dYu9ueku3ew1CokmLsNkywllAVaebw+0s9qOV9hLLuC989HQxQJPtTj54SrhcPrPTZBYME7G5dqo9PrB3oTnUDoJmoLmOABjawKBgQCeyd12ShpKYHZS4ZvE87OfXanuNfpVxhcXOqYHpQz2W0a+oUu9e78MlwTVooR4O52W/Ohch2FPEzq/1DBjJrK6PrMY8DS018BIVpQ9DS35/Ga9NtSi8DX7jTXacYPwL9n/+//U3vw0mjaoMXgCv44nYu4ro62J6wvVM98hjQmLJw==" - ], - "keyUse": ["SIG"], - "certificate": [ - "MIICqTCCAZECBgGBz6+bXzANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1zcGlmZndvcmtmbG93MB4XDTIyMDcwNTE4NDUwMVoXDTMyMDcwNTE4NDY0MVowGDEWMBQGA1UEAwwNc3BpZmZ3b3JrZmxvdzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAIpm35htqS96nrFoVK0GskcmAW0RRNFJeSIvwFvCquimnfUdp9rE9gb11p7FgR+iriDVBSc902/oFstt+SctCHZnpMMetyUdtP/2Se3ckPm9YmnZmD6asosRi+za1cUGE9ABBi3A/xcLIfOvRd44H0Lezdeatmq0Zzq1uTTMIviL7R8WUWd1JbsUobrdue+3n/HGpMAr+M6E1dvcvmd14bZSmeFjgrqiBy90kR8DZG/pPHGNBsHPPBR5f4oRUdH1/5XO1tgB0XffHsfIpeJARX0oQWbq/GSxO0FkHV0WNhGGK9w/1uv+cH/0dnwL21Zhs3GBmdqpunfitVx/FVN1/OMCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAaI7BEPZpf4MU7bMWmNgyfRTRDy5wtpyfuLPGHZ9EqtnvwwzsmlmXXsC55SLXx3wJETm+rFqeRFbo/hamlRajzzD317AUpE7nhnONTukmh6UuB8hXoWiQTD+YDYMy8kneSP4zvfm27F+TgUC4cvJSYuWVaCxFx52kxqW1hZkBzYUcfi21Qb1jRrbTbso37BxuVX+GdN015If3DPD6QnAhLPAYEFA9jiL16YeMdWHdvlXXmvriDegMUYQjFYPRh6iPzUEdG6KGHItF4AkOYBQAcoaYhfxpxofVlDdOqMZ/1c7AAbe4lR6/jYQ0CbHwdUu4dzJQe3vxr7GdxcB1ypvXPA==" - ], - "priority": ["100"] - } - }, - { - "id": "e16c740d-3ae2-4cc5-a68d-49d99e079672", - "name": "rsa-enc-generated", - "providerId": "rsa-enc-generated", - "subComponents": {}, - "config": { - "privateKey": [ - "MIIEowIBAAKCAQEAsqGsclDQDFSTn8HS1LiiNAnTwn3CS8HXPLDYMHr/jUQ8r5eD+vQY5ICh5V5c8l8J6ydbpzffFEKam54Ypp4yzaWJZ4huYBMf4vL7xrAZ4VXBreu16BIxOrThzrJe9WmI8+Annzo62mNYZbjf4WNpZDURmxZSo7v6Czprd5O6T4N5bxr8sjRRptZR8hxtrRvJnuC0jF+dLHIO5SKR1hUVG/gbpIBqGcsLkNC9nnS6M/N5YFzUIV5JhXo3+mrR/yvw7m+oS5yRsN0raCSXVenNP05Dhsd4FOYqoXBBcdgXXbiDxed0HWB/g5dASqyMydHriddGr8FU0W8/uZmF79wxPwIDAQABAoIBAFsWCaL5Bj1jWytZYDJMO5mhcTN5gPu0ShaObo66CVl1dCRtdEUg9xh9ZxBYf7ivMZWRKjEoUj44gDHd+d/sRyeJw3jhnraqydWl5TC5V1kJq4sN6GH/9M5kscf+OGGXgNgqcsnEnYICqm6kSLTbRkBstx+H0HfhQG09StNcpuIn4MsoMZT8XmZbXRLb3FhfpuTSX3t2nbSDRfUf7LI1EDnFQen/AJAA5lOHthLCdz4Gj1vfalOFjCMYOUWmL/mCDEb38F6QJZxkyhmS/r2kM09PFLOio6z3J8C8mVeq7uao0s5xAKj5SJqx4r+TTvL5aOF8JBWm8Hz1Vcip9/MjsQECgYEA/8Hpb4RggNyn+YzTxqxtPtbLFL0YywtNT+gutmJH1gyTjfx7p3dmA/NsdIeuJmBpZfA7oDXIqfj2M9QLfC5bdKnggQzrIO3BgClI88zOIWd229Bt6D1yx92k4+9eaRwOKBPn8+u0mCk8TBv32ecMLQ9o8AKNIHeCZQjByvOrIMECgYEAss0J3TzrRuEOpnxJ9fNOeB3rNpIFrpNua+oEQI4gDbBvyT7osBKkGqfXJpUQMftr8a6uBHLHV7/Wq6/aRkRhk+aER8h01DUIWGLmbCUdkFSJZ8iObMZQvURtckhzxxhYu0Ybwn0RJg/zzR4onTRO+eL1fTnb5Id55PyPt3Pp0f8CgYEAovDOoP6MYOyzk5h1/7gwrX04ytCicBGWQtdgk0/QBn3ir+3wdcPq2Y+HREKA3/BClfBUfIBnhGqZqHFqk8YQ/CWSY4Vwc30l71neIX0UwlFhdy+2JeSoMM9z0sfYtUxrdHsiJtO/LcXvpWmYIVpC9p4/s9FcShf5mhbXKE7PcsECgYBN7qqvAH94LF4rWJ8QEZWRK1E7Ptg1KFOHu79Qt+HmtZFzwPTA0c8vQxq22V/uuSxqcf2tOK4EZDxYJtTXrbRuN5pOg2PQnrDdfXX7iw3gu8gMMVFKvgGxDSM7HbNBAy6hqcQtuD+CPI/CRrPjGUqXBkKD63UZnacWlLK7fk1a1wKBgExUaqOBKmr0vldVn66E1XzZj4F4+fV5Ggka9289pBNBRlJFD4VmIYkDkOrLimyy2cYeCkocrOvF6HMJqTcOzD50pj44OWkYFRbs6vK0S7iLSX0eR158XOR9C+uZzp1vIA4sYwW3504HVdVoIU5M8ItSgDsFjGnvHopTGu3MBWPT" - ], - "keyUse": ["ENC"], - "certificate": [ - "MIICqTCCAZECBgGBz6+byzANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1zcGlmZndvcmtmbG93MB4XDTIyMDcwNTE4NDUwMVoXDTMyMDcwNTE4NDY0MVowGDEWMBQGA1UEAwwNc3BpZmZ3b3JrZmxvdzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALKhrHJQ0AxUk5/B0tS4ojQJ08J9wkvB1zyw2DB6/41EPK+Xg/r0GOSAoeVeXPJfCesnW6c33xRCmpueGKaeMs2liWeIbmATH+Ly+8awGeFVwa3rtegSMTq04c6yXvVpiPPgJ586OtpjWGW43+FjaWQ1EZsWUqO7+gs6a3eTuk+DeW8a/LI0UabWUfIcba0byZ7gtIxfnSxyDuUikdYVFRv4G6SAahnLC5DQvZ50ujPzeWBc1CFeSYV6N/pq0f8r8O5vqEuckbDdK2gkl1XpzT9OQ4bHeBTmKqFwQXHYF124g8XndB1gf4OXQEqsjMnR64nXRq/BVNFvP7mZhe/cMT8CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEArDDC7bYbuBg33PbUQi7P77lV7PuE9uQU1F3HqulhkARQeM/xmBdJRj9CHjj62shkI3An70tJtGBJkVAHltmvjC+A6IDO5I8IbnPkvWJFu9HwphdP/C1HXYmGPPe7yGdKpy6mdCZ+LMZP7BENhOlx9yXLDFYtcGvqZ4u3XvfsLqUsRGqZHNlhVJD13dUbI6pvbwMsb3gIxozgTIa2ySHMbHafln2UQk5jD0eOIVkaNAdlHqMHiBpPjkoVxnhAmJ/dUIAqKBvuIbCOu9N0kOQSl82LqC7CZ21JCyT86Ll3n1RTkxY5G3JzGW4dyJMOGSyVnWaQ9Z+C92ZMFcOt611M2A==" - ], - "priority": ["100"], - "algorithm": ["RSA-OAEP"] - } + }, { + "id" : "d68e938d-dde6-47d9-bdc8-8e8523eb08cd", + "name" : "Max Clients Limit", + "providerId" : "max-clients", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "max-clients" : [ "200" ] } - ] + }, { + "id" : "1209fa5d-37df-4f9a-b4fa-4a3cd94e21fe", + "name" : "Allowed Protocol Mapper Types", + "providerId" : "allowed-protocol-mappers", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "allowed-protocol-mapper-types" : [ "oidc-address-mapper", "oidc-full-name-mapper", "oidc-usermodel-attribute-mapper", "saml-user-property-mapper", "saml-user-attribute-mapper", "oidc-sha256-pairwise-sub-mapper", "oidc-usermodel-property-mapper", "saml-role-list-mapper" ] + } + }, { + "id" : "3854361d-3fe5-47fb-9417-a99592e3dc5c", + "name" : "Allowed Client Scopes", + "providerId" : "allowed-client-templates", + "subType" : "authenticated", + "subComponents" : { }, + "config" : { + "allow-default-scopes" : [ "true" ] + } + }, { + "id" : "4c4076ec-68ed-46c1-b0a5-3c8ed08dd4f6", + "name" : "Consent Required", + "providerId" : "consent-required", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { } + }, { + "id" : "bbbe2ea2-2a36-494b-b57f-8b202740ebf4", + "name" : "Full Scope Disabled", + "providerId" : "scope", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { } + }, { + "id" : "41eef3e1-bf71-4e8a-b729-fea8eb16b5d8", + "name" : "Allowed Client Scopes", + "providerId" : "allowed-client-templates", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "allow-default-scopes" : [ "true" ] + } + } ], + "org.keycloak.userprofile.UserProfileProvider" : [ { + "id" : "576f8c6a-00e6-45dd-a63d-614100fb2cc4", + "providerId" : "declarative-user-profile", + "subComponents" : { }, + "config" : { } + } ], + "org.keycloak.keys.KeyProvider" : [ { + "id" : "1f9958a4-b3ac-4a1b-af95-fd8e6053864a", + "name" : "hmac-generated", + "providerId" : "hmac-generated", + "subComponents" : { }, + "config" : { + "kid" : [ "4e99c641-0494-49d5-979f-45cb5126f6f1" ], + "secret" : [ "4wV4voiQmFajEegv83Ugd8DxFoy3JpN4YzO5qMx4XfB7Abq8NKU4Az5AkSpxYBSdb5GJEQypA4aLmnaDyCWLIw" ], + "priority" : [ "100" ], + "algorithm" : [ "HS256" ] + } + }, { + "id" : "70fe0720-f3b7-47b4-a625-ae8fb6635da1", + "name" : "aes-generated", + "providerId" : "aes-generated", + "subComponents" : { }, + "config" : { + "kid" : [ "76118b54-fc74-4149-9028-fab1fdc07860" ], + "secret" : [ "DvxTn0KA4TEUPqSFBw8qAw" ], + "priority" : [ "100" ] + } + }, { + "id" : "a12fdd97-1d72-4d9e-9e6a-f9e0b5d4e5f0", + "name" : "rsa-generated", + "providerId" : "rsa-generated", + "subComponents" : { }, + "config" : { + "privateKey" : [ "MIIEpAIBAAKCAQEAimbfmG2pL3qesWhUrQayRyYBbRFE0Ul5Ii/AW8Kq6Kad9R2n2sT2BvXWnsWBH6KuINUFJz3Tb+gWy235Jy0Idmekwx63JR20//ZJ7dyQ+b1iadmYPpqyixGL7NrVxQYT0AEGLcD/Fwsh869F3jgfQt7N15q2arRnOrW5NMwi+IvtHxZRZ3UluxShut2577ef8cakwCv4zoTV29y+Z3XhtlKZ4WOCuqIHL3SRHwNkb+k8cY0Gwc88FHl/ihFR0fX/lc7W2AHRd98ex8il4kBFfShBZur8ZLE7QWQdXRY2EYYr3D/W6/5wf/R2fAvbVmGzcYGZ2qm6d+K1XH8VU3X84wIDAQABAoIBABXXrHwa+nOCz57CD3MLNoGiDuGOsySwisyJartQmraC7TTtDDurkASDMe72zq0WeJK368tIp6DmqQpL/eFf6xD8xHUC2PajnJg033AJuluftvNroupmcb0e9M1ZsBkbH29Zagc4iUmyuRYDWGx8wPpFvYjEYvuuIwiR+3vIp9A/0ZbcBwdtml3Of5gYTXChPj28PrA4K7oFib2Zu1aYCBEdF8h9bKRF/UlvyWeSajjddexSQ6gkEjzAEMpliCDbOGSFGwNu1pY7FF4EpyJbalzdpn44m5v9bqfS9/CDrIOOUus88Nn5wCD2OAmAQnWn0Hnh7at4A5fw3VBUmEt70ckCgYEAx0Fg8Gp3SuMaytrf9HJHJcltyDRsdSxysF1ZvDV9cDUsD28QOa/wFJRVsABxqElU+W6QEc20NMgOHVyPFed5UhQA6WfmydzGIcF5C6T5IbE/5Uk3ptGuPdI0aR7rlRfefQOnUBr28dz5UDBTb93t9+Klxcss+nLGRbugnFBAtTUCgYEAsdD+92nuF/GfET97vbHxtJ6+epHddttWlsa5PVeVOZBE/LUsOZRxmxm4afvZGOkhUrvmA1+U0arcp9crS5+Ol2LUGh/9efqLvoBImBxLwB37VcIYLJi0EVPrhVPh+9r3vah1YMBhtapS0VtuEZOr47Yz7asBg1s1Z06l+bD1JLcCgYA+3YS9NYn/qZl5aQcBs9B4vo2RfeC+M1DYDgvS0rmJ3mzRTcQ7vyOrCoXiarFxW/mgXN69jz4M7RVu9BX83jQrzj3fZjWteKdWXRlYsCseEzNKnwgc7MjhnmGEzQmc15QNs0plfqxs8MAEKcsZX1bGP873kbvWJMIjnCf3SWaxBQKBgQCh9zt2w19jIewA+vFMbXw7SGk6Hgk6zTlG50YtkMxU/YtJIAFjhUohu8DVkNhDr35x7MLribF1dYu9ueku3ew1CokmLsNkywllAVaebw+0s9qOV9hLLuC989HQxQJPtTj54SrhcPrPTZBYME7G5dqo9PrB3oTnUDoJmoLmOABjawKBgQCeyd12ShpKYHZS4ZvE87OfXanuNfpVxhcXOqYHpQz2W0a+oUu9e78MlwTVooR4O52W/Ohch2FPEzq/1DBjJrK6PrMY8DS018BIVpQ9DS35/Ga9NtSi8DX7jTXacYPwL9n/+//U3vw0mjaoMXgCv44nYu4ro62J6wvVM98hjQmLJw==" ], + "keyUse" : [ "SIG" ], + "certificate" : [ "MIICqTCCAZECBgGBz6+bXzANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1zcGlmZndvcmtmbG93MB4XDTIyMDcwNTE4NDUwMVoXDTMyMDcwNTE4NDY0MVowGDEWMBQGA1UEAwwNc3BpZmZ3b3JrZmxvdzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAIpm35htqS96nrFoVK0GskcmAW0RRNFJeSIvwFvCquimnfUdp9rE9gb11p7FgR+iriDVBSc902/oFstt+SctCHZnpMMetyUdtP/2Se3ckPm9YmnZmD6asosRi+za1cUGE9ABBi3A/xcLIfOvRd44H0Lezdeatmq0Zzq1uTTMIviL7R8WUWd1JbsUobrdue+3n/HGpMAr+M6E1dvcvmd14bZSmeFjgrqiBy90kR8DZG/pPHGNBsHPPBR5f4oRUdH1/5XO1tgB0XffHsfIpeJARX0oQWbq/GSxO0FkHV0WNhGGK9w/1uv+cH/0dnwL21Zhs3GBmdqpunfitVx/FVN1/OMCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAaI7BEPZpf4MU7bMWmNgyfRTRDy5wtpyfuLPGHZ9EqtnvwwzsmlmXXsC55SLXx3wJETm+rFqeRFbo/hamlRajzzD317AUpE7nhnONTukmh6UuB8hXoWiQTD+YDYMy8kneSP4zvfm27F+TgUC4cvJSYuWVaCxFx52kxqW1hZkBzYUcfi21Qb1jRrbTbso37BxuVX+GdN015If3DPD6QnAhLPAYEFA9jiL16YeMdWHdvlXXmvriDegMUYQjFYPRh6iPzUEdG6KGHItF4AkOYBQAcoaYhfxpxofVlDdOqMZ/1c7AAbe4lR6/jYQ0CbHwdUu4dzJQe3vxr7GdxcB1ypvXPA==" ], + "priority" : [ "100" ] + } + }, { + "id" : "e16c740d-3ae2-4cc5-a68d-49d99e079672", + "name" : "rsa-enc-generated", + "providerId" : "rsa-enc-generated", + "subComponents" : { }, + "config" : { + "privateKey" : [ "MIIEowIBAAKCAQEAsqGsclDQDFSTn8HS1LiiNAnTwn3CS8HXPLDYMHr/jUQ8r5eD+vQY5ICh5V5c8l8J6ydbpzffFEKam54Ypp4yzaWJZ4huYBMf4vL7xrAZ4VXBreu16BIxOrThzrJe9WmI8+Annzo62mNYZbjf4WNpZDURmxZSo7v6Czprd5O6T4N5bxr8sjRRptZR8hxtrRvJnuC0jF+dLHIO5SKR1hUVG/gbpIBqGcsLkNC9nnS6M/N5YFzUIV5JhXo3+mrR/yvw7m+oS5yRsN0raCSXVenNP05Dhsd4FOYqoXBBcdgXXbiDxed0HWB/g5dASqyMydHriddGr8FU0W8/uZmF79wxPwIDAQABAoIBAFsWCaL5Bj1jWytZYDJMO5mhcTN5gPu0ShaObo66CVl1dCRtdEUg9xh9ZxBYf7ivMZWRKjEoUj44gDHd+d/sRyeJw3jhnraqydWl5TC5V1kJq4sN6GH/9M5kscf+OGGXgNgqcsnEnYICqm6kSLTbRkBstx+H0HfhQG09StNcpuIn4MsoMZT8XmZbXRLb3FhfpuTSX3t2nbSDRfUf7LI1EDnFQen/AJAA5lOHthLCdz4Gj1vfalOFjCMYOUWmL/mCDEb38F6QJZxkyhmS/r2kM09PFLOio6z3J8C8mVeq7uao0s5xAKj5SJqx4r+TTvL5aOF8JBWm8Hz1Vcip9/MjsQECgYEA/8Hpb4RggNyn+YzTxqxtPtbLFL0YywtNT+gutmJH1gyTjfx7p3dmA/NsdIeuJmBpZfA7oDXIqfj2M9QLfC5bdKnggQzrIO3BgClI88zOIWd229Bt6D1yx92k4+9eaRwOKBPn8+u0mCk8TBv32ecMLQ9o8AKNIHeCZQjByvOrIMECgYEAss0J3TzrRuEOpnxJ9fNOeB3rNpIFrpNua+oEQI4gDbBvyT7osBKkGqfXJpUQMftr8a6uBHLHV7/Wq6/aRkRhk+aER8h01DUIWGLmbCUdkFSJZ8iObMZQvURtckhzxxhYu0Ybwn0RJg/zzR4onTRO+eL1fTnb5Id55PyPt3Pp0f8CgYEAovDOoP6MYOyzk5h1/7gwrX04ytCicBGWQtdgk0/QBn3ir+3wdcPq2Y+HREKA3/BClfBUfIBnhGqZqHFqk8YQ/CWSY4Vwc30l71neIX0UwlFhdy+2JeSoMM9z0sfYtUxrdHsiJtO/LcXvpWmYIVpC9p4/s9FcShf5mhbXKE7PcsECgYBN7qqvAH94LF4rWJ8QEZWRK1E7Ptg1KFOHu79Qt+HmtZFzwPTA0c8vQxq22V/uuSxqcf2tOK4EZDxYJtTXrbRuN5pOg2PQnrDdfXX7iw3gu8gMMVFKvgGxDSM7HbNBAy6hqcQtuD+CPI/CRrPjGUqXBkKD63UZnacWlLK7fk1a1wKBgExUaqOBKmr0vldVn66E1XzZj4F4+fV5Ggka9289pBNBRlJFD4VmIYkDkOrLimyy2cYeCkocrOvF6HMJqTcOzD50pj44OWkYFRbs6vK0S7iLSX0eR158XOR9C+uZzp1vIA4sYwW3504HVdVoIU5M8ItSgDsFjGnvHopTGu3MBWPT" ], + "keyUse" : [ "ENC" ], + "certificate" : [ "MIICqTCCAZECBgGBz6+byzANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1zcGlmZndvcmtmbG93MB4XDTIyMDcwNTE4NDUwMVoXDTMyMDcwNTE4NDY0MVowGDEWMBQGA1UEAwwNc3BpZmZ3b3JrZmxvdzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALKhrHJQ0AxUk5/B0tS4ojQJ08J9wkvB1zyw2DB6/41EPK+Xg/r0GOSAoeVeXPJfCesnW6c33xRCmpueGKaeMs2liWeIbmATH+Ly+8awGeFVwa3rtegSMTq04c6yXvVpiPPgJ586OtpjWGW43+FjaWQ1EZsWUqO7+gs6a3eTuk+DeW8a/LI0UabWUfIcba0byZ7gtIxfnSxyDuUikdYVFRv4G6SAahnLC5DQvZ50ujPzeWBc1CFeSYV6N/pq0f8r8O5vqEuckbDdK2gkl1XpzT9OQ4bHeBTmKqFwQXHYF124g8XndB1gf4OXQEqsjMnR64nXRq/BVNFvP7mZhe/cMT8CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEArDDC7bYbuBg33PbUQi7P77lV7PuE9uQU1F3HqulhkARQeM/xmBdJRj9CHjj62shkI3An70tJtGBJkVAHltmvjC+A6IDO5I8IbnPkvWJFu9HwphdP/C1HXYmGPPe7yGdKpy6mdCZ+LMZP7BENhOlx9yXLDFYtcGvqZ4u3XvfsLqUsRGqZHNlhVJD13dUbI6pvbwMsb3gIxozgTIa2ySHMbHafln2UQk5jD0eOIVkaNAdlHqMHiBpPjkoVxnhAmJ/dUIAqKBvuIbCOu9N0kOQSl82LqC7CZ21JCyT86Ll3n1RTkxY5G3JzGW4dyJMOGSyVnWaQ9Z+C92ZMFcOt611M2A==" ], + "priority" : [ "100" ], + "algorithm" : [ "RSA-OAEP" ] + } + } ] }, - "internationalizationEnabled": false, - "supportedLocales": [], - "authenticationFlows": [ - { - "id": "ff21c216-5ea8-4d26-95ca-2b467a9d5059", - "alias": "Account verification options", - "description": "Method with which to verity the existing account", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "idp-email-verification", - "authenticatorFlow": false, - "requirement": "ALTERNATIVE", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "ALTERNATIVE", - "priority": 20, - "autheticatorFlow": true, - "flowAlias": "Verify Existing Account by Re-authentication", - "userSetupAllowed": false - } - ] - }, - { - "id": "256108f7-b791-4e54-b4cb-a551afdf870a", - "alias": "Authentication Options", - "description": "Authentication options.", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "basic-auth", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "basic-auth-otp", - "authenticatorFlow": false, - "requirement": "DISABLED", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "auth-spnego", - "authenticatorFlow": false, - "requirement": "DISABLED", - "priority": 30, - "autheticatorFlow": false, - "userSetupAllowed": false - } - ] - }, - { - "id": "fa9b2739-d814-4f83-805f-2ab0f5692cc8", - "alias": "Browser - Conditional OTP", - "description": "Flow to determine if the OTP is required for the authentication", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "conditional-user-configured", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "auth-otp-form", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - } - ] - }, - { - "id": "76819f1b-04b8-412e-933c-3e30b48f350b", - "alias": "Direct Grant - Conditional OTP", - "description": "Flow to determine if the OTP is required for the authentication", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "conditional-user-configured", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "direct-grant-validate-otp", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - } - ] - }, - { - "id": "54f89ad2-b2b2-4554-8528-04a8b4e73e68", - "alias": "First broker login - Conditional OTP", - "description": "Flow to determine if the OTP is required for the authentication", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "conditional-user-configured", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "auth-otp-form", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - } - ] - }, - { - "id": "08664454-8aa7-4f07-990b-9b59ddd19a26", - "alias": "Handle Existing Account", - "description": "Handle what to do if there is existing account with same email/username like authenticated identity provider", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "idp-confirm-link", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": true, - "flowAlias": "Account verification options", - "userSetupAllowed": false - } - ] - }, - { - "id": "29af9cfb-11d1-4781-aee3-844b436d4c08", - "alias": "Reset - Conditional OTP", - "description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "conditional-user-configured", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "reset-otp", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - } - ] - }, - { - "id": "2c2d44f6-115e-420e-bc86-1d58914b16ac", - "alias": "User creation or linking", - "description": "Flow for the existing/non-existing user alternatives", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticatorConfig": "create unique user config", - "authenticator": "idp-create-user-if-unique", - "authenticatorFlow": false, - "requirement": "ALTERNATIVE", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "ALTERNATIVE", - "priority": 20, - "autheticatorFlow": true, - "flowAlias": "Handle Existing Account", - "userSetupAllowed": false - } - ] - }, - { - "id": "050e3be8-d313-49ec-a891-fa84592c6cc4", - "alias": "Verify Existing Account by Re-authentication", - "description": "Reauthentication of existing account", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "idp-username-password-form", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "CONDITIONAL", - "priority": 20, - "autheticatorFlow": true, - "flowAlias": "First broker login - Conditional OTP", - "userSetupAllowed": false - } - ] - }, - { - "id": "d04138a1-dfa4-4854-a59e-b7f4693b56e6", - "alias": "browser", - "description": "browser based authentication", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "auth-cookie", - "authenticatorFlow": false, - "requirement": "ALTERNATIVE", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "auth-spnego", - "authenticatorFlow": false, - "requirement": "DISABLED", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "identity-provider-redirector", - "authenticatorFlow": false, - "requirement": "ALTERNATIVE", - "priority": 25, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "ALTERNATIVE", - "priority": 30, - "autheticatorFlow": true, - "flowAlias": "forms", - "userSetupAllowed": false - } - ] - }, - { - "id": "998cd89f-b1da-4101-9c75-658998ad3503", - "alias": "clients", - "description": "Base authentication for clients", - "providerId": "client-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "client-secret", - "authenticatorFlow": false, - "requirement": "ALTERNATIVE", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "client-jwt", - "authenticatorFlow": false, - "requirement": "ALTERNATIVE", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "client-secret-jwt", - "authenticatorFlow": false, - "requirement": "ALTERNATIVE", - "priority": 30, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "client-x509", - "authenticatorFlow": false, - "requirement": "ALTERNATIVE", - "priority": 40, - "autheticatorFlow": false, - "userSetupAllowed": false - } - ] - }, - { - "id": "e75753f0-6cd8-4fe5-88d5-55affdbbc5d1", - "alias": "direct grant", - "description": "OpenID Connect Resource Owner Grant", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "direct-grant-validate-username", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "direct-grant-validate-password", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "CONDITIONAL", - "priority": 30, - "autheticatorFlow": true, - "flowAlias": "Direct Grant - Conditional OTP", - "userSetupAllowed": false - } - ] - }, - { - "id": "3854b6cc-eb08-473b-95f8-71eaab9219de", - "alias": "docker auth", - "description": "Used by Docker clients to authenticate against the IDP", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "docker-http-basic-authenticator", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - } - ] - }, - { - "id": "a52f25a7-8509-468c-925c-4bb02e8ccd8e", - "alias": "first broker login", - "description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticatorConfig": "review profile config", - "authenticator": "idp-review-profile", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": true, - "flowAlias": "User creation or linking", - "userSetupAllowed": false - } - ] - }, - { - "id": "cc9b12fa-7f7d-44ef-aa11-d7e374b2ec0d", - "alias": "forms", - "description": "Username, password, otp and other auth forms.", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "auth-username-password-form", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "CONDITIONAL", - "priority": 20, - "autheticatorFlow": true, - "flowAlias": "Browser - Conditional OTP", - "userSetupAllowed": false - } - ] - }, - { - "id": "289ec9b7-c2b8-4222-922a-81be4450ac2e", - "alias": "http challenge", - "description": "An authentication flow based on challenge-response HTTP Authentication Schemes", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "no-cookie-redirect", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": true, - "flowAlias": "Authentication Options", - "userSetupAllowed": false - } - ] - }, - { - "id": "295c9bc2-0252-4fd3-b7da-47d4d2f0a09b", - "alias": "registration", - "description": "registration flow", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "registration-page-form", - "authenticatorFlow": true, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": true, - "flowAlias": "registration form", - "userSetupAllowed": false - } - ] - }, - { - "id": "260f9fad-5f32-4507-9e39-6e46bc26e74e", - "alias": "registration form", - "description": "registration form", - "providerId": "form-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "registration-user-creation", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "registration-profile-action", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 40, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "registration-password-action", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 50, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "registration-recaptcha-action", - "authenticatorFlow": false, - "requirement": "DISABLED", - "priority": 60, - "autheticatorFlow": false, - "userSetupAllowed": false - } - ] - }, - { - "id": "39ef84e4-b7a0-434d-ba2a-5869b78e7aa0", - "alias": "reset credentials", - "description": "Reset credentials for a user if they forgot their password or something", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "reset-credentials-choose-user", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "reset-credential-email", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "reset-password", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 30, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "CONDITIONAL", - "priority": 40, - "autheticatorFlow": true, - "flowAlias": "Reset - Conditional OTP", - "userSetupAllowed": false - } - ] - }, - { - "id": "e47473b7-22e0-4bd0-a253-60300aadd9b9", - "alias": "saml ecp", - "description": "SAML ECP Profile Authentication Flow", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "http-basic-authenticator", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - } - ] + "internationalizationEnabled" : false, + "supportedLocales" : [ ], + "authenticationFlows" : [ { + "id" : "b896c673-57ab-4f24-bbb1-334bdadbecd3", + "alias" : "Account verification options", + "description" : "Method with which to verity the existing account", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "idp-email-verification", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "ALTERNATIVE", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "Verify Existing Account by Re-authentication", + "userSetupAllowed" : false + } ] + }, { + "id" : "4da99e29-371e-4f4b-a863-e5079f30a714", + "alias" : "Authentication Options", + "description" : "Authentication options.", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "basic-auth", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "basic-auth-otp", + "authenticatorFlow" : false, + "requirement" : "DISABLED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "auth-spnego", + "authenticatorFlow" : false, + "requirement" : "DISABLED", + "priority" : 30, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "d398c928-e201-4e8b-ab09-289bb351cd2e", + "alias" : "Browser - Conditional OTP", + "description" : "Flow to determine if the OTP is required for the authentication", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "conditional-user-configured", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "auth-otp-form", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "663b7aa3-84f6-4347-8ed4-588c2464b75d", + "alias" : "Direct Grant - Conditional OTP", + "description" : "Flow to determine if the OTP is required for the authentication", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "conditional-user-configured", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "direct-grant-validate-otp", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "98013bc1-e4dd-41f7-9849-1f898143b944", + "alias" : "First broker login - Conditional OTP", + "description" : "Flow to determine if the OTP is required for the authentication", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "conditional-user-configured", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "auth-otp-form", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "b77e7545-9e39-4d72-93f8-1b38c954c2e2", + "alias" : "Handle Existing Account", + "description" : "Handle what to do if there is existing account with same email/username like authenticated identity provider", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "idp-confirm-link", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "Account verification options", + "userSetupAllowed" : false + } ] + }, { + "id" : "2470e6f4-9a01-476a-9057-75d78e577182", + "alias" : "Reset - Conditional OTP", + "description" : "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "conditional-user-configured", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "reset-otp", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "8e7dad0b-f4e1-4534-b618-b635b0a0e4f9", + "alias" : "User creation or linking", + "description" : "Flow for the existing/non-existing user alternatives", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticatorConfig" : "create unique user config", + "authenticator" : "idp-create-user-if-unique", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "ALTERNATIVE", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "Handle Existing Account", + "userSetupAllowed" : false + } ] + }, { + "id" : "97c83e43-cba8-4d92-b108-9181bca07a1e", + "alias" : "Verify Existing Account by Re-authentication", + "description" : "Reauthentication of existing account", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "idp-username-password-form", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "CONDITIONAL", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "First broker login - Conditional OTP", + "userSetupAllowed" : false + } ] + }, { + "id" : "fbabd64c-20de-4b8c-bfd2-be6822572278", + "alias" : "browser", + "description" : "browser based authentication", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "auth-cookie", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "auth-spnego", + "authenticatorFlow" : false, + "requirement" : "DISABLED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "identity-provider-redirector", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 25, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "ALTERNATIVE", + "priority" : 30, + "autheticatorFlow" : true, + "flowAlias" : "forms", + "userSetupAllowed" : false + } ] + }, { + "id" : "0628a99f-b194-495d-8e54-cc4ca8684956", + "alias" : "clients", + "description" : "Base authentication for clients", + "providerId" : "client-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "client-secret", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "client-jwt", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "client-secret-jwt", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 30, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "client-x509", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 40, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "ce6bf7af-3bff-48ce-b214-7fed08503a2a", + "alias" : "direct grant", + "description" : "OpenID Connect Resource Owner Grant", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "direct-grant-validate-username", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "direct-grant-validate-password", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "CONDITIONAL", + "priority" : 30, + "autheticatorFlow" : true, + "flowAlias" : "Direct Grant - Conditional OTP", + "userSetupAllowed" : false + } ] + }, { + "id" : "60ce729b-d055-4ae7-83cb-85dbcf8cfdaa", + "alias" : "docker auth", + "description" : "Used by Docker clients to authenticate against the IDP", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "docker-http-basic-authenticator", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "0bd3cf93-7f33-46b2-ad1f-85cdfb0a87f9", + "alias" : "first broker login", + "description" : "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticatorConfig" : "review profile config", + "authenticator" : "idp-review-profile", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "User creation or linking", + "userSetupAllowed" : false + } ] + }, { + "id" : "3e52f178-9b9d-4a62-97d5-f9f3f872bcd9", + "alias" : "forms", + "description" : "Username, password, otp and other auth forms.", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "auth-username-password-form", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "CONDITIONAL", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "Browser - Conditional OTP", + "userSetupAllowed" : false + } ] + }, { + "id" : "3f5fd6cc-2935-45d8-9bef-6857bba3657a", + "alias" : "http challenge", + "description" : "An authentication flow based on challenge-response HTTP Authentication Schemes", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "no-cookie-redirect", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "Authentication Options", + "userSetupAllowed" : false + } ] + }, { + "id" : "2c2b32dd-57dc-45d7-9a24-b4a253cb6a03", + "alias" : "registration", + "description" : "registration flow", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "registration-page-form", + "authenticatorFlow" : true, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : true, + "flowAlias" : "registration form", + "userSetupAllowed" : false + } ] + }, { + "id" : "dbc28b13-dba7-42a0-a8ab-faa8762979c3", + "alias" : "registration form", + "description" : "registration form", + "providerId" : "form-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "registration-user-creation", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "registration-profile-action", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 40, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "registration-password-action", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 50, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "registration-recaptcha-action", + "authenticatorFlow" : false, + "requirement" : "DISABLED", + "priority" : 60, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "b4a901d5-e7b9-4eb6-9f8e-1d3305846828", + "alias" : "reset credentials", + "description" : "Reset credentials for a user if they forgot their password or something", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "reset-credentials-choose-user", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "reset-credential-email", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "reset-password", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 30, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "CONDITIONAL", + "priority" : 40, + "autheticatorFlow" : true, + "flowAlias" : "Reset - Conditional OTP", + "userSetupAllowed" : false + } ] + }, { + "id" : "824fe757-cc5c-4e13-ab98-9a2132e10f5c", + "alias" : "saml ecp", + "description" : "SAML ECP Profile Authentication Flow", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "http-basic-authenticator", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + } ], + "authenticatorConfig" : [ { + "id" : "817a93da-29df-447f-ab05-cd9557e66745", + "alias" : "create unique user config", + "config" : { + "require.password.update.after.registration" : "false" } - ], - "authenticatorConfig": [ - { - "id": "a85a0c1d-f3a2-4183-862e-394a22f12c28", - "alias": "create unique user config", - "config": { - "require.password.update.after.registration": "false" - } - }, - { - "id": "9167b412-f119-4f29-8b38-211437556f63", - "alias": "review profile config", - "config": { - "update.profile.on.first.login": "missing" - } + }, { + "id" : "4a8a9659-fa0d-4da8-907b-3b6daec1c878", + "alias" : "review profile config", + "config" : { + "update.profile.on.first.login" : "missing" } - ], - "requiredActions": [ - { - "alias": "CONFIGURE_TOTP", - "name": "Configure OTP", - "providerId": "CONFIGURE_TOTP", - "enabled": true, - "defaultAction": false, - "priority": 10, - "config": {} - }, - { - "alias": "terms_and_conditions", - "name": "Terms and Conditions", - "providerId": "terms_and_conditions", - "enabled": false, - "defaultAction": false, - "priority": 20, - "config": {} - }, - { - "alias": "UPDATE_PASSWORD", - "name": "Update Password", - "providerId": "UPDATE_PASSWORD", - "enabled": true, - "defaultAction": false, - "priority": 30, - "config": {} - }, - { - "alias": "UPDATE_PROFILE", - "name": "Update Profile", - "providerId": "UPDATE_PROFILE", - "enabled": true, - "defaultAction": false, - "priority": 40, - "config": {} - }, - { - "alias": "VERIFY_EMAIL", - "name": "Verify Email", - "providerId": "VERIFY_EMAIL", - "enabled": true, - "defaultAction": false, - "priority": 50, - "config": {} - }, - { - "alias": "delete_account", - "name": "Delete Account", - "providerId": "delete_account", - "enabled": false, - "defaultAction": false, - "priority": 60, - "config": {} - }, - { - "alias": "update_user_locale", - "name": "Update User Locale", - "providerId": "update_user_locale", - "enabled": true, - "defaultAction": false, - "priority": 1000, - "config": {} - } - ], - "browserFlow": "browser", - "registrationFlow": "registration", - "directGrantFlow": "direct grant", - "resetCredentialsFlow": "reset credentials", - "clientAuthenticationFlow": "clients", - "dockerAuthenticationFlow": "docker auth", - "attributes": { - "cibaBackchannelTokenDeliveryMode": "poll", - "cibaAuthRequestedUserHint": "login_hint", - "clientOfflineSessionMaxLifespan": "0", - "oauth2DevicePollingInterval": "5", - "clientSessionIdleTimeout": "0", - "actionTokenGeneratedByUserLifespan-execute-actions": "", - "actionTokenGeneratedByUserLifespan-verify-email": "", - "clientOfflineSessionIdleTimeout": "0", - "actionTokenGeneratedByUserLifespan-reset-credentials": "", - "cibaInterval": "5", - "cibaExpiresIn": "120", - "oauth2DeviceCodeLifespan": "600", - "actionTokenGeneratedByUserLifespan-idp-verify-account-via-email": "", - "parRequestUriLifespan": "60", - "clientSessionMaxLifespan": "0" + } ], + "requiredActions" : [ { + "alias" : "CONFIGURE_TOTP", + "name" : "Configure OTP", + "providerId" : "CONFIGURE_TOTP", + "enabled" : true, + "defaultAction" : false, + "priority" : 10, + "config" : { } + }, { + "alias" : "terms_and_conditions", + "name" : "Terms and Conditions", + "providerId" : "terms_and_conditions", + "enabled" : false, + "defaultAction" : false, + "priority" : 20, + "config" : { } + }, { + "alias" : "UPDATE_PASSWORD", + "name" : "Update Password", + "providerId" : "UPDATE_PASSWORD", + "enabled" : true, + "defaultAction" : false, + "priority" : 30, + "config" : { } + }, { + "alias" : "UPDATE_PROFILE", + "name" : "Update Profile", + "providerId" : "UPDATE_PROFILE", + "enabled" : true, + "defaultAction" : false, + "priority" : 40, + "config" : { } + }, { + "alias" : "VERIFY_EMAIL", + "name" : "Verify Email", + "providerId" : "VERIFY_EMAIL", + "enabled" : true, + "defaultAction" : false, + "priority" : 50, + "config" : { } + }, { + "alias" : "delete_account", + "name" : "Delete Account", + "providerId" : "delete_account", + "enabled" : false, + "defaultAction" : false, + "priority" : 60, + "config" : { } + }, { + "alias" : "update_user_locale", + "name" : "Update User Locale", + "providerId" : "update_user_locale", + "enabled" : true, + "defaultAction" : false, + "priority" : 1000, + "config" : { } + } ], + "browserFlow" : "browser", + "registrationFlow" : "registration", + "directGrantFlow" : "direct grant", + "resetCredentialsFlow" : "reset credentials", + "clientAuthenticationFlow" : "clients", + "dockerAuthenticationFlow" : "docker auth", + "attributes" : { + "cibaBackchannelTokenDeliveryMode" : "poll", + "cibaAuthRequestedUserHint" : "login_hint", + "clientOfflineSessionMaxLifespan" : "0", + "oauth2DevicePollingInterval" : "5", + "clientSessionIdleTimeout" : "0", + "actionTokenGeneratedByUserLifespan-execute-actions" : "", + "actionTokenGeneratedByUserLifespan-verify-email" : "", + "clientOfflineSessionIdleTimeout" : "0", + "actionTokenGeneratedByUserLifespan-reset-credentials" : "", + "cibaInterval" : "5", + "realmReusableOtpCode" : "false", + "cibaExpiresIn" : "120", + "oauth2DeviceCodeLifespan" : "600", + "actionTokenGeneratedByUserLifespan-idp-verify-account-via-email" : "", + "parRequestUriLifespan" : "60", + "clientSessionMaxLifespan" : "0" }, - "keycloakVersion": "19.0.3", - "userManagedAccessAllowed": false, - "clientProfiles": { - "profiles": [] + "keycloakVersion" : "20.0.1", + "userManagedAccessAllowed" : false, + "clientProfiles" : { + "profiles" : [ ] }, - "clientPolicies": { - "policies": [] + "clientPolicies" : { + "policies" : [ ] } } diff --git a/bin/start_keycloak b/bin/start_keycloak index 002c2668..32b502ca 100755 --- a/bin/start_keycloak +++ b/bin/start_keycloak @@ -27,7 +27,7 @@ docker run \ -e KEYCLOAK_LOGLEVEL=ALL \ -e ROOT_LOGLEVEL=ALL \ -e KEYCLOAK_ADMIN=admin \ - -e KEYCLOAK_ADMIN_PASSWORD=admin quay.io/keycloak/keycloak:19.0.3 start-dev \ + -e KEYCLOAK_ADMIN_PASSWORD=admin quay.io/keycloak/keycloak:20.0.1 start-dev \ -Dkeycloak.profile.feature.token_exchange=enabled \ -Dkeycloak.profile.feature.admin_fine_grained_authz=enabled diff --git a/conftest.py b/conftest.py index 4751b858..c3af9433 100644 --- a/conftest.py +++ b/conftest.py @@ -96,7 +96,7 @@ def setup_process_instances_for_reports( # ) process_instances = [] for data in [kay(), ray(), jay()]: - process_instance = ProcessInstanceService.create_process_instance( + process_instance = ProcessInstanceService.create_process_instance_from_process_model_identifier( # process_group_identifier=process_group_id, process_model_identifier=process_model_identifier, user=user, diff --git a/keycloak/Dockerfile b/keycloak/Dockerfile index 14c47206..60837bb6 100644 --- a/keycloak/Dockerfile +++ b/keycloak/Dockerfile @@ -1,4 +1,4 @@ -FROM quay.io/keycloak/keycloak:19.0.3 as builder +FROM quay.io/keycloak/keycloak:20.0.1 as builder ENV KEYCLOAK_LOGLEVEL="ALL" ENV ROOT_LOGLEVEL="ALL" diff --git a/migrations/env.py b/migrations/env.py index 68feded2..630e381a 100644 --- a/migrations/env.py +++ b/migrations/env.py @@ -1,5 +1,3 @@ -from __future__ import with_statement - import logging from logging.config import fileConfig diff --git a/migrations/versions/70223f5c7b98_.py b/migrations/versions/4d75421c0af0_.py similarity index 92% rename from migrations/versions/70223f5c7b98_.py rename to migrations/versions/4d75421c0af0_.py index 0d920944..34fa1e97 100644 --- a/migrations/versions/70223f5c7b98_.py +++ b/migrations/versions/4d75421c0af0_.py @@ -1,8 +1,8 @@ """empty message -Revision ID: 70223f5c7b98 +Revision ID: 4d75421c0af0 Revises: -Create Date: 2022-11-20 19:54:45.061376 +Create Date: 2022-12-06 17:42:56.417673 """ from alembic import op @@ -10,7 +10,7 @@ import sqlalchemy as sa # revision identifiers, used by Alembic. -revision = '70223f5c7b98' +revision = '4d75421c0af0' down_revision = None branch_labels = None depends_on = None @@ -79,8 +79,7 @@ def upgrade(): sa.Column('email', sa.String(length=255), nullable=True), sa.PrimaryKeyConstraint('id'), sa.UniqueConstraint('service', 'service_id', name='service_key'), - sa.UniqueConstraint('uid'), - sa.UniqueConstraint('username') + sa.UniqueConstraint('uid') ) op.create_table('message_correlation_property', sa.Column('id', sa.Integer(), nullable=False), @@ -97,14 +96,12 @@ def upgrade(): sa.Column('id', sa.Integer(), nullable=False), sa.Column('message_model_id', sa.Integer(), nullable=False), sa.Column('process_model_identifier', sa.String(length=50), nullable=False), - sa.Column('process_group_identifier', sa.String(length=50), nullable=False), sa.Column('updated_at_in_seconds', sa.Integer(), nullable=True), sa.Column('created_at_in_seconds', sa.Integer(), nullable=True), sa.ForeignKeyConstraint(['message_model_id'], ['message_model.id'], ), sa.PrimaryKeyConstraint('id'), sa.UniqueConstraint('message_model_id') ) - op.create_index(op.f('ix_message_triggerable_process_model_process_group_identifier'), 'message_triggerable_process_model', ['process_group_identifier'], unique=False) op.create_index(op.f('ix_message_triggerable_process_model_process_model_identifier'), 'message_triggerable_process_model', ['process_model_identifier'], unique=False) op.create_table('principal', sa.Column('id', sa.Integer(), nullable=False), @@ -120,7 +117,7 @@ def upgrade(): op.create_table('process_instance', sa.Column('id', sa.Integer(), nullable=False), sa.Column('process_model_identifier', sa.String(length=255), nullable=False), - sa.Column('process_group_identifier', sa.String(length=50), nullable=False), + sa.Column('process_model_display_name', sa.String(length=255), nullable=False), sa.Column('process_initiator_id', sa.Integer(), nullable=False), sa.Column('bpmn_json', sa.JSON(), nullable=True), sa.Column('start_in_seconds', sa.Integer(), nullable=True), @@ -134,7 +131,7 @@ def upgrade(): sa.ForeignKeyConstraint(['process_initiator_id'], ['user.id'], ), sa.PrimaryKeyConstraint('id') ) - op.create_index(op.f('ix_process_instance_process_group_identifier'), 'process_instance', ['process_group_identifier'], unique=False) + op.create_index(op.f('ix_process_instance_process_model_display_name'), 'process_instance', ['process_model_display_name'], unique=False) op.create_index(op.f('ix_process_instance_process_model_identifier'), 'process_instance', ['process_model_identifier'], unique=False) op.create_table('process_instance_report', sa.Column('id', sa.Integer(), nullable=False), @@ -168,17 +165,6 @@ def upgrade(): sa.PrimaryKeyConstraint('id'), sa.UniqueConstraint('key') ) - op.create_table('spiff_step_details', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('process_instance_id', sa.Integer(), nullable=False), - sa.Column('spiff_step', sa.Integer(), nullable=False), - sa.Column('task_json', sa.JSON(), nullable=False), - sa.Column('timestamp', sa.DECIMAL(precision=17, scale=6), nullable=False), - sa.Column('completed_by_user_id', sa.Integer(), nullable=True), - sa.Column('lane_assignment_id', sa.Integer(), nullable=True), - sa.ForeignKeyConstraint(['lane_assignment_id'], ['group.id'], ), - sa.PrimaryKeyConstraint('id') - ) op.create_table('user_group_assignment', sa.Column('id', sa.Integer(), nullable=False), sa.Column('user_id', sa.Integer(), nullable=False), @@ -251,6 +237,30 @@ def upgrade(): sa.PrimaryKeyConstraint('id'), sa.UniqueConstraint('principal_id', 'permission_target_id', 'permission', name='permission_assignment_uniq') ) + op.create_table('process_instance_metadata', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('process_instance_id', sa.Integer(), nullable=False), + sa.Column('key', sa.String(length=255), nullable=False), + sa.Column('value', sa.String(length=255), nullable=False), + sa.Column('updated_at_in_seconds', sa.Integer(), nullable=False), + sa.Column('created_at_in_seconds', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['process_instance_id'], ['process_instance.id'], ), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('process_instance_id', 'key', name='process_instance_metadata_unique') + ) + op.create_index(op.f('ix_process_instance_metadata_key'), 'process_instance_metadata', ['key'], unique=False) + op.create_table('spiff_step_details', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('process_instance_id', sa.Integer(), nullable=False), + sa.Column('spiff_step', sa.Integer(), nullable=False), + sa.Column('task_json', sa.JSON(), nullable=False), + sa.Column('timestamp', sa.DECIMAL(precision=17, scale=6), nullable=False), + sa.Column('completed_by_user_id', sa.Integer(), nullable=True), + sa.Column('lane_assignment_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['lane_assignment_id'], ['group.id'], ), + sa.ForeignKeyConstraint(['process_instance_id'], ['process_instance.id'], ), + sa.PrimaryKeyConstraint('id') + ) op.create_table('active_task_user', sa.Column('id', sa.Integer(), nullable=False), sa.Column('active_task_id', sa.Integer(), nullable=False), @@ -284,6 +294,9 @@ def downgrade(): op.drop_index(op.f('ix_active_task_user_user_id'), table_name='active_task_user') op.drop_index(op.f('ix_active_task_user_active_task_id'), table_name='active_task_user') op.drop_table('active_task_user') + op.drop_table('spiff_step_details') + op.drop_index(op.f('ix_process_instance_metadata_key'), table_name='process_instance_metadata') + op.drop_table('process_instance_metadata') op.drop_table('permission_assignment') op.drop_table('message_instance') op.drop_index(op.f('ix_message_correlation_value'), table_name='message_correlation') @@ -293,18 +306,16 @@ def downgrade(): op.drop_table('message_correlation') op.drop_table('active_task') op.drop_table('user_group_assignment') - op.drop_table('spiff_step_details') op.drop_table('secret') op.drop_table('refresh_token') op.drop_index(op.f('ix_process_instance_report_identifier'), table_name='process_instance_report') op.drop_index(op.f('ix_process_instance_report_created_by_id'), table_name='process_instance_report') op.drop_table('process_instance_report') op.drop_index(op.f('ix_process_instance_process_model_identifier'), table_name='process_instance') - op.drop_index(op.f('ix_process_instance_process_group_identifier'), table_name='process_instance') + op.drop_index(op.f('ix_process_instance_process_model_display_name'), table_name='process_instance') op.drop_table('process_instance') op.drop_table('principal') op.drop_index(op.f('ix_message_triggerable_process_model_process_model_identifier'), table_name='message_triggerable_process_model') - op.drop_index(op.f('ix_message_triggerable_process_model_process_group_identifier'), table_name='message_triggerable_process_model') op.drop_table('message_triggerable_process_model') op.drop_index(op.f('ix_message_correlation_property_identifier'), table_name='message_correlation_property') op.drop_table('message_correlation_property') diff --git a/poetry.lock b/poetry.lock index ddb3b37d..a23004b4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -44,12 +44,12 @@ python-versions = "*" dev = ["black", "coverage", "isort", "pre-commit", "pyenchant", "pylint"] [[package]] -name = "APScheduler" -version = "3.9.1" +name = "apscheduler" +version = "3.9.1.post1" description = "In-process task scheduler with Cron-like capabilities" category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" +python-versions = "!=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" [package.dependencies] pytz = "*" @@ -477,6 +477,17 @@ six = ">=1.9.0" gmpy = ["gmpy"] gmpy2 = ["gmpy2"] +[[package]] +name = "exceptiongroup" +version = "1.0.4" +description = "Backport of PEP 654 (exception groups)" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.extras] +test = ["pytest (>=6)"] + [[package]] name = "filelock" version = "3.8.0" @@ -929,22 +940,6 @@ category = "main" optional = false python-versions = ">=3.6" -[[package]] -name = "libcst" -version = "0.4.7" -description = "A concrete syntax tree with AST-like properties for Python 3.5, 3.6, 3.7, 3.8, 3.9, and 3.10 programs." -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -pyyaml = ">=5.2" -typing-extensions = ">=3.7.4.2" -typing-inspect = ">=0.4.0" - -[package.extras] -dev = ["black (==22.3.0)", "coverage (>=4.5.4)", "fixit (==0.1.1)", "flake8 (>=3.7.8)", "hypothesis (>=4.36.0)", "hypothesmith (>=0.0.4)", "jinja2 (==3.0.3)", "jupyter (>=1.0.0)", "maturin (>=0.8.3,<0.9)", "nbsphinx (>=0.4.2)", "prompt-toolkit (>=2.0.9)", "pyre-check (==0.9.9)", "setuptools-rust (>=0.12.1)", "setuptools-scm (>=6.0.1)", "slotscheck (>=0.7.1)", "sphinx-rtd-theme (>=0.4.3)", "ufmt (==1.3)", "usort (==1.0.0rc1)"] - [[package]] name = "livereload" version = "2.6.3" @@ -1050,18 +1045,6 @@ category = "dev" optional = false python-versions = "*" -[[package]] -name = "MonkeyType" -version = "22.2.0" -description = "Generating type annotations from sampled production types" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -libcst = ">=0.3.7" -mypy-extensions = "*" - [[package]] name = "mypy" version = "0.982" @@ -1240,14 +1223,6 @@ category = "main" optional = false python-versions = ">=3.6" -[[package]] -name = "py" -version = "1.11.0" -description = "library with cross-python path, ini-parsing, io, code, log facilities" -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - [[package]] name = "pyasn1" version = "0.4.8" @@ -1332,7 +1307,7 @@ python-versions = ">=3.7" [[package]] name = "pytest" -version = "7.1.3" +version = "7.2.0" description = "pytest: simple powerful testing with Python" category = "main" optional = false @@ -1341,11 +1316,11 @@ python-versions = ">=3.7" [package.dependencies] attrs = ">=19.2.0" colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" packaging = "*" pluggy = ">=0.12,<2.0" -py = ">=1.8.2" -tomli = ">=1.0.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} [package.extras] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] @@ -1876,7 +1851,7 @@ lxml = "*" type = "git" url = "https://github.com/sartography/SpiffWorkflow" reference = "main" -resolved_reference = "46f410a2852baeedc8f9ac5165347ce6d4470594" +resolved_reference = "ffb1686757f944065580dd2db8def73d6c1f0134" [[package]] name = "SQLAlchemy" @@ -2094,18 +2069,6 @@ category = "main" optional = false python-versions = ">=3.7" -[[package]] -name = "typing-inspect" -version = "0.8.0" -description = "Runtime inspection utilities for typing module." -category = "dev" -optional = false -python-versions = "*" - -[package.dependencies] -mypy-extensions = ">=0.3.0" -typing-extensions = ">=3.7.4" - [[package]] name = "tzdata" version = "2022.5" @@ -2259,7 +2222,7 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools" [metadata] lock-version = "1.1" python-versions = ">=3.9,<3.12" -content-hash = "a6d3882a3ab142b82201b83ee8a0552fd16112c4540e2a1dbcb5c38599b917c1" +content-hash = "bbbd1c8bdce7f3dd7ec17c62b85dc7c95045fe500a759bb1a89c93add58a2a25" [metadata.files] alabaster = [ @@ -2278,9 +2241,9 @@ aniso8601 = [ {file = "aniso8601-9.0.1-py2.py3-none-any.whl", hash = "sha256:1d2b7ef82963909e93c4f24ce48d4de9e66009a21bf1c1e1c85bdd0812fe412f"}, {file = "aniso8601-9.0.1.tar.gz", hash = "sha256:72e3117667eedf66951bb2d93f4296a56b94b078a8a95905a052611fb3f1b973"}, ] -APScheduler = [ - {file = "APScheduler-3.9.1-py2.py3-none-any.whl", hash = "sha256:ddc25a0ddd899de44d7f451f4375fb971887e65af51e41e5dcf681f59b8b2c9a"}, - {file = "APScheduler-3.9.1.tar.gz", hash = "sha256:65e6574b6395498d371d045f2a8a7e4f7d50c6ad21ef7313d15b1c7cf20df1e3"}, +apscheduler = [ + {file = "APScheduler-3.9.1.post1-py2.py3-none-any.whl", hash = "sha256:c8c618241dbb2785ed5a687504b14cb1851d6f7b5a4edf3a51e39cc6a069967a"}, + {file = "APScheduler-3.9.1.post1.tar.gz", hash = "sha256:b2bea0309569da53a7261bfa0ce19c67ddbfe151bda776a6a907579fdbd3eb2a"}, ] astroid = [ {file = "astroid-2.12.12-py3-none-any.whl", hash = "sha256:72702205200b2a638358369d90c222d74ebc376787af8fb2f7f2a86f7b5cc85f"}, @@ -2484,6 +2447,10 @@ ecdsa = [ {file = "ecdsa-0.18.0-py2.py3-none-any.whl", hash = "sha256:80600258e7ed2f16b9aa1d7c295bd70194109ad5a30fdee0eaeefef1d4c559dd"}, {file = "ecdsa-0.18.0.tar.gz", hash = "sha256:190348041559e21b22a1d65cee485282ca11a6f81d503fddb84d5017e9ed1e49"}, ] +exceptiongroup = [ + {file = "exceptiongroup-1.0.4-py3-none-any.whl", hash = "sha256:542adf9dea4055530d6e1279602fa5cb11dab2395fa650b8674eaec35fc4a828"}, + {file = "exceptiongroup-1.0.4.tar.gz", hash = "sha256:bd14967b79cd9bdb54d97323216f8fdf533e278df937aa2a90089e7d6e06e5ec"}, +] filelock = [ {file = "filelock-3.8.0-py3-none-any.whl", hash = "sha256:617eb4e5eedc82fc5f47b6d61e4d11cb837c56cb4544e39081099fa17ad109d4"}, {file = "filelock-3.8.0.tar.gz", hash = "sha256:55447caa666f2198c5b6b13a26d2084d26fa5b115c00d065664b2124680c4edc"}, @@ -2703,32 +2670,6 @@ lazy-object-proxy = [ {file = "lazy_object_proxy-1.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:677ea950bef409b47e51e733283544ac3d660b709cfce7b187f5ace137960d61"}, {file = "lazy_object_proxy-1.7.1-pp37.pp38-none-any.whl", hash = "sha256:d66906d5785da8e0be7360912e99c9188b70f52c422f9fc18223347235691a84"}, ] -libcst = [ - {file = "libcst-0.4.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dc6f8965b6ca68d47e11321772887d81fa6fd8ea86e6ef87434ca2147de10747"}, - {file = "libcst-0.4.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a8f47d809df59fcd83058b777b86a300154ee3a1f1b0523a398a67b5f8affd4c"}, - {file = "libcst-0.4.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c0d19de56aa733b4ef024527e3ce4896d4b0e9806889797f409ec24caa651a44"}, - {file = "libcst-0.4.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31da97bc986dc3f7a97f7d431fa911932aaf716d2f8bcda947fc964afd3b57cd"}, - {file = "libcst-0.4.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:71b2e2c5e33e53669c20de0853cecfac1ffb8657ee727ab8527140f39049b820"}, - {file = "libcst-0.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:76fae68bd6b7ce069e267b3322c806b4305341cea78d161ae40e0ed641c8c660"}, - {file = "libcst-0.4.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:bac76d69980bb3254f503f52128c256ef4d1bcbaabe4a17c3a9ebcd1fc0472c0"}, - {file = "libcst-0.4.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26f86535271eaefe84a99736875566a038449f92e1a2a61ea0b588d8359fbefd"}, - {file = "libcst-0.4.7-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:617f7fa2610a8c86cf22d8d03416f25391383d05bd0ad1ca8ef68023ddd6b4f6"}, - {file = "libcst-0.4.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c3637fffe476c5b4ee2225c6474b83382518f2c1b2fe4771039e06bdd7835a4a"}, - {file = "libcst-0.4.7-cp37-cp37m-win_amd64.whl", hash = "sha256:f56565124c2541adee0634e411b2126b3f335306d19e91ed2bfe52efa698b219"}, - {file = "libcst-0.4.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0ca2771ff3cfdf1f148349f89fcae64afa365213ed5c2703a69a89319325d0c8"}, - {file = "libcst-0.4.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:aa438131b7befc7e5a3cbadb5a7b1506305de5d62262ea0556add0152f40925e"}, - {file = "libcst-0.4.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c6bd66a8be2ffad7b968d90dae86c62fd4739c0e011d71f3e76544a891ae743"}, - {file = "libcst-0.4.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:214a9c4f4f90cd5b4bfa18e17877da4dd9a896821d9af9be86fa3effdc289b9b"}, - {file = "libcst-0.4.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27a37f2b459a8b51a41e260bd89c24ae41ab1d658f610c91650c79b1bbf27138"}, - {file = "libcst-0.4.7-cp38-cp38-win_amd64.whl", hash = "sha256:2f6766391d90472f036b88a95251c87d498ab068c377724f212ab0cc20509a68"}, - {file = "libcst-0.4.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:234293aa8681a3d47fef1716c5622797a81cbe85a9381fe023815468cfe20eed"}, - {file = "libcst-0.4.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fa618dc359663a0a097c633452b104c1ca93365da7a811e655c6944f6b323239"}, - {file = "libcst-0.4.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3569d9901c18940632414fb7a0943bffd326db9f726a9c041664926820857815"}, - {file = "libcst-0.4.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:beb5347e46b419f782589da060e9300957e71d561aa5574309883b71f93c1dfe"}, - {file = "libcst-0.4.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e541ccfeebda1ae5f005fc120a5bf3e8ac9ccfda405ec3efd3df54fc4688ac3"}, - {file = "libcst-0.4.7-cp39-cp39-win_amd64.whl", hash = "sha256:3a2b7253cd2e3f0f8a3e23b5c2acb492811d865ef36e0816091c925f32b713d2"}, - {file = "libcst-0.4.7.tar.gz", hash = "sha256:95c52c2130531f6e726a3b077442cfd486975435fecf3db8224d43fba7b85099"}, -] livereload = [ {file = "livereload-2.6.3.tar.gz", hash = "sha256:776f2f865e59fde56490a56bcc6773b6917366bce0c267c60ee8aaf1a0959869"}, ] @@ -2866,10 +2807,6 @@ mccabe = [ {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, ] -MonkeyType = [ - {file = "MonkeyType-22.2.0-py3-none-any.whl", hash = "sha256:3d0815c7e98a18e9267990a452548247f6775fd636e65df5a7d77100ea7ad282"}, - {file = "MonkeyType-22.2.0.tar.gz", hash = "sha256:6b0c00b49dcc5095a2c08d28246cf005e05673fc51f64d203f9a6bca2036dfab"}, -] mypy = [ {file = "mypy-0.982-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5085e6f442003fa915aeb0a46d4da58128da69325d8213b4b35cc7054090aed5"}, {file = "mypy-0.982-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:41fd1cf9bc0e1c19b9af13a6580ccb66c381a5ee2cf63ee5ebab747a4badeba3"}, @@ -3051,10 +2988,6 @@ psycopg2 = [ {file = "psycopg2-2.9.4-cp39-cp39-win_amd64.whl", hash = "sha256:849bd868ae3369932127f0771c08d1109b254f08d48dc42493c3d1b87cb2d308"}, {file = "psycopg2-2.9.4.tar.gz", hash = "sha256:d529926254e093a1b669f692a3aa50069bc71faf5b0ecd91686a78f62767d52f"}, ] -py = [ - {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, - {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"}, @@ -3118,8 +3051,8 @@ pyrsistent = [ {file = "pyrsistent-0.18.1.tar.gz", hash = "sha256:d4d61f8b993a7255ba714df3aca52700f8125289f84f704cf80916517c46eb96"}, ] pytest = [ - {file = "pytest-7.1.3-py3-none-any.whl", hash = "sha256:1377bda3466d70b55e3f5cecfa55bb7cfcf219c7964629b967c37cf0bda818b7"}, - {file = "pytest-7.1.3.tar.gz", hash = "sha256:4f365fec2dff9c1162f834d9f18af1ba13062db0c708bf7b946f8a5c76180c39"}, + {file = "pytest-7.2.0-py3-none-any.whl", hash = "sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71"}, + {file = "pytest-7.2.0.tar.gz", hash = "sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59"}, ] pytest-flask = [ {file = "pytest-flask-1.2.0.tar.gz", hash = "sha256:46fde652f77777bf02dc91205aec4ce20cdf2acbbbd66a918ab91f5c14693d3d"}, @@ -3598,10 +3531,6 @@ typing-extensions = [ {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"}, {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"}, ] -typing-inspect = [ - {file = "typing_inspect-0.8.0-py3-none-any.whl", hash = "sha256:5fbf9c1e65d4fa01e701fe12a5bca6c6e08a4ffd5bc60bfac028253a447c5188"}, - {file = "typing_inspect-0.8.0.tar.gz", hash = "sha256:8b1ff0c400943b6145df8119c41c244ca8207f1f10c9c057aeed1560e4806e3d"}, -] tzdata = [ {file = "tzdata-2022.5-py2.py3-none-any.whl", hash = "sha256:323161b22b7802fdc78f20ca5f6073639c64f1a7227c40cd3e19fd1d0ce6650a"}, {file = "tzdata-2022.5.tar.gz", hash = "sha256:e15b2b3005e2546108af42a0eb4ccab4d9e225e2dfbf4f77aad50c70a4b1f3ab"}, diff --git a/pyproject.toml b/pyproject.toml index 645c56ed..c41350d8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,7 @@ sentry-sdk = "^1.10" sphinx-autoapi = "^2.0" flask-bpmn = {git = "https://github.com/sartography/flask-bpmn", rev = "main"} # flask-bpmn = {develop = true, path = "../flask-bpmn"} -mysql-connector-python = "^8.0.29" +mysql-connector-python = "*" pytest-flask = "^1.2.0" pytest-flask-sqlalchemy = "^1.1.0" psycopg2 = "^2.9.3" @@ -45,7 +45,7 @@ marshmallow-sqlalchemy = "^0.28.0" PyJWT = "^2.6.0" gunicorn = "^20.1.0" python-keycloak = "^2.5.0" -APScheduler = "^3.9.1" +APScheduler = "*" Jinja2 = "^3.1.2" RestrictedPython = "^6.0" Flask-SQLAlchemy = "^3" @@ -75,7 +75,7 @@ types-dateparser = "^1.1.4.1" [tool.poetry.dev-dependencies] -pytest = "^7.1.2" +pytest = "*" coverage = {extras = ["toml"], version = "^6.1"} safety = "^2.3.1" mypy = ">=0.961" @@ -103,7 +103,6 @@ sphinx-click = "^4.3.0" Pygments = "^2.10.0" pyupgrade = "^3.1.0" furo = ">=2021.11.12" -MonkeyType = "^22.2.0" [tool.poetry.scripts] spiffworkflow-backend = "spiffworkflow_backend.__main__:main" diff --git a/src/spiffworkflow_backend/__init__.py b/src/spiffworkflow_backend/__init__.py index d17beac3..9599116a 100644 --- a/src/spiffworkflow_backend/__init__.py +++ b/src/spiffworkflow_backend/__init__.py @@ -19,6 +19,9 @@ from werkzeug.exceptions import NotFound import spiffworkflow_backend.load_database_models # noqa: F401 from spiffworkflow_backend.config import setup_config from spiffworkflow_backend.routes.admin_blueprint.admin_blueprint import admin_blueprint +from spiffworkflow_backend.routes.openid_blueprint.openid_blueprint import ( + openid_blueprint, +) from spiffworkflow_backend.routes.process_api_blueprint import process_api_blueprint from spiffworkflow_backend.routes.user import verify_token from spiffworkflow_backend.routes.user_blueprint import user_blueprint @@ -67,9 +70,9 @@ def start_scheduler( seconds=10, ) scheduler.add_job( - BackgroundProcessingService(app).run, + BackgroundProcessingService(app).process_waiting_process_instances, "interval", - seconds=30, + seconds=10, ) scheduler.start() @@ -103,12 +106,16 @@ def create_app() -> flask.app.Flask: app.register_blueprint(process_api_blueprint) app.register_blueprint(api_error_blueprint) app.register_blueprint(admin_blueprint, url_prefix="/admin") + app.register_blueprint(openid_blueprint, url_prefix="/openid") + # preflight options requests will be allowed if they meet the requirements of the url regex. + # we will add an Access-Control-Max-Age header to the response to tell the browser it doesn't + # need to continually keep asking for the same path. origins_re = [ r"^https?:\/\/%s(.*)" % o.replace(".", r"\.") for o in app.config["CORS_ALLOW_ORIGINS"] ] - CORS(app, origins=origins_re) + CORS(app, origins=origins_re, max_age=3600) connexion_app.add_api("api.yml", base_path="/v1.0") diff --git a/src/spiffworkflow_backend/api.yml b/src/spiffworkflow_backend/api.yml index 2840d5ae..764ba543 100755 --- a/src/spiffworkflow_backend/api.yml +++ b/src/spiffworkflow_backend/api.yml @@ -160,7 +160,7 @@ paths: schema: type: integer get: - operationId: spiffworkflow_backend.routes.process_api_blueprint.process_groups_list + operationId: spiffworkflow_backend.routes.process_api_blueprint.process_group_list summary: get list tags: - Process Groups @@ -238,6 +238,33 @@ paths: schema: $ref: "#/components/schemas/ProcessModelCategory" + /process-groups/{modified_process_group_identifier}/move: + parameters: + - name: modified_process_group_identifier + in: path + required: true + description: The unique id of an existing process group. + schema: + type: string + - name: new_location + in: query + required: true + description: the new location, as an existing process group id + schema: + type: string + put: + operationId: spiffworkflow_backend.routes.process_api_blueprint.process_group_move + summary: returns the new group + tags: + - Process Groups + responses: + "200": + description: Process Group + content: + application/json: + schema: + $ref: "#/components/schemas/ProcessModelCategory" + /process-models: parameters: - name: process_group_identifier @@ -246,6 +273,18 @@ paths: description: The group containing the models we want to return schema: type: string + - name: recursive + in: query + required: false + description: Get all sub process models recursively if true + schema: + type: boolean + - name: filter_runnable_by_user + in: query + required: false + description: Get only the process models that the user can run + schema: + type: boolean - name: page in: query required: false @@ -274,6 +313,13 @@ paths: $ref: "#/components/schemas/ProcessModel" /process-models/{modified_process_group_id}: + parameters: + - name: modified_process_group_id + in: path + required: true + description: modified id of an existing process group + schema: + type: string post: operationId: spiffworkflow_backend.routes.process_api_blueprint.process_model_create summary: Creates a new process model with the given parameters. @@ -292,9 +338,9 @@ paths: schema: $ref: "#/components/schemas/ProcessModel" - /process-models/{modified_process_model_id}/files: + /process-models/{modified_process_model_identifier}/files: parameters: - - name: modified_process_model_id + - name: modified_process_model_identifier in: path required: true description: The process_model_id, modified to replace slashes (/) @@ -372,6 +418,60 @@ paths: schema: $ref: "#/components/schemas/OkTrue" + /process-models/{modified_process_model_identifier}/move: + parameters: + - name: modified_process_model_identifier + in: path + required: true + description: the modified process model id + schema: + type: string + - name: new_location + in: query + required: true + description: the new location for the process model, as a process group id + schema: + type: string + put: + operationId: spiffworkflow_backend.routes.process_api_blueprint.process_model_move + summary: returns the new model + tags: + - Process Models + responses: + "200": + description: Process Model + content: + application/json: + schema: + $ref: "#/components/schemas/ProcessModel" + + /process-models/{modified_process_model_identifier}/publish: + parameters: + - name: modified_process_model_identifier + in: path + required: true + description: the modified process model id + schema: + type: string + - name: branch_to_update + in: query + required: false + description: the name of the branch we want to merge into + schema: + type: string + post: + operationId: spiffworkflow_backend.routes.process_api_blueprint.process_model_publish + summary: Merge changes from this model to another branch. + tags: + - Process Models + responses: + "200": + description: The process model was published. + content: + application/json: + schema: + type: string + /processes: get: operationId: spiffworkflow_backend.routes.process_api_blueprint.process_list @@ -390,6 +490,25 @@ paths: items: $ref: "#/components/schemas/Process" + /github-webhook-receive: + post: + operationId: spiffworkflow_backend.routes.process_api_blueprint.github_webhook_receive + summary: receives push webhooks from github so we can keep our process model repo up to date + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/ProcessModelCategory" + tags: + - git + responses: + "200": + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/OkTrue" + /process-instances: parameters: - name: process_model_identifier @@ -440,6 +559,24 @@ paths: description: For filtering - not_started, user_input_required, waiting, complete, error, or suspended schema: type: string + - name: initiated_by_me + in: query + required: false + description: For filtering - show instances initiated by me + schema: + type: boolean + - name: with_tasks_completed_by_me + in: query + required: false + description: For filtering - show instances with tasks completed by me + schema: + type: boolean + - name: with_tasks_completed_by_my_group + in: query + required: false + description: For filtering - show instances with tasks completed by my group + schema: + type: boolean - name: user_filter in: query required: false @@ -452,6 +589,12 @@ paths: description: Specifies the identifier of a report to use, if any schema: type: string + - name: report_id + in: query + required: false + description: Specifies the identifier of a report to use, if any + schema: + type: integer get: operationId: spiffworkflow_backend.routes.process_api_blueprint.process_instance_list summary: Returns a list of process instances for a given process model @@ -467,33 +610,6 @@ paths: items: $ref: "#/components/schemas/Workflow" - /process-instances/{process_instance_id}/task/{task_id}/update: - parameters: - - name: process_instance_id - in: path - required: true - description: The unique id of the process instance - schema: - type: string - - name: task_id - in: path - required: true - description: The unique id of the task - schema: - type: string - post: - operationId: spiffworkflow_backend.routes.process_api_blueprint.update_task_data - summary: Update the task data for requested instance and task - tags: - - Process Instances - responses: - "200": - description: Task Updated Successfully - content: - application/json: - schema: - $ref: "#/components/schemas/Workflow" - /process-models/{process_group_id}/{process_model_id}/script-unit-tests: parameters: - name: process_group_id @@ -548,15 +664,14 @@ paths: schema: $ref: "#/components/schemas/Workflow" - /process-models/{modified_process_model_id}/process-instances: + /process-instances/{modified_process_model_identifier}: parameters: - - name: modified_process_model_id + - name: modified_process_model_identifier in: path required: true description: The unique id of an existing process model. schema: type: string - # process_instance_create post: operationId: spiffworkflow_backend.routes.process_api_blueprint.process_instance_create summary: Creates an process instance from a process model and returns the instance @@ -570,28 +685,7 @@ paths: schema: $ref: "#/components/schemas/Workflow" - /process-instances/{process_instance_id}: - parameters: - - name: process_instance_id - in: path - required: true - description: The unique id of an existing process instance. - schema: - type: integer - delete: - operationId: spiffworkflow_backend.routes.process_api_blueprint.process_instance_delete - summary: Deletes a single process instance - tags: - - Process Instances - responses: - "200": - description: The process instance was deleted. - content: - application/json: - schema: - $ref: "#/components/schemas/OkTrue" - - /process-models/{modified_process_model_identifier}/process-instances/{process_instance_id}: + /process-instances/{modified_process_model_identifier}/{process_instance_id}: parameters: - name: modified_process_model_identifier in: path @@ -617,8 +711,20 @@ paths: application/json: schema: $ref: "#/components/schemas/Workflow" + delete: + operationId: spiffworkflow_backend.routes.process_api_blueprint.process_instance_delete + summary: Deletes a single process instance + tags: + - Process Instances + responses: + "200": + description: The process instance was deleted. + content: + application/json: + schema: + $ref: "#/components/schemas/OkTrue" - /process-instances/{process_instance_id}/run: + /process-instances/{modified_process_model_identifier}/{process_instance_id}/run: parameters: - name: process_instance_id in: path @@ -632,7 +738,6 @@ paths: description: Defaults to true, can be set to false if you are just looking at the workflow not completeing it. schema: type: boolean - # process_instance_run post: operationId: spiffworkflow_backend.routes.process_api_blueprint.process_instance_run summary: Run a process instance @@ -646,7 +751,7 @@ paths: schema: $ref: "#/components/schemas/Workflow" - /process-instances/{process_instance_id}/terminate: + /process-instances/{modified_process_model_identifier}/{process_instance_id}/terminate: parameters: - name: process_instance_id in: path @@ -667,7 +772,7 @@ paths: schema: $ref: "#/components/schemas/OkTrue" - /process-instances/{process_instance_id}/suspend: + /process-instances/{modified_process_model_identifier}/{process_instance_id}/suspend: parameters: - name: process_instance_id in: path @@ -688,7 +793,7 @@ paths: schema: $ref: "#/components/schemas/OkTrue" - /process-instances/{process_instance_id}/resume: + /process-instances/{modified_process_model_identifier}/{process_instance_id}/resume: parameters: - name: process_instance_id in: path @@ -750,14 +855,30 @@ paths: schema: $ref: "#/components/schemas/OkTrue" - /process-instances/reports/{report_identifier}: + /process-instances/reports/columns: + get: + operationId: spiffworkflow_backend.routes.process_api_blueprint.process_instance_report_column_list + summary: Returns all available columns for a process instance report. + tags: + - Process Instances + responses: + "200": + description: Workflow. + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Workflow" + + /process-instances/reports/{report_id}: parameters: - - name: report_identifier + - name: report_id in: path required: true description: The unique id of an existing report schema: - type: string + type: integer - name: page in: query required: false @@ -809,9 +930,9 @@ paths: schema: $ref: "#/components/schemas/OkTrue" - /process-models/{modified_process_model_id}/files/{file_name}: + /process-models/{modified_process_model_identifier}/files/{file_name}: parameters: - - name: modified_process_model_id + - name: modified_process_model_identifier in: path required: true description: The modified process model id @@ -988,8 +1109,14 @@ paths: items: $ref: "#/components/schemas/Task" - /process-instances/{modified_process_model_id}/{process_instance_id}/tasks: + /task-data/{modified_process_model_identifier}/{process_instance_id}: parameters: + - name: modified_process_model_identifier + in: path + required: true + description: The modified id of an existing process model + schema: + type: string - name: process_instance_id in: path required: true @@ -1023,11 +1150,44 @@ paths: items: $ref: "#/components/schemas/Task" - /service_tasks: + /task-data/{modified_process_model_identifier}/{process_instance_id}/{task_id}: + parameters: + - name: modified_process_model_identifier + in: path + required: true + description: The modified id of an existing process model + schema: + type: string + - name: process_instance_id + in: path + required: true + description: The unique id of an existing process instance. + schema: + type: integer + - name: task_id + in: path + required: true + description: The unique id of the task. + schema: + type: string + put: + operationId: spiffworkflow_backend.routes.process_api_blueprint.update_task_data + summary: Update the task data for requested instance and task + tags: + - Process Instances + responses: + "200": + description: Task Updated Successfully + content: + application/json: + schema: + $ref: "#/components/schemas/Workflow" + + /service-tasks: get: tags: - Service Tasks - operationId: spiffworkflow_backend.routes.process_api_blueprint.service_tasks_show + operationId: spiffworkflow_backend.routes.process_api_blueprint.service_task_list summary: Gets all available service task connectors responses: "200": @@ -1207,7 +1367,7 @@ paths: schema: $ref: "#/components/schemas/Workflow" - /process-instances/{process_instance_id}/logs: + /logs/{modified_process_model_identifier}/{process_instance_id}: parameters: - name: process_instance_id in: path @@ -1227,6 +1387,12 @@ paths: description: The number of items to show per page. Defaults to page 10. schema: type: integer + - name: detailed + in: query + required: false + description: Show the detailed view, which includes all log entries + schema: + type: boolean get: tags: - Process Instances @@ -1568,10 +1734,6 @@ components: type: integer x-nullable: true example: 12 - study_id: - type: integer - x-nullable: true - example: 42 user_id: type: string x-nullable: true @@ -1696,8 +1858,6 @@ components: type: integer num_tasks_incomplete: type: integer - study_id: - type: integer example: id: 291234 @@ -1832,9 +1992,6 @@ components: workflow_id: example: 42 type: integer - study_id: - example: 187 - type: integer user_uid: example: "dhf8r" type: string diff --git a/src/spiffworkflow_backend/config/__init__.py b/src/spiffworkflow_backend/config/__init__.py index b56683ca..106b0735 100644 --- a/src/spiffworkflow_backend/config/__init__.py +++ b/src/spiffworkflow_backend/config/__init__.py @@ -14,13 +14,13 @@ class ConfigurationError(Exception): def setup_database_uri(app: Flask) -> None: """Setup_database_uri.""" - if os.environ.get("SPIFFWORKFLOW_BACKEND_DATABASE_URI") is None: + if app.config.get("SPIFFWORKFLOW_BACKEND_DATABASE_URI") is None: database_name = f"spiffworkflow_backend_{app.config['ENV_IDENTIFIER']}" - if os.environ.get("SPIFF_DATABASE_TYPE") == "sqlite": + if app.config.get("SPIFF_DATABASE_TYPE") == "sqlite": app.config[ "SQLALCHEMY_DATABASE_URI" ] = f"sqlite:///{app.instance_path}/db_{app.config['ENV_IDENTIFIER']}.sqlite3" - elif os.environ.get("SPIFF_DATABASE_TYPE") == "postgres": + elif app.config.get("SPIFF_DATABASE_TYPE") == "postgres": app.config[ "SQLALCHEMY_DATABASE_URI" ] = f"postgresql://spiffworkflow_backend:spiffworkflow_backend@localhost:5432/{database_name}" @@ -33,11 +33,22 @@ def setup_database_uri(app: Flask) -> None: "SQLALCHEMY_DATABASE_URI" ] = f"mysql+mysqlconnector://root:{db_pswd}@localhost/{database_name}" else: - app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get( + app.config["SQLALCHEMY_DATABASE_URI"] = app.config.get( "SPIFFWORKFLOW_BACKEND_DATABASE_URI" ) +def load_config_file(app: Flask, env_config_module: str) -> None: + """Load_config_file.""" + try: + app.config.from_object(env_config_module) + except ImportStringError as exception: + if os.environ.get("TERRAFORM_DEPLOYED_ENVIRONMENT") != "true": + raise ModuleNotFoundError( + f"Cannot find config module: {env_config_module}" + ) from exception + + def setup_config(app: Flask) -> None: """Setup_config.""" # ensure the instance folder exists @@ -52,30 +63,22 @@ def setup_config(app: Flask) -> None: app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False app.config.from_object("spiffworkflow_backend.config.default") + env_config_prefix = "spiffworkflow_backend.config." + if ( + os.environ.get("TERRAFORM_DEPLOYED_ENVIRONMENT") == "true" + and os.environ.get("SPIFFWORKFLOW_BACKEND_ENV") is not None + ): + load_config_file(app, f"{env_config_prefix}terraform_deployed_environment") + + env_config_module = env_config_prefix + app.config["ENV_IDENTIFIER"] + load_config_file(app, env_config_module) + # This allows config/testing.py or instance/config.py to override the default config if "ENV_IDENTIFIER" in app.config and app.config["ENV_IDENTIFIER"] == "testing": app.config.from_pyfile("config/testing.py", silent=True) else: app.config.from_pyfile(f"{app.instance_path}/config.py", silent=True) - env_config_prefix = "spiffworkflow_backend.config." - env_config_module = env_config_prefix + app.config["ENV_IDENTIFIER"] - try: - app.config.from_object(env_config_module) - except ImportStringError as exception: - if ( - os.environ.get("TERRAFORM_DEPLOYED_ENVIRONMENT") == "true" - and os.environ.get("SPIFFWORKFLOW_BACKEND_ENV") is not None - ): - app.config.from_object("{env_config_prefix}terraform_deployed_environment") - else: - raise ModuleNotFoundError( - f"Cannot find config module: {env_config_module}" - ) from exception - - setup_database_uri(app) - setup_logger(app) - app.config["PERMISSIONS_FILE_FULLPATH"] = None if app.config["SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME"]: app.config["PERMISSIONS_FILE_FULLPATH"] = os.path.join( @@ -92,5 +95,8 @@ def setup_config(app: Flask) -> None: if app.config["BPMN_SPEC_ABSOLUTE_DIR"] is None: raise ConfigurationError("BPMN_SPEC_ABSOLUTE_DIR config must be set") + setup_database_uri(app) + setup_logger(app) + thread_local_data = threading.local() app.config["THREAD_LOCAL_DATA"] = thread_local_data diff --git a/src/spiffworkflow_backend/config/default.py b/src/spiffworkflow_backend/config/default.py index 53d670c7..d0d6a401 100644 --- a/src/spiffworkflow_backend/config/default.py +++ b/src/spiffworkflow_backend/config/default.py @@ -27,12 +27,13 @@ CONNECTOR_PROXY_URL = environ.get( "CONNECTOR_PROXY_URL", default="http://localhost:7004" ) -GIT_COMMIT_ON_SAVE = environ.get("GIT_COMMIT_ON_SAVE", default="false") == "true" - # Open ID server -OPEN_ID_SERVER_URL = environ.get("OPEN_ID_SERVER_URL", default="http://localhost:7002") +OPEN_ID_SERVER_URL = environ.get( + "OPEN_ID_SERVER_URL", default="http://localhost:7002/realms/spiffworkflow" +) +# Replace above line with this to use the built-in Open ID Server. +# OPEN_ID_SERVER_URL = environ.get("OPEN_ID_SERVER_URL", default="http://localhost:7000/openid") OPEN_ID_CLIENT_ID = environ.get("OPEN_ID_CLIENT_ID", default="spiffworkflow-backend") -OPEN_ID_REALM_NAME = environ.get("OPEN_ID_REALM_NAME", default="spiffworkflow") OPEN_ID_CLIENT_SECRET_KEY = environ.get( "OPEN_ID_CLIENT_SECRET_KEY", default="JXeQExm0JhQPLumgHtIIqf52bDalHz0q" ) # noqa: S105 @@ -57,3 +58,19 @@ SENTRY_TRACES_SAMPLE_RATE = environ.get( SPIFFWORKFLOW_BACKEND_LOG_LEVEL = environ.get( "SPIFFWORKFLOW_BACKEND_LOG_LEVEL", default="info" ) + +# When a user clicks on the `Publish` button, this is the default branch this server merges into. +# I.e., dev server could have `staging` here. Staging server might have `production` here. +GIT_BRANCH_TO_PUBLISH_TO = environ.get("GIT_BRANCH_TO_PUBLISH_TO") +GIT_BRANCH = environ.get("GIT_BRANCH") +GIT_CLONE_URL_FOR_PUBLISHING = environ.get("GIT_CLONE_URL") +GIT_COMMIT_ON_SAVE = environ.get("GIT_COMMIT_ON_SAVE", default="false") == "true" + +# Datbase Configuration +SPIFF_DATABASE_TYPE = environ.get( + "SPIFF_DATABASE_TYPE", default="mysql" +) # can also be sqlite, postgres +# Overide above with specific sqlalchymy connection string. +SPIFFWORKFLOW_BACKEND_DATABASE_URI = environ.get( + "SPIFFWORKFLOW_BACKEND_DATABASE_URI", default=None +) diff --git a/src/spiffworkflow_backend/config/demo.py b/src/spiffworkflow_backend/config/demo.py index db5abf0e..06e9184d 100644 --- a/src/spiffworkflow_backend/config/demo.py +++ b/src/spiffworkflow_backend/config/demo.py @@ -2,8 +2,8 @@ from os import environ GIT_COMMIT_ON_SAVE = True -GIT_COMMIT_USERNAME = "demo" -GIT_COMMIT_EMAIL = "demo@example.com" +GIT_USERNAME = "demo" +GIT_USER_EMAIL = "demo@example.com" SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME = environ.get( "SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME", default="terraform_deployed_environment.yml", diff --git a/src/spiffworkflow_backend/config/dev.py b/src/spiffworkflow_backend/config/dev.py new file mode 100644 index 00000000..ce6b516c --- /dev/null +++ b/src/spiffworkflow_backend/config/dev.py @@ -0,0 +1,8 @@ +"""Dev.""" +from os import environ + +GIT_BRANCH_TO_PUBLISH_TO = environ.get("GIT_BRANCH_TO_PUBLISH_TO", default="staging") +GIT_USERNAME = environ.get("GIT_USERNAME", default="sartography-automated-committer") +GIT_USER_EMAIL = environ.get( + "GIT_USER_EMAIL", default="sartography-automated-committer@users.noreply.github.com" +) diff --git a/src/spiffworkflow_backend/config/development.py b/src/spiffworkflow_backend/config/development.py index c3c47946..15cbead8 100644 --- a/src/spiffworkflow_backend/config/development.py +++ b/src/spiffworkflow_backend/config/development.py @@ -12,3 +12,10 @@ SPIFFWORKFLOW_BACKEND_LOG_LEVEL = environ.get( RUN_BACKGROUND_SCHEDULER = ( environ.get("RUN_BACKGROUND_SCHEDULER", default="true") == "true" ) +GIT_CLONE_URL_FOR_PUBLISHING = environ.get( + "GIT_CLONE_URL", default="https://github.com/sartography/sample-process-models.git" +) +GIT_USERNAME = "sartography-automated-committer" +GIT_USER_EMAIL = f"{GIT_USERNAME}@users.noreply.github.com" +GIT_BRANCH_TO_PUBLISH_TO = "main" +GIT_BRANCH = "main" diff --git a/src/spiffworkflow_backend/config/permissions/development.yml b/src/spiffworkflow_backend/config/permissions/development.yml index 618a9719..419c925f 100644 --- a/src/spiffworkflow_backend/config/permissions/development.yml +++ b/src/spiffworkflow_backend/config/permissions/development.yml @@ -1,29 +1,30 @@ default_group: everybody +users: + admin: + email: admin@spiffworkflow.org + password: admin + preferred_username: Admin + groups: admin: users: [ + admin, jakub, kb, alex, dan, mike, jason, - amir, + j, jarrad, elizabeth, jon, - harmeet, - sasha, - manuchehr, natalia, ] Finance Team: - users: [finance_user1, jason] - - Project Lead: users: [ jakub, @@ -31,18 +32,42 @@ groups: dan, mike, jason, + j, + amir, jarrad, elizabeth, jon, natalia, + sasha, + fin, + fin1, + ] + + demo: + users: + [ + core, + fin, + fin1, + harmeet, + sasha, manuchehr, + lead, + lead1 + ] + + core-contributor: + users: + [ + core, + harmeet, ] permissions: admin: groups: [admin] users: [] - allowed_permissions: [create, read, update, delete, list, instantiate] + allowed_permissions: [create, read, update, delete] uri: /* tasks-crud: @@ -50,46 +75,129 @@ permissions: users: [] allowed_permissions: [create, read, update, delete] uri: /v1.0/tasks/* - - process-model-read-all: + service-tasks: groups: [everybody] users: [] allowed_permissions: [read] - uri: /v1.0/process-models/* + uri: /v1.0/service-tasks - process-group-read-all: + + # read all for everybody + read-all-process-groups: groups: [everybody] users: [] allowed_permissions: [read] uri: /v1.0/process-groups/* - - process-instance-list: + read-all-process-models: groups: [everybody] users: [] allowed_permissions: [read] - uri: /v1.0/process-instances + uri: /v1.0/process-models/* + read-all-process-instance: + groups: [everybody] + users: [] + allowed_permissions: [read] + uri: /v1.0/process-instances/* + read-process-instance-reports: + groups: [everybody] + users: [] + allowed_permissions: [read] + uri: /v1.0/process-instances/reports/* + processes-read: + groups: [everybody] + users: [] + allowed_permissions: [read] + uri: /v1.0/processes - # TODO: all uris should really have the same structure - finance-admin-group: + task-data-read: + groups: [demo] + users: [] + allowed_permissions: [read] + uri: /v1.0/task-data/* + + + manage-procurement-admin: + groups: ["Project Lead"] + users: [] + allowed_permissions: [create, read, update, delete] + uri: /v1.0/process-groups/manage-procurement:* + manage-procurement-admin-slash: + groups: ["Project Lead"] + users: [] + allowed_permissions: [create, read, update, delete] + uri: /v1.0/process-groups/manage-procurement/* + manage-procurement-admin-models: + groups: ["Project Lead"] + users: [] + allowed_permissions: [create, read, update, delete] + uri: /v1.0/process-models/manage-procurement:* + manage-procurement-admin-models-slash: + groups: ["Project Lead"] + users: [] + allowed_permissions: [create, read, update, delete] + uri: /v1.0/process-models/manage-procurement/* + manage-procurement-admin-instances: + groups: ["Project Lead"] + users: [] + allowed_permissions: [create, read, update, delete] + uri: /v1.0/process-instances/manage-procurement:* + manage-procurement-admin-instances-slash: + groups: ["Project Lead"] + users: [] + allowed_permissions: [create, read, update, delete] + uri: /v1.0/process-instances/manage-procurement/* + + finance-admin: groups: ["Finance Team"] users: [] allowed_permissions: [create, read, update, delete] - uri: /v1.0/process-groups/finance/* + uri: /v1.0/process-groups/manage-procurement:procurement:* - finance-admin-model: - groups: ["Finance Team"] + manage-revenue-streams-instantiate: + groups: ["core-contributor", "demo"] users: [] - allowed_permissions: [create, read, update, delete] - uri: /v1.0/process-models/finance/* + allowed_permissions: [create] + uri: /v1.0/process-models/manage-revenue-streams:product-revenue-streams:customer-contracts-trade-terms/* + manage-revenue-streams-instances: + groups: ["core-contributor", "demo"] + users: [] + allowed_permissions: [create, read] + uri: /v1.0/process-instances/manage-revenue-streams:product-revenue-streams:customer-contracts-trade-terms/* - read-all: - groups: [admin, "Project Lead"] + manage-procurement-invoice-instantiate: + groups: ["core-contributor", "demo"] users: [] - allowed_permissions: [read] - uri: /* + allowed_permissions: [create] + uri: /v1.0/process-models/manage-procurement:procurement:core-contributor-invoice-management:* + manage-procurement-invoice-instances: + groups: ["core-contributor", "demo"] + users: [] + allowed_permissions: [create, read] + uri: /v1.0/process-instances/manage-procurement:procurement:core-contributor-invoice-management:* - invoice-approval-tasks-read: - groups: ["Finance Team"] + manage-procurement-instantiate: + groups: ["core-contributor", "demo"] users: [] - allowed_permissions: [read] - uri: /v1.0/process-instances/category_number_one:lanes/* + allowed_permissions: [create] + uri: /v1.0/process-models/manage-procurement:vendor-lifecycle-management:* + manage-procurement-instances: + groups: ["core-contributor", "demo"] + users: [] + allowed_permissions: [create, read] + uri: /v1.0/process-instances/manage-procurement:vendor-lifecycle-management:* + + core1-admin-models-instantiate: + groups: ["core-contributor", "Finance Team"] + users: [] + allowed_permissions: [create] + uri: /v1.0/process-models/misc:category_number_one:process-model-with-form/process-instances + core1-admin-instances: + groups: ["core-contributor", "Finance Team"] + users: [] + allowed_permissions: [create, read] + uri: /v1.0/process-instances/misc:category_number_one:process-model-with-form:* + core1-admin-instances-slash: + groups: ["core-contributor", "Finance Team"] + users: [] + allowed_permissions: [create, read] + uri: /v1.0/process-instances/misc:category_number_one:process-model-with-form/* diff --git a/src/spiffworkflow_backend/config/permissions/example.yml b/src/spiffworkflow_backend/config/permissions/example.yml new file mode 100644 index 00000000..79bfed81 --- /dev/null +++ b/src/spiffworkflow_backend/config/permissions/example.yml @@ -0,0 +1,88 @@ +default_group: everybody + +users: + admin: + email: admin@spiffworkflow.org + password: admin + preferred_username: Admin + nelson: + email: nelson@spiffworkflow.org + password: nelson + preferred_username: Nelson + malala: + email: malala@spiffworkflow.org + password: malala + preferred_username: Malala + +groups: + admin: + users: + [ + admin, + ] + Education: + users: + [ + malala + ] + President: + users: + [ + nelson + ] + +permissions: + # Admins have access to everything. + admin: + groups: [admin] + users: [] + allowed_permissions: [create, read, update, delete] + uri: /* + + # Everybody can participate in tasks assigned to them. + tasks-crud: + groups: [everybody] + users: [] + allowed_permissions: [create, read, update, delete] + uri: /v1.0/tasks/* + + # Everyone can see everything (all groups, and processes are visible) + read-all-process-groups: + groups: [ everybody ] + users: [ ] + allowed_permissions: [ read ] + uri: /v1.0/process-groups/* + read-all-process-models: + groups: [ everybody ] + users: [ ] + allowed_permissions: [ read ] + uri: /v1.0/process-models/* + read-all-process-instance: + groups: [ everybody ] + users: [ ] + allowed_permissions: [ read ] + uri: /v1.0/process-instances/* + read-process-instance-reports: + groups: [ everybody ] + users: [ ] + allowed_permissions: [ read ] + uri: /v1.0/process-instances/reports/* + processes-read: + groups: [ everybody ] + users: [ ] + allowed_permissions: [ read ] + uri: /v1.0/processes + + # Members of the Education group can change they processes work. + education-admin: + groups: ["Education", "President"] + users: [] + allowed_permissions: [create, read, update, delete] + uri: /v1.0/process-groups/education:* + + # Anyone can start an education process. + education-everybody: + groups: [everybody] + users: [] + allowed_permissions: [create, read] + uri: /v1.0/process-instances/misc:category_number_one:process-model-with-form/* diff --git a/src/spiffworkflow_backend/config/permissions/terraform_deployed_environment.yml b/src/spiffworkflow_backend/config/permissions/terraform_deployed_environment.yml index 96a5c1c5..2e41e3b0 100644 --- a/src/spiffworkflow_backend/config/permissions/terraform_deployed_environment.yml +++ b/src/spiffworkflow_backend/config/permissions/terraform_deployed_environment.yml @@ -4,20 +4,18 @@ groups: admin: users: [ + admin, jakub, kb, alex, dan, mike, jason, - amir, + j, jarrad, elizabeth, jon, natalia, - harmeet, - sasha, - manuchehr, ] Finance Team: @@ -28,60 +26,157 @@ groups: dan, mike, jason, + j, amir, jarrad, elizabeth, jon, natalia, sasha, + fin, + fin1, ] - Project Lead: + demo: users: [ - jakub, - alex, - dan, - mike, - jason, - jarrad, - elizabeth, - jon, - natalia, + core, + fin, + fin1, + harmeet, + sasha, manuchehr, + lead, + lead1 ] - hr: - users: [manuchehr] + core-contributor: + users: + [ + core, + harmeet, + ] permissions: + admin: + groups: [admin] + users: [] + allowed_permissions: [create, read, update, delete] + uri: /* + tasks-crud: groups: [everybody] users: [] allowed_permissions: [create, read, update, delete] uri: /v1.0/tasks/* - admin: - groups: [admin] + service-tasks: + groups: [everybody] users: [] - allowed_permissions: [create, read, update, delete, list, instantiate] - uri: /* + allowed_permissions: [read] + uri: /v1.0/service-tasks - # TODO: all uris should really have the same structure - finance-admin-group: - groups: ["Finance Team"] + + # read all for everybody + read-all-process-groups: + groups: [everybody] + users: [] + allowed_permissions: [read] + uri: /v1.0/process-groups/* + read-all-process-models: + groups: [everybody] + users: [] + allowed_permissions: [read] + uri: /v1.0/process-models/* + read-all-process-instance: + groups: [everybody] + users: [] + allowed_permissions: [read] + uri: /v1.0/process-instances/* + read-process-instance-reports: + groups: [everybody] + users: [] + allowed_permissions: [read] + uri: /v1.0/process-instances/reports/* + processes-read: + groups: [everybody] + users: [] + allowed_permissions: [read] + uri: /v1.0/processes + + task-data-read: + groups: [demo] + users: [] + allowed_permissions: [read] + uri: /v1.0/task-data/* + + + manage-procurement-admin: + groups: ["Project Lead"] users: [] allowed_permissions: [create, read, update, delete] - uri: /v1.0/process-groups/finance/* + uri: /v1.0/process-groups/manage-procurement:* + manage-procurement-admin-slash: + groups: ["Project Lead"] + users: [] + allowed_permissions: [create, read, update, delete] + uri: /v1.0/process-groups/manage-procurement/* + manage-procurement-admin-models: + groups: ["Project Lead"] + users: [] + allowed_permissions: [create, read, update, delete] + uri: /v1.0/process-models/manage-procurement:* + manage-procurement-admin-models-slash: + groups: ["Project Lead"] + users: [] + allowed_permissions: [create, read, update, delete] + uri: /v1.0/process-models/manage-procurement/* + manage-procurement-admin-instances: + groups: ["Project Lead"] + users: [] + allowed_permissions: [create, read, update, delete] + uri: /v1.0/process-instances/manage-procurement:* + manage-procurement-admin-instances-slash: + groups: ["Project Lead"] + users: [] + allowed_permissions: [create, read, update, delete] + uri: /v1.0/process-instances/manage-procurement/* finance-admin: groups: ["Finance Team"] users: [] allowed_permissions: [create, read, update, delete] - uri: /v1.0/process-groups/finance/* + uri: /v1.0/process-groups/manage-procurement:procurement:* - read-all: - groups: ["Finance Team", "Project Lead", hr, admin] + manage-revenue-streams-instantiate: + groups: ["core-contributor", "demo"] users: [] - allowed_permissions: [read] - uri: /* + allowed_permissions: [create] + uri: /v1.0/process-models/manage-revenue-streams:product-revenue-streams:customer-contracts-trade-terms/* + manage-revenue-streams-instances: + groups: ["core-contributor", "demo"] + users: [] + allowed_permissions: [create, read] + uri: /v1.0/process-instances/manage-revenue-streams:product-revenue-streams:customer-contracts-trade-terms/* + + manage-procurement-invoice-instantiate: + groups: ["core-contributor", "demo"] + users: [] + allowed_permissions: [create] + uri: /v1.0/process-models/manage-procurement:procurement:core-contributor-invoice-management:* + manage-procurement-invoice-instances: + groups: ["core-contributor", "demo"] + users: [] + allowed_permissions: [create, read] + uri: /v1.0/process-instances/manage-procurement:procurement:core-contributor-invoice-management:* + + manage-procurement-instantiate: + groups: ["core-contributor", "demo"] + users: [] + allowed_permissions: [create] + uri: /v1.0/process-models/manage-procurement:vendor-lifecycle-management:* + manage-procurement-instances: + groups: ["core-contributor", "demo"] + users: [] + allowed_permissions: [create, read] + uri: /v1.0/process-instances/manage-procurement:vendor-lifecycle-management:* diff --git a/src/spiffworkflow_backend/config/staging.py b/src/spiffworkflow_backend/config/staging.py index 53c8af61..5f0fec4c 100644 --- a/src/spiffworkflow_backend/config/staging.py +++ b/src/spiffworkflow_backend/config/staging.py @@ -1,9 +1,6 @@ """Staging.""" from os import environ -GIT_COMMIT_ON_SAVE = True -GIT_COMMIT_USERNAME = "staging" -GIT_COMMIT_EMAIL = "staging@example.com" -SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME = environ.get( - "SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME", default="staging.yml" -) +GIT_BRANCH = environ.get("GIT_BRANCH_TO_PUBLISH_TO", default="staging") +GIT_BRANCH_TO_PUBLISH_TO = environ.get("GIT_BRANCH_TO_PUBLISH_TO", default="main") +GIT_COMMIT_ON_SAVE = False diff --git a/src/spiffworkflow_backend/config/terraform_deployed_environment.py b/src/spiffworkflow_backend/config/terraform_deployed_environment.py new file mode 100644 index 00000000..efd45183 --- /dev/null +++ b/src/spiffworkflow_backend/config/terraform_deployed_environment.py @@ -0,0 +1,29 @@ +"""Terraform-deployed environment.""" +from os import environ + +# default.py already ensured that this key existed as was not None +environment_identifier_for_this_config_file_only = environ["SPIFFWORKFLOW_BACKEND_ENV"] + +GIT_COMMIT_ON_SAVE = True +GIT_USERNAME = "sartography-automated-committer" +GIT_USER_EMAIL = f"{GIT_USERNAME}@users.noreply.github.com" +SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME = environ.get( + "SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME", + default="terraform_deployed_environment.yml", +) + +RUN_BACKGROUND_SCHEDULER = ( + environ.get("RUN_BACKGROUND_SCHEDULER", default="false") == "true" +) + +OPEN_ID_SERVER_URL = f"https://keycloak.{environment_identifier_for_this_config_file_only}.spiffworkflow.org/realms/spiffworkflow" +SPIFFWORKFLOW_FRONTEND_URL = ( + f"https://{environment_identifier_for_this_config_file_only}.spiffworkflow.org" +) +SPIFFWORKFLOW_BACKEND_URL = ( + f"https://api.{environment_identifier_for_this_config_file_only}.spiffworkflow.org" +) +CONNECTOR_PROXY_URL = f"https://connector-proxy.{environment_identifier_for_this_config_file_only}.spiffworkflow.org" +GIT_CLONE_URL_FOR_PUBLISHING = environ.get( + "GIT_CLONE_URL", default="https://github.com/sartography/sample-process-models.git" +) diff --git a/src/spiffworkflow_backend/config/terraform_deployed_environment.rb b/src/spiffworkflow_backend/config/terraform_deployed_environment.rb deleted file mode 100644 index f1be3410..00000000 --- a/src/spiffworkflow_backend/config/terraform_deployed_environment.rb +++ /dev/null @@ -1,16 +0,0 @@ -"""Terraform-deployed environment.""" -from os import environ - -# default.py already ensured that this key existed as was not None -environment_identifier_for_this_config_file_only = environ["SPIFFWORKFLOW_BACKEND_ENV"] - -GIT_COMMIT_ON_SAVE = True -GIT_COMMIT_USERNAME = environment_identifier_for_this_config_file_only -GIT_COMMIT_EMAIL = f"{environment_identifier_for_this_config_file_only}@example.com" -SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME = environ.get( - "SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME", default="terraform_deployed_environment.yml" -) - -RUN_BACKGROUND_SCHEDULER = ( - environ.get("RUN_BACKGROUND_SCHEDULER", default="false") == "true" -) diff --git a/src/spiffworkflow_backend/load_database_models.py b/src/spiffworkflow_backend/load_database_models.py index 97c99000..71adb57c 100644 --- a/src/spiffworkflow_backend/load_database_models.py +++ b/src/spiffworkflow_backend/load_database_models.py @@ -51,5 +51,8 @@ from spiffworkflow_backend.models.spiff_step_details import ( ) # noqa: F401 from spiffworkflow_backend.models.user import UserModel # noqa: F401 from spiffworkflow_backend.models.group import GroupModel # noqa: F401 +from spiffworkflow_backend.models.process_instance_metadata import ( + ProcessInstanceMetadataModel, +) # noqa: F401 add_listeners() diff --git a/src/spiffworkflow_backend/models/message_triggerable_process_model.py b/src/spiffworkflow_backend/models/message_triggerable_process_model.py index 9e4c3928..cc883465 100644 --- a/src/spiffworkflow_backend/models/message_triggerable_process_model.py +++ b/src/spiffworkflow_backend/models/message_triggerable_process_model.py @@ -16,8 +16,6 @@ class MessageTriggerableProcessModel(SpiffworkflowBaseDBModel): ForeignKey(MessageModel.id), nullable=False, unique=True ) process_model_identifier: str = db.Column(db.String(50), nullable=False, index=True) - # fixme: Maybe we don't need this anymore? - process_group_identifier: str = db.Column(db.String(50), nullable=False, index=True) updated_at_in_seconds: int = db.Column(db.Integer) created_at_in_seconds: int = db.Column(db.Integer) diff --git a/src/spiffworkflow_backend/models/process_group.py b/src/spiffworkflow_backend/models/process_group.py index 236641e3..1439b045 100644 --- a/src/spiffworkflow_backend/models/process_group.py +++ b/src/spiffworkflow_backend/models/process_group.py @@ -2,6 +2,7 @@ from __future__ import annotations import dataclasses +import os from dataclasses import dataclass from dataclasses import field from typing import Any @@ -28,10 +29,11 @@ class ProcessGroup: default_factory=list[ProcessModelInfo] ) process_groups: list[ProcessGroup] = field(default_factory=list["ProcessGroup"]) + parent_groups: list[dict] | None = None def __post_init__(self) -> None: """__post_init__.""" - self.sort_index = self.id + self.sort_index = self.display_name def __eq__(self, other: Any) -> bool: """__eq__.""" @@ -47,6 +49,11 @@ class ProcessGroup: original_dict = dataclasses.asdict(self) return {x: original_dict[x] for x in original_dict if x not in ["sort_index"]} + # for use with os.path.join, so it can work on windows + def id_for_file_path(self) -> str: + """Id_for_file_path.""" + return self.id.replace("/", os.sep) + class ProcessGroupSchema(Schema): """ProcessGroupSchema.""" diff --git a/src/spiffworkflow_backend/models/process_instance.py b/src/spiffworkflow_backend/models/process_instance.py index 2e94e994..c89f457b 100644 --- a/src/spiffworkflow_backend/models/process_instance.py +++ b/src/spiffworkflow_backend/models/process_instance.py @@ -1,7 +1,6 @@ """Process_instance.""" from __future__ import annotations -from dataclasses import dataclass from typing import Any from typing import cast @@ -18,12 +17,15 @@ from sqlalchemy.orm import relationship from sqlalchemy.orm import validates from spiffworkflow_backend.helpers.spiff_enum import SpiffEnum -from spiffworkflow_backend.models.process_model import ProcessModelInfo from spiffworkflow_backend.models.task import Task from spiffworkflow_backend.models.task import TaskSchema from spiffworkflow_backend.models.user import UserModel +class ProcessInstanceNotFoundError(Exception): + """ProcessInstanceNotFoundError.""" + + class NavigationItemSchema(Schema): """NavigationItemSchema.""" @@ -74,7 +76,9 @@ class ProcessInstanceModel(SpiffworkflowBaseDBModel): process_model_identifier: str = db.Column( db.String(255), nullable=False, index=True ) - process_group_identifier: str = db.Column(db.String(50), nullable=False, index=True) + process_model_display_name: str = db.Column( + db.String(255), nullable=False, index=True + ) process_initiator_id: int = db.Column(ForeignKey(UserModel.id), nullable=False) process_initiator = relationship("UserModel") @@ -89,7 +93,7 @@ class ProcessInstanceModel(SpiffworkflowBaseDBModel): created_at_in_seconds: int = db.Column(db.Integer) status: str = db.Column(db.String(50)) - bpmn_xml_file_contents: bytes | None = None + bpmn_xml_file_contents: str | None = None bpmn_version_control_type: str = db.Column(db.String(50)) bpmn_version_control_identifier: str = db.Column(db.String(255)) spiff_step: int = db.Column(db.Integer) @@ -97,21 +101,19 @@ class ProcessInstanceModel(SpiffworkflowBaseDBModel): @property def serialized(self) -> dict[str, Any]: """Return object data in serializeable format.""" - local_bpmn_xml_file_contents = "" - if self.bpmn_xml_file_contents: - local_bpmn_xml_file_contents = self.bpmn_xml_file_contents.decode("utf-8") return { "id": self.id, "process_model_identifier": self.process_model_identifier, - "process_group_identifier": self.process_group_identifier, + "process_model_display_name": self.process_model_display_name, "status": self.status, "start_in_seconds": self.start_in_seconds, "end_in_seconds": self.end_in_seconds, "process_initiator_id": self.process_initiator_id, - "bpmn_xml_file_contents": local_bpmn_xml_file_contents, + "bpmn_xml_file_contents": self.bpmn_xml_file_contents, "bpmn_version_control_identifier": self.bpmn_version_control_identifier, "bpmn_version_control_type": self.bpmn_version_control_type, "spiff_step": self.spiff_step, + "username": self.process_initiator.username, } @property @@ -140,7 +142,7 @@ class ProcessInstanceModelSchema(Schema): fields = [ "id", "process_model_identifier", - "process_group_identifier", + "process_model_display_name", "process_initiator_id", "start_in_seconds", "end_in_seconds", @@ -166,23 +168,18 @@ class ProcessInstanceApi: status: ProcessInstanceStatus, next_task: Task | None, process_model_identifier: str, - process_group_identifier: str, + process_model_display_name: str, completed_tasks: int, updated_at_in_seconds: int, - is_review: bool, - title: str, ) -> None: """__init__.""" self.id = id self.status = status self.next_task = next_task # The next task that requires user input. - # self.navigation = navigation fixme: would be a hotness. self.process_model_identifier = process_model_identifier - self.process_group_identifier = process_group_identifier + self.process_model_display_name = process_model_display_name self.completed_tasks = completed_tasks self.updated_at_in_seconds = updated_at_in_seconds - self.title = title - self.is_review = is_review class ProcessInstanceApiSchema(Schema): @@ -196,24 +193,15 @@ class ProcessInstanceApiSchema(Schema): "id", "status", "next_task", - "navigation", "process_model_identifier", - "process_group_identifier", + "process_model_display_name", "completed_tasks", "updated_at_in_seconds", - "is_review", - "title", - "study_id", - "state", ] unknown = INCLUDE status = EnumField(ProcessInstanceStatus) next_task = marshmallow.fields.Nested(TaskSchema, dump_only=True, required=False) - navigation = marshmallow.fields.List( - marshmallow.fields.Nested(NavigationItemSchema, dump_only=True) - ) - state = marshmallow.fields.String(allow_none=True) @marshmallow.post_load def make_process_instance( @@ -224,73 +212,11 @@ class ProcessInstanceApiSchema(Schema): "id", "status", "next_task", - "navigation", "process_model_identifier", - "process_group_identifier", + "process_model_display_name", "completed_tasks", "updated_at_in_seconds", - "is_review", - "title", - "study_id", - "state", ] filtered_fields = {key: data[key] for key in keys} filtered_fields["next_task"] = TaskSchema().make_task(data["next_task"]) return ProcessInstanceApi(**filtered_fields) - - -@dataclass -class ProcessInstanceMetadata: - """ProcessInstanceMetadata.""" - - id: int - display_name: str | None = None - description: str | None = None - spec_version: str | None = None - state: str | None = None - status: str | None = None - completed_tasks: int | None = None - is_review: bool | None = None - state_message: str | None = None - process_model_identifier: str | None = None - process_group_id: str | None = None - - @classmethod - def from_process_instance( - cls, process_instance: ProcessInstanceModel, process_model: ProcessModelInfo - ) -> ProcessInstanceMetadata: - """From_process_instance.""" - instance = cls( - id=process_instance.id, - display_name=process_model.display_name, - description=process_model.description, - process_group_id=process_model.process_group, - state_message=process_instance.state_message, - status=process_instance.status, - completed_tasks=process_instance.completed_tasks, - is_review=process_model.is_review, - process_model_identifier=process_instance.process_model_identifier, - ) - return instance - - -class ProcessInstanceMetadataSchema(Schema): - """ProcessInstanceMetadataSchema.""" - - status = EnumField(ProcessInstanceStatus) - - class Meta: - """Meta.""" - - model = ProcessInstanceMetadata - additional = [ - "id", - "display_name", - "description", - "state", - "completed_tasks", - "process_group_id", - "is_review", - "state_message", - ] - unknown = INCLUDE diff --git a/src/spiffworkflow_backend/models/process_instance_metadata.py b/src/spiffworkflow_backend/models/process_instance_metadata.py new file mode 100644 index 00000000..c9003594 --- /dev/null +++ b/src/spiffworkflow_backend/models/process_instance_metadata.py @@ -0,0 +1,30 @@ +"""Spiff_step_details.""" +from dataclasses import dataclass + +from flask_bpmn.models.db import db +from flask_bpmn.models.db import SpiffworkflowBaseDBModel +from sqlalchemy import ForeignKey + +from spiffworkflow_backend.models.process_instance import ProcessInstanceModel + + +@dataclass +class ProcessInstanceMetadataModel(SpiffworkflowBaseDBModel): + """ProcessInstanceMetadataModel.""" + + __tablename__ = "process_instance_metadata" + __table_args__ = ( + db.UniqueConstraint( + "process_instance_id", "key", name="process_instance_metadata_unique" + ), + ) + + id: int = db.Column(db.Integer, primary_key=True) + process_instance_id: int = db.Column( + ForeignKey(ProcessInstanceModel.id), nullable=False # type: ignore + ) + key: str = db.Column(db.String(255), nullable=False, index=True) + value: str = db.Column(db.String(255), nullable=False) + + updated_at_in_seconds: int = db.Column(db.Integer, nullable=False) + created_at_in_seconds: int = db.Column(db.Integer, nullable=False) diff --git a/src/spiffworkflow_backend/models/process_instance_report.py b/src/spiffworkflow_backend/models/process_instance_report.py index ea85a23e..1f22a383 100644 --- a/src/spiffworkflow_backend/models/process_instance_report.py +++ b/src/spiffworkflow_backend/models/process_instance_report.py @@ -26,6 +26,10 @@ from spiffworkflow_backend.services.process_instance_processor import ( ReportMetadata = dict[str, Any] +class ProcessInstanceReportAlreadyExistsError(Exception): + """ProcessInstanceReportAlreadyExistsError.""" + + class ProcessInstanceReportResult(TypedDict): """ProcessInstanceReportResult.""" @@ -63,7 +67,7 @@ class ProcessInstanceReportModel(SpiffworkflowBaseDBModel): ), ) - id = db.Column(db.Integer, primary_key=True) + id: int = db.Column(db.Integer, primary_key=True) identifier: str = db.Column(db.String(50), nullable=False, index=True) report_metadata: dict = deferred(db.Column(db.JSON)) # type: ignore created_by_id = db.Column(ForeignKey(UserModel.id), nullable=False, index=True) @@ -72,41 +76,15 @@ class ProcessInstanceReportModel(SpiffworkflowBaseDBModel): updated_at_in_seconds = db.Column(db.Integer) @classmethod - def default_report(cls, user: UserModel) -> ProcessInstanceReportModel: - """Default_report.""" - identifier = "default" - process_instance_report = ProcessInstanceReportModel.query.filter_by( - identifier=identifier, created_by_id=user.id - ).first() - - # TODO replace with system report that is loaded on launch (or similar) - if process_instance_report is None: - report_metadata = { - "columns": [ - {"Header": "id", "accessor": "id"}, - { - "Header": "process_model_identifier", - "accessor": "process_model_identifier", - }, - {"Header": "start_in_seconds", "accessor": "start_in_seconds"}, - {"Header": "end_in_seconds", "accessor": "end_in_seconds"}, - {"Header": "status", "accessor": "status"}, - ], - } - - process_instance_report = cls( - identifier=identifier, - created_by_id=user.id, - report_metadata=report_metadata, - ) - - return process_instance_report # type: ignore + def default_order_by(cls) -> list[str]: + """Default_order_by.""" + return ["-start_in_seconds", "-id"] @classmethod def add_fixtures(cls) -> None: """Add_fixtures.""" try: - # process_model = ProcessModelService().get_process_model( + # process_model = ProcessModelService.get_process_model( # process_model_id="sartography-admin/ticket" # ) user = UserModel.query.first() @@ -151,21 +129,27 @@ class ProcessInstanceReportModel(SpiffworkflowBaseDBModel): identifier: str, user: UserModel, report_metadata: ReportMetadata, - ) -> None: + ) -> ProcessInstanceReportModel: """Make_fixture_report.""" process_instance_report = ProcessInstanceReportModel.query.filter_by( identifier=identifier, created_by_id=user.id, ).first() - if process_instance_report is None: - process_instance_report = cls( - identifier=identifier, - created_by_id=user.id, - report_metadata=report_metadata, + if process_instance_report is not None: + raise ProcessInstanceReportAlreadyExistsError( + f"Process instance report with identifier already exists: {identifier}" ) - db.session.add(process_instance_report) - db.session.commit() + + process_instance_report = cls( + identifier=identifier, + created_by_id=user.id, + report_metadata=report_metadata, + ) + db.session.add(process_instance_report) + db.session.commit() + + return process_instance_report # type: ignore @classmethod def ticket_for_month_report(cls) -> dict: @@ -235,18 +219,8 @@ class ProcessInstanceReportModel(SpiffworkflowBaseDBModel): user: UserModel, ) -> ProcessInstanceReportModel: """Create_with_attributes.""" - # <<<<<<< HEAD - # process_model = ProcessModelService().get_process_model( - # process_model_id=f"{process_model_identifier}" - # ) - # process_instance_report = cls( - # identifier=identifier, - # process_group_identifier="process_model.process_group_id", - # process_model_identifier=process_model.id, - # ======= process_instance_report = cls( identifier=identifier, - # >>>>>>> main created_by_id=user.id, report_metadata=report_metadata, ) diff --git a/src/spiffworkflow_backend/models/process_model.py b/src/spiffworkflow_backend/models/process_model.py index 3ab55d07..e8d5eed1 100644 --- a/src/spiffworkflow_backend/models/process_model.py +++ b/src/spiffworkflow_backend/models/process_model.py @@ -34,10 +34,11 @@ class ProcessModelInfo: primary_file_name: str | None = None primary_process_id: str | None = None display_order: int | None = 0 - is_review: bool = False files: list[File] | None = field(default_factory=list[File]) fault_or_suspend_on_exception: str = NotificationType.fault.value exception_notification_addresses: list[str] = field(default_factory=list) + parent_groups: list[dict] | None = None + metadata_extraction_paths: list[dict[str, str]] | None = None def __post_init__(self) -> None: """__post_init__.""" @@ -71,12 +72,18 @@ class ProcessModelInfoSchema(Schema): display_order = marshmallow.fields.Integer(allow_none=True) primary_file_name = marshmallow.fields.String(allow_none=True) primary_process_id = marshmallow.fields.String(allow_none=True) - is_review = marshmallow.fields.Boolean(allow_none=True) files = marshmallow.fields.List(marshmallow.fields.Nested("FileSchema")) fault_or_suspend_on_exception = marshmallow.fields.String() exception_notification_addresses = marshmallow.fields.List( marshmallow.fields.String ) + metadata_extraction_paths = marshmallow.fields.List( + marshmallow.fields.Dict( + keys=marshmallow.fields.Str(required=False), + values=marshmallow.fields.Str(required=False), + required=False, + ) + ) @post_load def make_spec( diff --git a/src/spiffworkflow_backend/models/spiff_step_details.py b/src/spiffworkflow_backend/models/spiff_step_details.py index e00e7cac..91d70116 100644 --- a/src/spiffworkflow_backend/models/spiff_step_details.py +++ b/src/spiffworkflow_backend/models/spiff_step_details.py @@ -8,6 +8,7 @@ from sqlalchemy import ForeignKey from sqlalchemy.orm import deferred from spiffworkflow_backend.models.group import GroupModel +from spiffworkflow_backend.models.process_instance import ProcessInstanceModel @dataclass @@ -16,7 +17,9 @@ class SpiffStepDetailsModel(SpiffworkflowBaseDBModel): __tablename__ = "spiff_step_details" id: int = db.Column(db.Integer, primary_key=True) - process_instance_id: int = db.Column(db.Integer, nullable=False) + process_instance_id: int = db.Column( + ForeignKey(ProcessInstanceModel.id), nullable=False # type: ignore + ) spiff_step: int = db.Column(db.Integer, nullable=False) task_json: str = deferred(db.Column(db.JSON, nullable=False)) # type: ignore timestamp: float = db.Column(db.DECIMAL(17, 6), nullable=False) diff --git a/src/spiffworkflow_backend/models/user.py b/src/spiffworkflow_backend/models/user.py index c33a72e7..b8c83d0f 100644 --- a/src/spiffworkflow_backend/models/user.py +++ b/src/spiffworkflow_backend/models/user.py @@ -30,7 +30,8 @@ class UserModel(SpiffworkflowBaseDBModel): __table_args__ = (db.UniqueConstraint("service", "service_id", name="service_key"),) id = db.Column(db.Integer, primary_key=True) - username = db.Column(db.String(255), nullable=False, unique=True) + # server and service id must be unique, not username. + username = db.Column(db.String(255), nullable=False, unique=False) uid = db.Column(db.String(50), unique=True) service = db.Column(db.String(50), nullable=False, unique=False) service_id = db.Column(db.String(255), nullable=False, unique=False) @@ -83,10 +84,6 @@ class UserModel(SpiffworkflowBaseDBModel): algorithm="HS256", ) - def is_admin(self) -> bool: - """Is_admin.""" - return True - # @classmethod # def from_open_id_user_info(cls, user_info: dict) -> Any: # """From_open_id_user_info.""" diff --git a/src/spiffworkflow_backend/routes/admin_blueprint/admin_blueprint.py b/src/spiffworkflow_backend/routes/admin_blueprint/admin_blueprint.py index 2e480f2a..f1223ae0 100644 --- a/src/spiffworkflow_backend/routes/admin_blueprint/admin_blueprint.py +++ b/src/spiffworkflow_backend/routes/admin_blueprint/admin_blueprint.py @@ -27,28 +27,28 @@ ALLOWED_BPMN_EXTENSIONS = {"bpmn", "dmn"} @admin_blueprint.route("/process-groups", methods=["GET"]) -def process_groups_list() -> str: - """Process_groups_list.""" - process_groups = ProcessModelService().get_process_groups() - return render_template("process_groups_list.html", process_groups=process_groups) +def process_group_list() -> str: + """Process_group_list.""" + process_groups = ProcessModelService.get_process_groups() + return render_template("process_group_list.html", process_groups=process_groups) @admin_blueprint.route("/process-groups/", methods=["GET"]) def process_group_show(process_group_id: str) -> str: """Show_process_group.""" - process_group = ProcessModelService().get_process_group(process_group_id) + process_group = ProcessModelService.get_process_group(process_group_id) return render_template("process_group_show.html", process_group=process_group) @admin_blueprint.route("/process-models/", methods=["GET"]) def process_model_show(process_model_id: str) -> Union[str, Response]: """Show_process_model.""" - process_model = ProcessModelService().get_process_model(process_model_id) + process_model = ProcessModelService.get_process_model(process_model_id) files = SpecFileService.get_files(process_model, extension_filter="bpmn") current_file_name = process_model.primary_file_name if current_file_name is None: flash("No primary_file_name", "error") - return redirect(url_for("admin.process_groups_list")) + return redirect(url_for("admin.process_group_list")) bpmn_xml = SpecFileService.get_data(process_model, current_file_name) return render_template( "process_model_show.html", @@ -64,7 +64,7 @@ def process_model_show(process_model_id: str) -> Union[str, Response]: ) def process_model_show_file(process_model_id: str, file_name: str) -> str: """Process_model_show_file.""" - process_model = ProcessModelService().get_process_model(process_model_id) + process_model = ProcessModelService.get_process_model(process_model_id) bpmn_xml = SpecFileService.get_data(process_model, file_name) files = SpecFileService.get_files(process_model, extension_filter="bpmn") return render_template( @@ -81,8 +81,7 @@ def process_model_show_file(process_model_id: str, file_name: str) -> str: ) def process_model_upload_file(process_model_id: str) -> Response: """Process_model_upload_file.""" - process_model_service = ProcessModelService() - process_model = process_model_service.get_process_model(process_model_id) + process_model = ProcessModelService.get_process_model(process_model_id) if "file" not in request.files: flash("No file part", "error") @@ -97,7 +96,7 @@ def process_model_upload_file(process_model_id: str) -> Response: SpecFileService.add_file( process_model, request_file.filename, request_file.stream.read() ) - process_model_service.save_process_model(process_model) + ProcessModelService.save_process_model(process_model) return redirect( url_for("admin.process_model_show", process_model_id=process_model.id) @@ -109,7 +108,7 @@ def process_model_upload_file(process_model_id: str) -> Response: ) def process_model_edit(process_model_id: str, file_name: str) -> str: """Edit_bpmn.""" - process_model = ProcessModelService().get_process_model(process_model_id) + process_model = ProcessModelService.get_process_model(process_model_id) bpmn_xml = SpecFileService.get_data(process_model, file_name) return render_template( @@ -125,11 +124,11 @@ def process_model_edit(process_model_id: str, file_name: str) -> str: ) def process_model_save(process_model_id: str, file_name: str) -> Union[str, Response]: """Process_model_save.""" - process_model = ProcessModelService().get_process_model(process_model_id) + process_model = ProcessModelService.get_process_model(process_model_id) SpecFileService.update_file(process_model, file_name, request.get_data()) if process_model.primary_file_name is None: flash("No primary_file_name", "error") - return redirect(url_for("admin.process_groups_list")) + return redirect(url_for("admin.process_group_list")) bpmn_xml = SpecFileService.get_data(process_model, process_model.primary_file_name) return render_template( "process_model_edit.html", @@ -143,19 +142,21 @@ def process_model_save(process_model_id: str, file_name: str) -> Union[str, Resp def process_model_run(process_model_id: str) -> Union[str, Response]: """Process_model_run.""" user = UserService.create_user("internal", "Mr. Test", username="Mr. Test") - process_instance = ProcessInstanceService.create_process_instance( - process_model_id, user + process_instance = ( + ProcessInstanceService.create_process_instance_from_process_model_identifier( + process_model_id, user + ) ) processor = ProcessInstanceProcessor(process_instance) processor.do_engine_steps() result = processor.get_data() - process_model = ProcessModelService().get_process_model(process_model_id) + process_model = ProcessModelService.get_process_model(process_model_id) files = SpecFileService.get_files(process_model, extension_filter="bpmn") current_file_name = process_model.primary_file_name if current_file_name is None: flash("No primary_file_name", "error") - return redirect(url_for("admin.process_groups_list")) + return redirect(url_for("admin.process_group_list")) bpmn_xml = SpecFileService.get_data(process_model, current_file_name) return render_template( diff --git a/src/spiffworkflow_backend/routes/admin_blueprint/templates/process_groups_list.html b/src/spiffworkflow_backend/routes/admin_blueprint/templates/process_group_list.html similarity index 100% rename from src/spiffworkflow_backend/routes/admin_blueprint/templates/process_groups_list.html rename to src/spiffworkflow_backend/routes/admin_blueprint/templates/process_group_list.html diff --git a/src/spiffworkflow_backend/routes/admin_blueprint/templates/process_group_show.html b/src/spiffworkflow_backend/routes/admin_blueprint/templates/process_group_show.html index a5fbab88..2a41abe9 100644 --- a/src/spiffworkflow_backend/routes/admin_blueprint/templates/process_group_show.html +++ b/src/spiffworkflow_backend/routes/admin_blueprint/templates/process_group_show.html @@ -3,7 +3,7 @@ {% block content %} diff --git a/src/spiffworkflow_backend/routes/openid_blueprint/__init__.py b/src/spiffworkflow_backend/routes/openid_blueprint/__init__.py new file mode 100644 index 00000000..f520b09d --- /dev/null +++ b/src/spiffworkflow_backend/routes/openid_blueprint/__init__.py @@ -0,0 +1 @@ +"""__init__.""" diff --git a/src/spiffworkflow_backend/routes/openid_blueprint/openid_blueprint.py b/src/spiffworkflow_backend/routes/openid_blueprint/openid_blueprint.py new file mode 100644 index 00000000..f812ab03 --- /dev/null +++ b/src/spiffworkflow_backend/routes/openid_blueprint/openid_blueprint.py @@ -0,0 +1,153 @@ +"""OpenID Implementation for demos and local development. + +A very insecure and partial OpenID implementation for use in demos and testing. +Provides the bare minimum endpoints required by SpiffWorkflow to +handle openid authentication -- definitely not a production ready system. +This is just here to make local development, testing, and demonstration easier. +""" +import base64 +import time +from typing import Any +from urllib.parse import urlencode + +import jwt +import yaml +from flask import Blueprint +from flask import current_app +from flask import redirect +from flask import render_template +from flask import request +from flask import url_for +from werkzeug.wrappers import Response + +openid_blueprint = Blueprint( + "openid", __name__, template_folder="templates", static_folder="static" +) + +OPEN_ID_CODE = ":this_is_not_secure_do_not_use_in_production" + + +@openid_blueprint.route("/.well-known/openid-configuration", methods=["GET"]) +def well_known() -> dict: + """Open ID Discovery endpoint. + + These urls can be very different from one openid impl to the next, this is just a small subset. + """ + host_url = request.host_url.strip("/") + return { + "issuer": f"{host_url}/openid", + "authorization_endpoint": f"{host_url}{url_for('openid.auth')}", + "token_endpoint": f"{host_url}{url_for('openid.token')}", + "end_session_endpoint": f"{host_url}{url_for('openid.end_session')}", + } + + +@openid_blueprint.route("/auth", methods=["GET"]) +def auth() -> str: + """Accepts a series of parameters.""" + return render_template( + "login.html", + state=request.args.get("state"), + response_type=request.args.get("response_type"), + client_id=request.args.get("client_id"), + scope=request.args.get("scope"), + redirect_uri=request.args.get("redirect_uri"), + error_message=request.args.get("error_message", ""), + ) + + +@openid_blueprint.route("/form_submit", methods=["POST"]) +def form_submit() -> Any: + """Handles the login form submission.""" + users = get_users() + if ( + request.values["Uname"] in users + and request.values["Pass"] == users[request.values["Uname"]]["password"] + ): + # Redirect back to the end user with some detailed information + state = request.values.get("state") + data = { + "state": state, + "code": request.values["Uname"] + OPEN_ID_CODE, + "session_state": "", + } + url = request.values.get("redirect_uri") + "?" + urlencode(data) + return redirect(url) + else: + return render_template( + "login.html", + state=request.values.get("state"), + response_type=request.values.get("response_type"), + client_id=request.values.get("client_id"), + scope=request.values.get("scope"), + redirect_uri=request.values.get("redirect_uri"), + error_message="Login failed. Please try again.", + ) + + +@openid_blueprint.route("/token", methods=["POST"]) +def token() -> dict: + """Url that will return a valid token, given the super secret sauce.""" + request.values.get("grant_type") + code = request.values.get("code") + request.values.get("redirect_uri") + + """We just stuffed the user name on the front of the code, so grab it.""" + user_name, secret_hash = code.split(":") + user_details = get_users()[user_name] + + """Get authentication from headers.""" + authorization = request.headers.get("Authorization", "Basic ") + authorization = authorization[6:] # Remove "Basic" + authorization = base64.b64decode(authorization).decode("utf-8") + client_id, client_secret = authorization.split(":") + + base_url = request.host_url + "openid" + + id_token = jwt.encode( + { + "iss": base_url, + "aud": [client_id, "account"], + "iat": time.time(), + "exp": time.time() + 86400, # Expire after a day. + "sub": user_name, + "preferred_username": user_details.get("preferred_username", user_name), + }, + client_secret, + algorithm="HS256", + ) + response = { + "access_token": id_token, + "id_token": id_token, + "refresh_token": id_token, + } + return response + + +@openid_blueprint.route("/end_session", methods=["GET"]) +def end_session() -> Response: + """Logout.""" + redirect_url = request.args.get("post_logout_redirect_uri", "http://localhost") + request.args.get("id_token_hint") + return redirect(redirect_url) + + +@openid_blueprint.route("/refresh", methods=["POST"]) +def refresh() -> str: + """Refresh.""" + return "" + + +permission_cache = None + + +def get_users() -> Any: + """Load users from a local configuration file.""" + global permission_cache + if not permission_cache: + with open(current_app.config["PERMISSIONS_FILE_FULLPATH"]) as file: + permission_cache = yaml.safe_load(file) + if "users" in permission_cache: + return permission_cache["users"] + else: + return {} diff --git a/src/spiffworkflow_backend/routes/openid_blueprint/static/login.css b/src/spiffworkflow_backend/routes/openid_blueprint/static/login.css new file mode 100644 index 00000000..15b093f6 --- /dev/null +++ b/src/spiffworkflow_backend/routes/openid_blueprint/static/login.css @@ -0,0 +1,112 @@ + body{ + margin: 0; + padding: 0; + background-color:white; + font-family: 'Arial'; + } + header { + width: 100%; + background-color: black; + } + .logo_small { + padding: 5px 20px; + } + .error { + margin: 20px auto; + color: red; + font-weight: bold; + text-align: center; + } + .login{ + width: 400px; + overflow: hidden; + margin: 20px auto; + padding: 50px; + background: #fff; + border-radius: 15px ; + } + h2{ + text-align: center; + color: #277582; + padding: 20px; + } + label{ + color: #fff; + width: 200px; + display: inline-block; + } + #log { + width: 100px; + height: 50px; + border: none; + padding-left: 7px; + background-color:#202020; + color: #DDD; + text-align: left; + } + .cds--btn--primary { + background-color: #0f62fe; + border: 1px solid #0000; + color: #fff; + } + .cds--btn { + align-items: center; + border: 0; + border-radius: 0; + box-sizing: border-box; + cursor: pointer; + display: inline-flex; + flex-shrink: 0; + font-family: inherit; + font-size: 100%; + font-size: .875rem; + font-weight: 400; + justify-content: space-between; + letter-spacing: .16px; + line-height: 1.28572; + margin: 0; + max-width: 20rem; + min-height: 3rem; + outline: none; + padding: calc(0.875rem - 3px) 63px calc(0.875rem - 3px) 15px; + position: relative; + text-align: left; + text-decoration: none; + transition: background 70ms cubic-bezier(0, 0, .38, .9), box-shadow 70ms cubic-bezier(0, 0, .38, .9), border-color 70ms cubic-bezier(0, 0, .38, .9), outline 70ms cubic-bezier(0, 0, .38, .9); + vertical-align: initial; + vertical-align: top; + width: max-content; + } + .cds--btn:hover { + background-color: #0145c5; + } + .cds--btn:focus { + background-color: #01369a; + } + + .cds--text-input { + background-color: #eee; + border: none; + border-bottom: 1px solid #8d8d8d; + color: #161616; + font-family: inherit; + font-size: .875rem; + font-weight: 400; + height: 2.5rem; + letter-spacing: .16px; + line-height: 1.28572; + outline: 2px solid #0000; + outline-offset: -2px; + padding: 0 1rem; + transition: background-color 70ms cubic-bezier(.2,0,.38,.9),outline 70ms cubic-bezier(.2,0,.38,.9); + width: 100%; + } + + span{ + color: white; + font-size: 17px; + } + a{ + float: right; + background-color: grey; + } diff --git a/src/spiffworkflow_backend/routes/openid_blueprint/static/logo.png b/src/spiffworkflow_backend/routes/openid_blueprint/static/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..4cffb07fdf112e035c669c06bd5cbdc9355f43a5 GIT binary patch literal 10138 zcmW++byyT%7hbwVSh_*F^G7Qo-MMtS(%l`qg0u)oHz**;g22)xNJw`rEJ$~Ee)!!# zX3jJB&O9?`&OPrv?|WmkHI?vjsc-=R0KST{yeM*tjV12VLpU7b(3JjJ);SY`y)gJ#7Gfetx_Tu1;QHYj+!7H&46l!`D;*00Tfp zURK{f=OEX?pJqC>dfBUG1D{%rhZ5X{o?IP8NW}0e!)lGN|CA+@n_r#b?gjdfG?&Ys znn9jgEN3-A!WtAxUt3m628c|x95w`?GKY`zIda%&=@hg6D=es;9#k{6rpSjCT61QuN`1^kUjwe&B{8N;zkqGm*=_*t@M5Qs)7Zwq*kw2wd5wk-3OuN~1 z-ow0--6^(`-z}M%kx@BXHv1W+BxmNgcg7u2f5_a;@^Q_YjQ{Yfrh`!Ofkqt;ptVh}7JFEMe&v?>S z3b$2BzQj?7o{3^g6KO&m|2B#9OH5>BvsdI28|I;>rz&Tz!wB0!vaJ13K88rtQyBWS z<%+?I%t{ry7Q+z=5+DM^K0WC?>XrhBw?V#lR&;t<2N3y-LX537s_I)luC>yov0#(^ zH>+2knTV!|-50`FfWBZWN;W5uqel+;n9oPm4Zs3syv)OwR6TF~bSW(EpQY zPZ8)O2q)eNI1OaV`9MGNq*`#d*+warztXb1$-VLx72?u8`d`%Blpfi?_bnYCM%*>i zB{oTkg&f%hjQ2g0MGHmRwpB^bNFcc$rewW@_AzJAVeI+rY^tvuYrLo$=fw&e9)0DoXw3z?5A|C3SjwuZ zNW>D)@(6_&Z?9#`HXMQd-x?aukf0$jL*|oj@TC`|3T>Hj_bsC9)HUJU6}9pV8#UpCV#N{F zI!JwPv#k2bGWOFJF}8GgL)VifucH_3FZ^}4&eiH4ruKe#!xiCd*mYwi)rWXV`L7!T z8i4jSqreQCP_mg^<$~P&m#>Lt@~LJ**zOSznIo_KWBqBwTDB=_v0atbARu&;%Xd(= zwo%FdyL*oDbH!HRbXLO^grp_W#Z6s)zPLOt7jvv3O$NbJ?;xdWMYS`G?9{@Euwgq^ zH$sLmwMNMx@)Qalrhyr3p=u1Rj@Wd|aqu2il@@Sd-(FJ#q)QC1sA%qVH zHT#Mank_4Eg#RQpS+Ng-W)J55z4MC-%io{g6vv%sx<^bnSuEv)8Me4sZRLTF*P!@i zZ=pAdT?i(!4TIkz8zyabSYnC|ZCB$(rwwt)#8-qcP-oc3gFWoJaaRi#ekU5uYW^9_ z`^--CBotdB?5-e8vDpNh-R7>2!EnjAU1Yqy8xHyjdY}JNTx3eng%)83!_3E{7MBxn z;04!Z%2D1dM(03L^DIr@rx;3MW)xj~jlLpNI?4voAJ`qo2`*+WQ+H~28IKhkajyf6$ZXKsurf4O5{|i03uj$|9x%sb8Rm+$QDw#JSGC5v*ZwX$px5uW zYoj8L+R;VR-X$g~VLPuyje{-3Gi0u%Xxoo?rJ;0QYA1dZZ>l&*N1k1vpY;1I-nOX# zO3Rpy6Ur_JN4S(V2>D5O84i#oh#_8EB{Zg5SvzQ<`oV_Lp`IZ62MZ>`l6-uGA=<^4 z=G-Ox99gUS?RvNr@#PcAF#{w)441{dEnC(q9fX?DK=k<5>PYHU&g4UC)k=iJ zmgAThb@-wKyYBA?Y%-K83{AYmCkrUs$N9g@u1BBuaL7c`ff;<JNi80PUhf^8G3j1$B;6C%V+aSB<(EDypjgU?5q*(M*?F?1} z;>Da#%!JWZ#2G*sle^lUVz+Nk;*B^|q51|Lbw>pnStZof>`KQ>cnCVrVExhwKRiRH z!`is!g%spyRbMn_<|(w}E4Ww|^=ZC1=7A>gvY}cs^Yh#%b$%i0fulPSd0{V(%}f+O zRIxP*n9+PP)hH0or=~+gZMkC$XF-n(8wL79%;f))9f7+?&)hms&_yqmqD8(v;}j=z zs)aX4f942gk>!90-~FE=jLkA@KiQKFR2DYDpB0SF|~UoyV=r#^y9SU|Cm`Fc0o%@Vj@4XewX6IHAEo ziovjEea%h1#*A5}Xz--Gy800NJN={H3Kyeu3{WP0JDj0G8CtSG(YuO|A@JOo@^rX9 z@9k#WmhbX}hcA-gt!89ojE|I&_HzbObhY^&Q%n4Vbw%`I0|qZasz4b;lPEZ_BPoPrSaT1jK|Wc}=@oEE=qsq`fGL`O514J;`q!$qoCst4t$NE zTLUdr@d4LeF#!05kv;}SyqmJkJonBJQ}iNN*MZ9*RemBBm+hVo;&YR1YL((NN8r1l z!7e{$h|59F&@1+30~@mSD_wedC{H!}%H7wA%nNkc(nFz`-#kmG5~;)lsoQmy7jbQ7 zTye;lQGOF;##T)&z{%T)kmdOu=WKnS49O&@cJd_pLsk`yp4UWA5Zuyhq}lVqX)oLM zyZ2H=jUK(u4N(CW0Ly6HQ5B4t;@F?FSz!^cO?sb|6R(#6*?m!QD;fv|^f>d;1UVJ? zE|lBCbKYj&XTOpplIOeUHa902ONkaOp=XwTM3Hz?uA3T`{oh^J4Ru<6dO~^PPWJA6 zoS)neu4RO#tyv#8)OmXRVJi@;yvh&wJ_$x4>&nSn@qIKW)-xci7X_(#dwZr*%lD%) z`JTRrpv0vqE%IT&Azjt(F+A;SGwLt;S!;se1F9;qvW}o1w?pP)$y^lhd{@)MHbM}& zUS3wNArELN*|9=5&&~Ak!J|UmUa}MWwnFei7&LJq{XzY{DFp01f25hDTR7Fy!e=XZj+{bzP@Slr)^p#Yo>lK6(>szuWyFDq~p~>^@HsTARCfdru^x! z=G&Q}lb4+Xg=k{?m5aV+vaT%x>skeDAAEY0tLQVZx%3;Dt$7$6_Wyoi)lKM~)%xon%&UlT~D1%&3m=rI2jj8VVmWvgO^jn zi6*8Xr*%c*+~xgsSE$2aoEqVDQId4MW*P#&vMrbL!s|Nu20wid8-cO&5b)0oaXW2* z=ya0uqZdSgnI+qY(4j7Ou!Ej$nz&DFe00ydCNvfvH4sOfq_y?{dT&>;)p3x;l|f^A5+Y>rm9MNVeRDCMAcl?im5?6KW?Qg ziYGOOi-KZIJ>R5M)GF!kZn6$ozzXHBL+hJm${p2OPl!cpE7iWdFjk*;>1(NO*0t7M{~jTOvwN@Udb?@^1_{E?Y5K& z^^W350;BOe#|e$ChKBzv1Q$~{RnnU0``uzt zeldt->bl-?)6*<)@*SQN>_c!r&<_Q`+-tG`|aIa>=3XX>I*;EltcexSZqY;CP6^~t}8byIbBq?Y@i$_S;VLv zPT+xGXgukU=dZSpDZMUlZiKb(WY*90K2gV>>~31{Yn)vc8)R@6wU@W&X+P+hR$MJ; z$4e8+|2}E~**L5Ef?lTy{Lp5hHO88RNDf?(7w$tXv?O_ z&wO}uRP;WBGHE$=@O8^(Mc$8oXF-|Wk>>Zw9ZhFz(8F?0#?J>eF{Dh3!`}U{mPg$; zvlV0-hly@}O7F4siF>y(oi5AM2fBJ(OJi>O`Xxe(h~-9sPh|o|9`OxKfQ$GKui9^4qsT~Nvw;NX7fh@m{qcT2{I5sE~;Ir$UQ(pIxxr?F%s?A7WA)<{{J^2mwcO(3EZ&8(wm z>lcx-ccf~4P#ETOXE#J8u$3$cZSpIC={N6ew_a>1!3i>xRqcudaeOvS$o60NbEH@A zwQ}`#Ov6u18FQjXm40PeNXzSE00Ajl--HOtPKz%kg1GY16RD~sa&HPe_%5ZAos5rn zJkL~wxtmc_-6W{X@{*m*{o^VEuiOIHX0!zPh#!96>aNqB?I+(@!zE3Y(fFgbF}TE|iysG*Sx+bAkO9PZz2FKA(~y`X zI-K_t5I^a1}Hk`;)wT{AuZCrzgzFt+8_ z)-ZLSDI@R?%bL9E!*VYKX-vE{Prq!-NXxs{GX|&RD58x(Hcz`L_xAjwLFd0F%E80% zg?v>}+T4LG5Uw;ZLnR=x8!)L8`uW|#neav2z@t(3QTuiJZyL-gq3`M$3%!bZ?6cZWEB7pqK3}Gk_T)yG9kx)ufSRM6{!IpV%{81- z6R<`*XhT)5YsV(;phm9xe+*5n-qSfEt9|@ z0q9}`ze9B&9hJT=er9ADFU<kwqhlns`y#nY;3>i*a^sO+Q2&+o}On4zsos@xcY~u+p}{bC&UH1 z){EJ0r4yM?6fzw;QE4Yf!n(Z=8cYX$d$m4~1IIfRWo>jDR0FK>XJR^diT^$oqa?{GlCZpAMU#u# zL_NB9o^+(!D;lwr5C;t^0 zJwlYK#cN6yp>ownvLFn3WG|gZ8pYvtGzeun9qC*R3 zef-QanJN=n5A_EYX1P}9OoF`3Cf&(} z8<}yuvL1lc_|qCNCk!jF#KNo-uz9BaEDPBvG@sWL%u`K#*WaDJiYLud%#M;|&1uv@}T=t|W0k3*E~>EgEsDFc~&8!Iz{U?n2g zH;aYO$~w7unax16G1kY{p>G*$Mxkr>)zOckunDTOSzw=O4Izo|Cb=$Z?RN zU22!tbY+qvaDywKFhQ4{ZiJ~Lj(u5LchI*grj?KrjEI+^GvS{7QZX{KKn=l%-F`hT ze3d_m4K|B7r~ zNuCHq@awnyIW0Q)?manP-aIw0?5$GYbW&6(hMXz!b$O??_-pudP4av;Bw_p;=xR;# zgPy3(Kj@M@z{X0ROJV_Zj=AITwN$l7ddycRg~qam?TH zl7n#^-dbKwlQHMNwU7_lNHK^qA9VE*w0`9|L||pUZD*P1qmt z$)dWZsGhWVcFhQjX#Jy^>L|s+nv22@9zU1`jQN9lG(Ad zi)t*nMkjUEs#lR6zKFyYhnDkw{7!I>3uL!hJPr+ipxo=q8v6F`XS9#oht$yb?LZH= z8Ew0SkDa;T_h)h>|NHK{S-{K3 zp4i9NsUzHbOeNjyGIyRxtWFc3US~p~a=8oxb$`1=vw4GchH%akf>&KkZ@(yeMLE{d z&R+>%L$F~9QWdeqM==~hUOrJGJc?8EGZe%Da-6@)ClB1Yex}4W2@J3WGZ#5VZJMPz zRM_t{5w#)41DDM;Pn*GizLM+cc+n=O3&0AO~@ahRJL;0x?nW_;}@{wLc? z)aQrx##57op^Th{!AGVP8E+{W8RkHxOyX2rH~HNuDCzforDl9>R*ebS0>*MIB~W?H zp&kD?UJxsd5>uNnp|y=iW~#h+W!-5hg%8$MgR1^|EfCTbKQ*;AsNhO1fNg6(Lb`gn z@kzq4ee!{4TmTu&V8+E|Od-<#q%8|cQ{0|BHX&^wI&nVHTP1_E2AHjyyp55fK+iN! zaorW79U~-eY9o`GK6e{!KZdCWrzd1AE%i-JKS*q{_cpJ+oYfy~Us=M|vB1t6B5x_h zP+!=VmXDZM21zvzsvyxL`g`c%-?HF9pT~X--`+FM=~lBt)#!!4Wc^hR_IvU2bw6R& zItr$tQN;9DO{hrod3Mf%mS)EU4$}T6(W@IYG1v06HWoLer5X3}Y8F?UC@#WrDrGhF zR~WpO~=!9{|1W>zK>?Jzy`%+|p9lWm~dE|EH8a@^nf*8J(?R zwcl+MEtTU)@e2Z(_n0^Wk0^7;Mch9EO4gw-ZxC>#Z36!X?EPf{ktXT!UjcQTRUI6> zLk4$Yzy9*U`0GWo25!(Fu!!If-j?l^pCS!gIu#b%SCJ6 zgb`ihEdDwwH5k3{J#vcXz>mA?q`fkgEN5)Bag`iVit!Gst8OS@*`p`pcz6!6w>$S& zD_@TcU+q&1w`-T@QorW=&YAkLEEuoPHk^?)CgY5Y#;bE`=mhCf=PV^A{HH7RUR7CU z9%Eqa52C3@&w0M>0zoGGdZ}zl75ns6#GBZ`!LJb1PC^GuSgoU-;?-xnE{v?VP=*a& zS&VlbX0CJqoaK8i*Q>GV`I*RWk}@UNf$7~O7q!ta+R?!~N6p`~RqxC5`DYW>FLL2( zZSYSCvH%L{@vWu-(}$uKgS>h7CUQzTnF!_D$W9C8^`qAHK!S(Cd>&3ZJ<-F2?ii6K z=l+=+7TZtnT{LaW5GklKq-_)L(tbEO1OrEfgb)67SJ!jAI6l}thfnPfU9jpT0glKW zZE9laVr)codHL(5vdjgarXN4?742}821F|I&F0e!^{6@4DVz=xwt{ z5ZhMamyCGk1Ndf3gmzXNaofb2MbgD2@tAyzqd{$^@RP*0-1j0_znPxN2OZU5f*9E>DCva5qw7(rn%81?+X`r;IFe9SaVi8(ny?U!h@Cn25aWtyjK;hdgF11WcPi z^|jk#{N-n)FEYi>pBX%5J391OISIx2M3+*_jTd@ZPA4;^RAj;}XnA{&F|7gZBDpu; ziYf?2ifsw{!#)Yl!nw(?h`Y+Z3fvE z=U~Dpy>J>uWP65V|02gRd4&;hezH9DQ?$2H+wGDCO){JUzwT_+r!Ypj&@d6Z!^^Gl zCqcT!Y0@c(#hP)L8S}~<1K2bS!jd)9NvOZV7u}P;aFRO~6PI1V>oki<8E{eyKUEGI zgqGdABSfCP5~rV&QCs9Zrff5t+DRN@<1pedtWcmst=X)?8T@ixU7t$!Dfd*h_hWlkHWJijem=c!Kc z0wpG*o4$$Asd!cm;K_rRUmV?b@K8VQY9qqr8C*Z@l{Y0t zVSk)H?6_axxtm}+@Xd$Qe#G zld+hu`HlRF=j%sEz1k?!>aRcMqvRq~CQ3&f30EA;zMQ?heZzaz&?TFTDt>=(Mfny4 zVj3nT#9kLnXWH5ORCy*{3FzVdUjV-%JnllCl2i0DkhbKx7{48Na z$${Z9f< zE!-l<(a3DXOx*cjIT7*ml5oI&fOKrr)XTf6;2vYqHz!efKH5_OC6XqA@gXU0p%E-V zt$np5w|}zHyW}u}p$FRG0^Lc#JF$6gZsz5 z&Zxb8IUKj_=lnO6AvTv?5(bmOmf*O`BsHjS;AWq9l(_j4=SVTD1LnT*%{DRD8CuWK z>4u%bD4x>Hr4`OpC?!LkP1zg4iVV+Ww{z4d4O*Y%p}lL3Iv1hBFqB_s>YPrG@k}cA zKy!3~FL!2zpy{;BnMnK(xhpdT`x?p@5VO6m0ln6L%J2&y=4dUar)K0qMVSl*+8*(r z_2?xa2{AFT5Rm}jU^t;erU$-Jvm9+NhnP6KBe&uCC*4Zg4b~^;s7p~Qea?7f==wIl z87arQ_$(y9t0xb&KRs_FlKse~s2E+GiFbSEY9d0!nsML|;@_SOpeKeuZ5 AE&u=k literal 0 HcmV?d00001 diff --git a/src/spiffworkflow_backend/routes/openid_blueprint/static/logo_small.png b/src/spiffworkflow_backend/routes/openid_blueprint/static/logo_small.png new file mode 100644 index 0000000000000000000000000000000000000000..d0ad4499a9f187c829438db52f20575863e526c1 GIT binary patch literal 5000 zcmV;36L;*1P)E{GC{1Z z){<~C!Ba7n%t9|$>)M^n$@zG+n+SV4|$~&0}ghw7Td)+_InVB;o2>}x9 zz0L3Q$tP#;z4qE`?^$O*)>``ry#%K`ezOSgHBfpdiHerqYNrqXItcX^>?iacYvFCw&9OzV; z_7VpH{W{2BFCsT~G}Ef;>mqWTstyGH2uuWK`toiB)&ds-MZjgiO(HT~RqKEUfiQ5e zh&-gK#{$0xeg(`2?)U4r0p9@*2WA5;z%?RL@5^w4pWg~x1pEm2EU;2U7OLt+z+@l; zJP+If%ma#n1tRkDevVro+Q9$}z1;m|bFp~^(Rm;ffe}EYhyFTJRDcowO#4z-D{Loas+rbA{q z@TrfW{cXTqZDITCjtW&Z3>*u*?PvYL|NlGSB%m2@cKsbN2UvnPQ+@_-M(zl2i--lb z;H@4JyxIR<;5PxfQecyaC{RXrI|G}5;eZv9W;_MO=Mw@3;mzQE=wXD@#*~XUb7iqn zktalT*PbeiA&qKNS2T*8$L=q}HlQ{gw)3~mcat4d>!+%B0Y?F~BC=Uk3!280dAOf` z9Jm3f06quo6p>7T{w-Bq1l*1{qXuz1-c0*9JO$3;KHyPRT?c#*xJp$Q0*Bzurq8SD zoxTju;LWJF`TXDNVo3V1j}RwT{Rfb-etLadKYM)Bt!{l!Fn40^U@#xz%{H)SB?UlL zhv02d8vU6KRUHIu7ZFv}e!x&*Gk}P+scNZ+G^%PbU_@kBfL{@wl6?pa5RvVw+8<~W zk+iBF1VlvS!*+5+Job|GXZc~k1`)BUYJ8rXfslwa`!t6FAGUj0`p^RigB+1h%5Qo~ zQ2|6kF<;5LrKI_KvbBS}roeM6u&!NsqyK+52ZV^UkzdBQ`+2(pbc1Lm#?AxYsyatLY*= zvQJ6-&{OC&Wlh%2dAzrH_u*d?VZde+s#5e3%Cbi|74a#eItJnShT1v5YF{3W&HWr= z&sEDPF{IV95s?>@8;|-04^PPy(5M_PqL~~`H1kVb_y1JvLL^pmwZTRJ5T0nLo%8#> zsXw%$=2IEfZwhjxfj-Q0@;tSzOeWlIJ=%f2L>Slr3;__8iA;Vd zl>K4M+`M)?Bo#Pig33(<<|b2F+-fg;UX^Hfl1wvGA=DYA!Qq#{diLPa;? z9h5&ZgjMwk5jh$F(G#5EZ#*Uy)l1c~b6=T?n2aLplPyEnu=ujJo)A>54k$>c%|Y4c zVc&HRZc}yXI51}kz`)q`Lxps8M>Hb3L_;>EY4d%u@e;~Yc=p>ITAO#CDkAyV0;}8c zJXtv$JoCC?X`89ZHWO0$ZTEY5fZ)1D8)N~VL!~{Ir50ba)tOO!przU)!m6QLMeL<` zUL1f)sxoPR;<3CpRdI?pC5hEspNwYa*{1T-PQc@VZy!AFVopmrGL{ zk9mm16x$fIc?wRDU8>AJgzMT=-u{ZhQ~1iNDxYsJf(T~@J7D0Hk+bIY|8V}5y8@XL z0zR_U1!eyvP{9e#MONhz9-flf?75QvX%PmfudDoZX)He8z;(cv=y-Y9XDE816N^8c zOmHVaX)J!4@gkZK(fb;jhdKJMg@PUdY!;Dw8+6Dm*;@u>Gi#2rR_k(1Gv?Cd zqS+smUg4e~8NL;0PMqOy{FYc8L{iDx$|)X=1Zi*@%yMFJfW~B^YO*iq3PG*}5cnLf z+Yi@m1sF8r`lymV-mjhlA_~R3(mN&qPX;fz5_s8`h6p{_?q3EGVKv9uQ@}7X^NgGo;NWG ztP$iV4T;L!ctm3Hguu}NsyvdatGXjw=FF)6B1V1?V7nxlsJbg(xB4rBT;Y#vea>{% zn5cRc`09vh?ttcEMj0{t93rS{LaI6e(d)_0)^Zhi%4&pRC};T{80hrdbT-Nbk{`f` zs?K$2oRDK5N|&#@=H`b?a|bjdqjTdBlBWQim^*Tth91KU zI}DCg1mrkl>~|vN)l*Y-bC!EN?IEama)8mpET!#dUXx=?Ph0Pzu4Ug{IzR=yM>pc74d^rxyDZ) zIrMUO_?AWP#zHa~k1!ti0M&O88BIQxV3LzGmnPxAJ8B>!rcK~wyrAk)fg&$#Pe=;3 z4K-cTWHXWpcbiilUxP5tE3*^vl~s(Kk&UHM)w=~Q#ygy| zpZe>9UHBv zzF1X$md)sZ1^_WUrRpiGdQ18({&Q9-n^RHyVzj}$<{if5#L_F=(#8dDqqSyYU|aqi zuG<=|s6JCgPR);r{2-O6ycxVWVjD86)mugfg}hq~)7ptN0#J}8D%=-BH(@gyc9UDA z4c86tXxRRsm}m=kkH0!MmdqW6GY1`KeL z;ju|r9VxFl4lhs$Ad}Ycw_7jfNI6@A7h3x%wl27u^1 z%fA%h^-|NqYa0q?*0LGy7;DVAB8)<{NkvvBHyr&)Z#xOX8N0SH$G#re4TVz!Lx+TS z3q$Q46cv{|*VeMD73ilhv}{K8r!*tuvhrk=*;uo}inni5I33r0j0VPM`IwNWtVS4> z3&j;)Z$Fl5_2q!6t8~ze8w#uz0HL%l+LPR>ItiGR%MfNIYiFbeO#;gHmt8$Sd(@~Z>!`^tUG!`FC@1ii;%b0?2y>Ph4O=_KD(=FfM z0s2xloiTEfTA7$ltLO@VbU5=2-u)}EGTzx@j=}c?0^1rQb`5|kkvw-Vj4=pp*VmFz zW;bQsvS?0sOiGgqQc8JOmooParZkvpdjHd@!g5h1?@Urr$NO&sjuAa}=;iJ}(9Z*i zVKsoD&-w%>oS)6wT>%8m zDs>6oMa67MuD+_ilF#R)_WN&R0(M#0u;~tWGy4R6HXXKORk_dEFSLcGeW8+KBT(f!i}LY#U^D;aw;^+5eAHpu{L<1-|iK1NK5=GkIkd zd060Dyg_!F2Fbh;)7-1pFLrlE%Bv@-@}GHrJhJan%M;t?&q;#b9*Na_Id{+!`Dxh< z_tpA^?mzaOw})h6&flEa+&_bU7C=RB7!s>_ZEK?PE#HbWFF?MbUL(0Zvk1#o`5u5l z&i0pz##DsY0g{Vmf8dnI*C8BA(Do`U&psNEp40>Eh^lc{=-yznnA|I!q_@<)+iFfaDo7K0k68L_CF}ax(fRdKX zv*LYkAV}9(zxmU-8O3+D+?`D;GQGLjG&-?(lZY$})TPFig9pm$f|wEEdH?^% z{hUlXWOLpyL(mDK6c~ghWA@t$pzx*|`=y5Yv)6S((tzY6_y;%H86rFo$SMK`1}d!+%vU-wN$oYZC$8NBatV2# z>WPk|Zt4l%4T;JpMf4K#dt>k>rmP;NMdaFK-JH7%D!Oi~c(E5bNPeH{XHXdQqxxL% zrb!s|b&&Bs@zk=4_~QE7sx`x|y7{2CmR*HwugWWkc0+-+1%zcq93`o&fGQ*<|F zRn;YeL;+NEO|UGPsQg{DqGqOAJ37hvb)-KGZ zZKf?}wP>iFb8lI>`+GH^Z>iN&5G?~q0)GsN{A^2Y<=XcAe0djwH92ZANB;3 zbnIPd&Km>XS4BQeKv98h3RfOj(fZJbLKs{(t*yo9Nc$-{kM|HWsWDOYr%uZI(1!zp zFn}Mwc#a=udY%X$M`f#uu1q@iXSh{ehNShO4+jDTu>ro>>gkV2mOk{MOZ+cn4Di%D S|CE~m0000 + + + Login Form + + + +
+ +
+ +

Login

+
{{error_message}}
+ + + diff --git a/src/spiffworkflow_backend/routes/process_api_blueprint.py b/src/spiffworkflow_backend/routes/process_api_blueprint.py index 42930765..3e7eed53 100644 --- a/src/spiffworkflow_backend/routes/process_api_blueprint.py +++ b/src/spiffworkflow_backend/routes/process_api_blueprint.py @@ -1,6 +1,7 @@ """APIs for dealing with process groups, process models, and process instances.""" import json import random +import re import string import uuid from typing import Any @@ -30,6 +31,9 @@ from SpiffWorkflow.task import TaskState from sqlalchemy import and_ from sqlalchemy import asc from sqlalchemy import desc +from sqlalchemy import func +from sqlalchemy.orm import aliased +from sqlalchemy.orm import selectinload from spiffworkflow_backend.exceptions.process_entity_not_found_error import ( ProcessEntityNotFoundError, @@ -51,6 +55,9 @@ from spiffworkflow_backend.models.process_instance import ProcessInstanceApiSche from spiffworkflow_backend.models.process_instance import ProcessInstanceModel from spiffworkflow_backend.models.process_instance import ProcessInstanceModelSchema from spiffworkflow_backend.models.process_instance import ProcessInstanceStatus +from spiffworkflow_backend.models.process_instance_metadata import ( + ProcessInstanceMetadataModel, +) from spiffworkflow_backend.models.process_instance_report import ( ProcessInstanceReportModel, ) @@ -63,6 +70,7 @@ from spiffworkflow_backend.models.spec_reference import SpecReferenceSchema from spiffworkflow_backend.models.spiff_logging import SpiffLoggingModel from spiffworkflow_backend.models.spiff_step_details import SpiffStepDetailsModel from spiffworkflow_backend.models.user import UserModel +from spiffworkflow_backend.models.user_group_assignment import UserGroupAssignmentModel 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 @@ -150,16 +158,15 @@ def modify_process_model_id(process_model_id: str) -> str: return process_model_id.replace("/", ":") -def un_modify_modified_process_model_id(modified_process_model_id: str) -> str: +def un_modify_modified_process_model_id(modified_process_model_identifier: str) -> str: """Un_modify_modified_process_model_id.""" - return modified_process_model_id.replace(":", "/") + return modified_process_model_identifier.replace(":", "/") def process_group_add(body: dict) -> flask.wrappers.Response: """Add_process_group.""" - process_model_service = ProcessModelService() process_group = ProcessGroup(**body) - process_model_service.add_process_group(process_group) + ProcessModelService.add_process_group(process_group) return make_response(jsonify(process_group), 201) @@ -183,20 +190,20 @@ def process_group_update( process_group_id = un_modify_modified_process_model_id(modified_process_group_id) process_group = ProcessGroup(id=process_group_id, **body_filtered) - ProcessModelService().update_process_group(process_group) + ProcessModelService.update_process_group(process_group) return make_response(jsonify(process_group), 200) -def process_groups_list( +def process_group_list( process_group_identifier: Optional[str] = None, page: int = 1, per_page: int = 100 ) -> flask.wrappers.Response: - """Process_groups_list.""" + """Process_group_list.""" if process_group_identifier is not None: - process_groups = ProcessModelService().get_process_groups( + process_groups = ProcessModelService.get_process_groups( process_group_identifier ) else: - process_groups = ProcessModelService().get_process_groups() + process_groups = ProcessModelService.get_process_groups() batch = ProcessModelService().get_batch( items=process_groups, page=page, per_page=per_page ) @@ -222,7 +229,7 @@ def process_group_show( """Process_group_show.""" process_group_id = un_modify_modified_process_model_id(modified_process_group_id) try: - process_group = ProcessModelService().get_process_group(process_group_id) + process_group = ProcessModelService.get_process_group(process_group_id) except ProcessEntityNotFoundError as exception: raise ( ApiError( @@ -231,32 +238,55 @@ def process_group_show( status_code=400, ) ) from exception + + process_group.parent_groups = ProcessModelService.get_parent_group_array( + process_group.id + ) return make_response(jsonify(process_group), 200) +def process_group_move( + modified_process_group_identifier: str, new_location: str +) -> flask.wrappers.Response: + """Process_group_move.""" + original_process_group_id = un_modify_modified_process_model_id( + modified_process_group_identifier + ) + new_process_group = ProcessModelService().process_group_move( + original_process_group_id, new_location + ) + return make_response(jsonify(new_process_group), 201) + + def process_model_create( modified_process_group_id: str, body: Dict[str, Union[str, bool, int]] ) -> flask.wrappers.Response: """Process_model_create.""" - process_model_info = ProcessModelInfoSchema().load(body) + body_include_list = [ + "id", + "display_name", + "primary_file_name", + "primary_process_id", + "description", + "metadata_extraction_paths", + ] + body_filtered = { + include_item: body[include_item] + for include_item in body_include_list + if include_item in body + } + if modified_process_group_id is None: raise ApiError( error_code="process_group_id_not_specified", message="Process Model could not be created when process_group_id path param is unspecified", status_code=400, ) - if process_model_info is None: - raise ApiError( - error_code="process_model_could_not_be_created", - message=f"Process Model could not be created from given body: {body}", - status_code=400, - ) unmodified_process_group_id = un_modify_modified_process_model_id( modified_process_group_id ) - process_model_service = ProcessModelService() - process_group = process_model_service.get_process_group(unmodified_process_group_id) + process_group = ProcessModelService.get_process_group(unmodified_process_group_id) if process_group is None: raise ApiError( error_code="process_model_could_not_be_created", @@ -264,7 +294,15 @@ def process_model_create( status_code=400, ) - process_model_service.add_process_model(process_model_info) + process_model_info = ProcessModelInfo(**body_filtered) # type: ignore + if process_model_info is None: + raise ApiError( + error_code="process_model_could_not_be_created", + message=f"Process Model could not be created from given body: {body}", + status_code=400, + ) + + ProcessModelService.add_process_model(process_model_info) return Response( json.dumps(ProcessModelInfoSchema().dump(process_model_info)), status=201, @@ -277,7 +315,6 @@ def process_model_delete( ) -> flask.wrappers.Response: """Process_model_delete.""" process_model_identifier = modified_process_model_identifier.replace(":", "/") - # process_model_identifier = f"{process_group_id}/{process_model_id}" ProcessModelService().process_model_delete(process_model_identifier) return Response(json.dumps({"ok": True}), status=200, mimetype="application/json") @@ -292,6 +329,7 @@ def process_model_update( "primary_file_name", "primary_process_id", "description", + "metadata_extraction_paths", ] body_filtered = { include_item: body[include_item] @@ -299,33 +337,68 @@ def process_model_update( if include_item in body } - # process_model_identifier = f"{process_group_id}/{process_model_id}" process_model = get_process_model(process_model_identifier) - ProcessModelService().update_process_model(process_model, body_filtered) + ProcessModelService.update_process_model(process_model, body_filtered) return ProcessModelInfoSchema().dump(process_model) def process_model_show(modified_process_model_identifier: str) -> Any: """Process_model_show.""" process_model_identifier = modified_process_model_identifier.replace(":", "/") - # process_model_identifier = f"{process_group_id}/{process_model_id}" process_model = get_process_model(process_model_identifier) - # TODO: Temporary. Should not need the next line once models have correct ids - # process_model.id = process_model_identifier - files = sorted(SpecFileService.get_files(process_model)) + files = sorted( + SpecFileService.get_files(process_model), + key=lambda f: "" if f.name == process_model.primary_file_name else f.sort_index, + ) process_model.files = files for file in process_model.files: file.references = SpecFileService.get_references_for_file(file, process_model) - process_model_json = ProcessModelInfoSchema().dump(process_model) - return process_model_json + + process_model.parent_groups = ProcessModelService.get_parent_group_array( + process_model.id + ) + return make_response(jsonify(process_model), 200) + + +def process_model_move( + modified_process_model_identifier: str, new_location: str +) -> flask.wrappers.Response: + """Process_model_move.""" + original_process_model_id = un_modify_modified_process_model_id( + modified_process_model_identifier + ) + new_process_model = ProcessModelService().process_model_move( + original_process_model_id, new_location + ) + return make_response(jsonify(new_process_model), 201) + + +def process_model_publish( + modified_process_model_identifier: str, branch_to_update: Optional[str] = None +) -> flask.wrappers.Response: + """Process_model_publish.""" + if branch_to_update is None: + branch_to_update = current_app.config["GIT_BRANCH_TO_PUBLISH_TO"] + process_model_identifier = un_modify_modified_process_model_id( + modified_process_model_identifier + ) + pr_url = GitService().publish(process_model_identifier, branch_to_update) + data = {"ok": True, "pr_url": pr_url} + return Response(json.dumps(data), status=200, mimetype="application/json") def process_model_list( - process_group_identifier: Optional[str] = None, page: int = 1, per_page: int = 100 + process_group_identifier: Optional[str] = None, + recursive: Optional[bool] = False, + filter_runnable_by_user: Optional[bool] = False, + page: int = 1, + per_page: int = 100, ) -> flask.wrappers.Response: """Process model list!""" - process_models = ProcessModelService().get_process_models( - process_group_id=process_group_identifier + process_models = ProcessModelService.get_process_models( + process_group_id=process_group_identifier, + recursive=recursive, + filter_runnable_by_user=filter_runnable_by_user, ) batch = ProcessModelService().get_batch( process_models, page=page, per_page=per_page @@ -355,9 +428,9 @@ def process_list() -> Any: return SpecReferenceSchema(many=True).dump(references) -def get_file(modified_process_model_id: str, file_name: str) -> Any: +def get_file(modified_process_model_identifier: str, file_name: str) -> Any: """Get_file.""" - process_model_identifier = modified_process_model_id.replace(":", "/") + process_model_identifier = modified_process_model_identifier.replace(":", "/") process_model = get_process_model(process_model_identifier) files = SpecFileService.get_files(process_model, file_name) if len(files) == 0: @@ -377,11 +450,10 @@ def get_file(modified_process_model_id: str, file_name: str) -> Any: def process_model_file_update( - modified_process_model_id: str, file_name: str + modified_process_model_identifier: str, file_name: str ) -> flask.wrappers.Response: """Process_model_file_update.""" - process_model_identifier = modified_process_model_id.replace(":", "/") - # process_model_identifier = f"{process_group_id}/{process_model_id}" + process_model_identifier = modified_process_model_identifier.replace(":", "/") process_model = get_process_model(process_model_identifier) request_file = get_file_from_request() @@ -407,10 +479,10 @@ def process_model_file_update( def process_model_file_delete( - modified_process_model_id: str, file_name: str + modified_process_model_identifier: str, file_name: str ) -> flask.wrappers.Response: """Process_model_file_delete.""" - process_model_identifier = modified_process_model_id.replace(":", "/") + process_model_identifier = modified_process_model_identifier.replace(":", "/") process_model = get_process_model(process_model_identifier) try: SpecFileService.delete_file(process_model, file_name) @@ -426,9 +498,9 @@ def process_model_file_delete( return Response(json.dumps({"ok": True}), status=200, mimetype="application/json") -def add_file(modified_process_model_id: str) -> flask.wrappers.Response: +def add_file(modified_process_model_identifier: str) -> flask.wrappers.Response: """Add_file.""" - process_model_identifier = modified_process_model_id.replace(":", "/") + process_model_identifier = modified_process_model_identifier.replace(":", "/") process_model = get_process_model(process_model_identifier) request_file = get_file_from_request() if not request_file.filename: @@ -449,13 +521,17 @@ def add_file(modified_process_model_id: str) -> flask.wrappers.Response: ) -def process_instance_create(modified_process_model_id: str) -> flask.wrappers.Response: +def process_instance_create( + modified_process_model_identifier: str, +) -> flask.wrappers.Response: """Create_process_instance.""" process_model_identifier = un_modify_modified_process_model_id( - modified_process_model_id + modified_process_model_identifier ) - process_instance = ProcessInstanceService.create_process_instance( - process_model_identifier, g.user + process_instance = ( + ProcessInstanceService.create_process_instance_from_process_model_identifier( + process_model_identifier, g.user + ) ) return Response( json.dumps(ProcessInstanceModelSchema().dump(process_instance)), @@ -465,6 +541,7 @@ def process_instance_create(modified_process_model_id: str) -> flask.wrappers.Re def process_instance_run( + modified_process_model_identifier: str, process_instance_id: int, do_engine_steps: bool = True, ) -> flask.wrappers.Response: @@ -507,6 +584,7 @@ def process_instance_run( def process_instance_terminate( process_instance_id: int, + modified_process_model_identifier: str, ) -> flask.wrappers.Response: """Process_instance_run.""" process_instance = ProcessInstanceService().get_process_instance( @@ -519,6 +597,7 @@ def process_instance_terminate( def process_instance_suspend( process_instance_id: int, + modified_process_model_identifier: str, ) -> flask.wrappers.Response: """Process_instance_suspend.""" process_instance = ProcessInstanceService().get_process_instance( @@ -531,6 +610,7 @@ def process_instance_suspend( def process_instance_resume( process_instance_id: int, + modified_process_model_identifier: str, ) -> flask.wrappers.Response: """Process_instance_resume.""" process_instance = ProcessInstanceService().get_process_instance( @@ -542,19 +622,24 @@ def process_instance_resume( def process_instance_log_list( + modified_process_model_identifier: str, process_instance_id: int, page: int = 1, per_page: int = 100, + detailed: bool = False, ) -> flask.wrappers.Response: """Process_instance_log_list.""" # to make sure the process instance exists process_instance = find_process_instance_by_id_or_raise(process_instance_id) + log_query = SpiffLoggingModel.query.filter( + SpiffLoggingModel.process_instance_id == process_instance.id + ) + if not detailed: + log_query = log_query.filter(SpiffLoggingModel.message.in_(["State change to COMPLETED"])) # type: ignore + logs = ( - SpiffLoggingModel.query.filter( - SpiffLoggingModel.process_instance_id == process_instance.id - ) - .order_by(SpiffLoggingModel.timestamp.desc()) # type: ignore + log_query.order_by(SpiffLoggingModel.timestamp.desc()) # type: ignore .join( UserModel, UserModel.id == SpiffLoggingModel.current_user_id, isouter=True ) # isouter since if we don't have a user, we still want the log @@ -600,6 +685,7 @@ def message_instance_list( .add_columns( MessageModel.identifier.label("message_identifier"), ProcessInstanceModel.process_model_identifier, + ProcessInstanceModel.process_model_display_name, ) .paginate(page=page, per_page=per_page, error_out=False) ) @@ -729,12 +815,16 @@ def process_instance_list( end_from: Optional[int] = None, end_to: Optional[int] = None, process_status: Optional[str] = None, + initiated_by_me: Optional[bool] = None, + with_tasks_completed_by_me: Optional[bool] = None, + with_tasks_completed_by_my_group: Optional[bool] = None, user_filter: Optional[bool] = False, report_identifier: Optional[str] = None, + report_id: Optional[int] = None, ) -> flask.wrappers.Response: """Process_instance_list.""" process_instance_report = ProcessInstanceReportService.report_with_identifier( - g.user, report_identifier + g.user, report_id, report_identifier ) if user_filter: @@ -745,6 +835,9 @@ def process_instance_list( end_from, end_to, process_status.split(",") if process_status else None, + initiated_by_me, + with_tasks_completed_by_me, + with_tasks_completed_by_my_group, ) else: report_filter = ( @@ -756,11 +849,18 @@ def process_instance_list( end_from, end_to, process_status, + initiated_by_me, + with_tasks_completed_by_me, + with_tasks_completed_by_my_group, ) ) - # process_model_identifier = un_modify_modified_process_model_id(modified_process_model_identifier) process_instance_query = ProcessInstanceModel.query + # Always join that hot user table for good performance at serialization time. + process_instance_query = process_instance_query.options( + selectinload(ProcessInstanceModel.process_initiator) + ) + if report_filter.process_model_identifier is not None: process_model = get_process_model( f"{report_filter.process_model_identifier}", @@ -804,20 +904,146 @@ def process_instance_list( ProcessInstanceModel.status.in_(report_filter.process_status) # type: ignore ) - process_instances = process_instance_query.order_by( - ProcessInstanceModel.start_in_seconds.desc(), ProcessInstanceModel.id.desc() # type: ignore - ).paginate(page=page, per_page=per_page, error_out=False) - - results = list( - map( - ProcessInstanceService.serialize_flat_with_task_data, - process_instances.items, + if report_filter.initiated_by_me is True: + process_instance_query = process_instance_query.filter( + ProcessInstanceModel.status.in_(["complete", "error", "terminated"]) # type: ignore ) + process_instance_query = process_instance_query.filter_by( + process_initiator=g.user + ) + + # TODO: not sure if this is exactly what is wanted + if report_filter.with_tasks_completed_by_me is True: + process_instance_query = process_instance_query.filter( + ProcessInstanceModel.status.in_(["complete", "error", "terminated"]) # type: ignore + ) + # process_instance_query = process_instance_query.join(UserModel, UserModel.id == ProcessInstanceModel.process_initiator_id) + # process_instance_query = process_instance_query.add_columns(UserModel.username) + # search for process_instance.UserModel.username in this file for more details about why adding columns is annoying. + + process_instance_query = process_instance_query.filter( + ProcessInstanceModel.process_initiator_id != g.user.id + ) + process_instance_query = process_instance_query.join( + SpiffStepDetailsModel, + ProcessInstanceModel.id == SpiffStepDetailsModel.process_instance_id, + ) + process_instance_query = process_instance_query.join( + SpiffLoggingModel, + ProcessInstanceModel.id == SpiffLoggingModel.process_instance_id, + ) + process_instance_query = process_instance_query.filter( + SpiffLoggingModel.message.contains("COMPLETED") # type: ignore + ) + process_instance_query = process_instance_query.filter( + SpiffLoggingModel.spiff_step == SpiffStepDetailsModel.spiff_step + ) + process_instance_query = process_instance_query.filter( + SpiffStepDetailsModel.completed_by_user_id == g.user.id + ) + + if report_filter.with_tasks_completed_by_my_group is True: + process_instance_query = process_instance_query.filter( + ProcessInstanceModel.status.in_(["complete", "error", "terminated"]) # type: ignore + ) + process_instance_query = process_instance_query.join( + SpiffStepDetailsModel, + ProcessInstanceModel.id == SpiffStepDetailsModel.process_instance_id, + ) + process_instance_query = process_instance_query.join( + SpiffLoggingModel, + ProcessInstanceModel.id == SpiffLoggingModel.process_instance_id, + ) + process_instance_query = process_instance_query.filter( + SpiffLoggingModel.message.contains("COMPLETED") # type: ignore + ) + process_instance_query = process_instance_query.filter( + SpiffLoggingModel.spiff_step == SpiffStepDetailsModel.spiff_step + ) + process_instance_query = process_instance_query.join( + GroupModel, + GroupModel.id == SpiffStepDetailsModel.lane_assignment_id, + ) + process_instance_query = process_instance_query.join( + UserGroupAssignmentModel, + UserGroupAssignmentModel.group_id == GroupModel.id, + ) + process_instance_query = process_instance_query.filter( + UserGroupAssignmentModel.user_id == g.user.id + ) + + instance_metadata_aliases = {} + stock_columns = ProcessInstanceReportService.get_column_names_for_model( + ProcessInstanceModel + ) + for column in process_instance_report.report_metadata["columns"]: + if column["accessor"] in stock_columns: + continue + instance_metadata_alias = aliased(ProcessInstanceMetadataModel) + instance_metadata_aliases[column["accessor"]] = instance_metadata_alias + + filter_for_column = None + if "filter_by" in process_instance_report.report_metadata: + filter_for_column = next( + ( + f + for f in process_instance_report.report_metadata["filter_by"] + if f["field_name"] == column["accessor"] + ), + None, + ) + isouter = True + conditions = [ + ProcessInstanceModel.id == instance_metadata_alias.process_instance_id, + instance_metadata_alias.key == column["accessor"], + ] + if filter_for_column: + isouter = False + conditions.append( + instance_metadata_alias.value == filter_for_column["field_value"] + ) + process_instance_query = process_instance_query.join( + instance_metadata_alias, and_(*conditions), isouter=isouter + ).add_columns(func.max(instance_metadata_alias.value).label(column["accessor"])) + + order_by_query_array = [] + order_by_array = process_instance_report.report_metadata["order_by"] + if len(order_by_array) < 1: + order_by_array = ProcessInstanceReportModel.default_order_by() + for order_by_option in order_by_array: + attribute = re.sub("^-", "", order_by_option) + if attribute in stock_columns: + if order_by_option.startswith("-"): + order_by_query_array.append( + getattr(ProcessInstanceModel, attribute).desc() + ) + else: + order_by_query_array.append( + getattr(ProcessInstanceModel, attribute).asc() + ) + elif attribute in instance_metadata_aliases: + if order_by_option.startswith("-"): + order_by_query_array.append( + instance_metadata_aliases[attribute].value.desc() + ) + else: + order_by_query_array.append( + instance_metadata_aliases[attribute].value.asc() + ) + + process_instances = ( + process_instance_query.group_by(ProcessInstanceModel.id) + .add_columns(ProcessInstanceModel.id) + .order_by(*order_by_query_array) + .paginate(page=page, per_page=per_page, error_out=False) + ) + + results = ProcessInstanceReportService.add_metadata_columns_to_process_instance( + process_instances.items, process_instance_report.report_metadata["columns"] ) - report_metadata = process_instance_report.report_metadata response_json = { - "report_metadata": report_metadata, + "report": process_instance_report, "results": results, "filters": report_filter.to_dict(), "pagination": { @@ -830,6 +1056,22 @@ def process_instance_list( return make_response(jsonify(response_json), 200) +def process_instance_report_column_list() -> flask.wrappers.Response: + """Process_instance_report_column_list.""" + table_columns = ProcessInstanceReportService.builtin_column_options() + columns_for_metadata = ( + db.session.query(ProcessInstanceMetadataModel.key) + .order_by(ProcessInstanceMetadataModel.key) + .distinct() # type: ignore + .all() + ) + columns_for_metadata_strings = [ + {"Header": i[0], "accessor": i[0], "filterable": True} + for i in columns_for_metadata + ] + return make_response(jsonify(table_columns + columns_for_metadata_strings), 200) + + def process_instance_show( modified_process_model_identifier: str, process_instance_id: int ) -> flask.wrappers.Response: @@ -846,7 +1088,7 @@ def process_instance_show( ): bpmn_xml_file_contents = SpecFileService.get_data( process_model, process_model.primary_file_name - ) + ).decode("utf-8") else: bpmn_xml_file_contents = GitService.get_instance_file_contents_for_revision( process_model, process_instance.bpmn_version_control_identifier @@ -856,7 +1098,9 @@ def process_instance_show( return make_response(jsonify(process_instance), 200) -def process_instance_delete(process_instance_id: int) -> flask.wrappers.Response: +def process_instance_delete( + process_instance_id: int, modified_process_model_identifier: str +) -> flask.wrappers.Response: """Create_process_instance.""" process_instance = find_process_instance_by_id_or_raise(process_instance_id) @@ -886,22 +1130,22 @@ def process_instance_report_list( def process_instance_report_create(body: Dict[str, Any]) -> flask.wrappers.Response: """Process_instance_report_create.""" - ProcessInstanceReportModel.create_report( + process_instance_report = ProcessInstanceReportModel.create_report( identifier=body["identifier"], user=g.user, report_metadata=body["report_metadata"], ) - return Response(json.dumps({"ok": True}), status=200, mimetype="application/json") + return make_response(jsonify(process_instance_report), 201) def process_instance_report_update( - report_identifier: str, + report_id: int, body: Dict[str, Any], ) -> flask.wrappers.Response: """Process_instance_report_create.""" process_instance_report = ProcessInstanceReportModel.query.filter_by( - identifier=report_identifier, + id=report_id, created_by_id=g.user.id, ).first() if process_instance_report is None: @@ -914,15 +1158,15 @@ def process_instance_report_update( process_instance_report.report_metadata = body["report_metadata"] db.session.commit() - return Response(json.dumps({"ok": True}), status=200, mimetype="application/json") + return make_response(jsonify(process_instance_report), 201) def process_instance_report_delete( - report_identifier: str, + report_id: int, ) -> flask.wrappers.Response: """Process_instance_report_create.""" process_instance_report = ProcessInstanceReportModel.query.filter_by( - identifier=report_identifier, + id=report_id, created_by_id=g.user.id, ).first() if process_instance_report is None: @@ -938,11 +1182,9 @@ def process_instance_report_delete( return Response(json.dumps({"ok": True}), status=200, mimetype="application/json") -def service_tasks_show() -> flask.wrappers.Response: - """Service_tasks_show.""" +def service_task_list() -> flask.wrappers.Response: + """Service_task_list.""" available_connectors = ServiceTaskService.available_connectors() - print(available_connectors) - return Response( json.dumps(available_connectors), status=200, mimetype="application/json" ) @@ -971,24 +1213,22 @@ def authentication_callback( f"{service}/{auth_method}", response, g.user.id, create_if_not_exists=True ) return redirect( - f"{current_app.config['SPIFFWORKFLOW_FRONTEND_URL']}/admin/authentications" + f"{current_app.config['SPIFFWORKFLOW_FRONTEND_URL']}/admin/configuration" ) def process_instance_report_show( - report_identifier: str, + report_id: int, page: int = 1, per_page: int = 100, ) -> flask.wrappers.Response: - """Process_instance_list.""" - process_instances = ProcessInstanceModel.query.order_by( # .filter_by(process_model_identifier=process_model.id) + """Process_instance_report_show.""" + process_instances = ProcessInstanceModel.query.order_by( ProcessInstanceModel.start_in_seconds.desc(), ProcessInstanceModel.id.desc() # type: ignore - ).paginate( - page=page, per_page=per_page, error_out=False - ) + ).paginate(page=page, per_page=per_page, error_out=False) process_instance_report = ProcessInstanceReportModel.query.filter_by( - identifier=report_identifier, + id=report_id, created_by_id=g.user.id, ).first() if process_instance_report is None: @@ -1026,7 +1266,7 @@ def task_list_my_tasks(page: int = 1, per_page: int = 100) -> flask.wrappers.Res # just need this add_columns to add the process_model_identifier. Then add everything back that was removed. .add_columns( ProcessInstanceModel.process_model_identifier, - ProcessInstanceModel.process_group_identifier, + ProcessInstanceModel.process_model_display_name, ProcessInstanceModel.status, ActiveTaskModel.task_name, ActiveTaskModel.task_title, @@ -1150,7 +1390,7 @@ def get_tasks( def process_instance_task_list( - modified_process_model_id: str, + modified_process_model_identifier: str, process_instance_id: int, all_tasks: bool = False, spiff_step: int = 0, @@ -1167,8 +1407,10 @@ def process_instance_task_list( ) .first() ) - if step_detail is not None: - process_instance.bpmn_json = json.dumps(step_detail.task_json) + if step_detail is not None and process_instance.bpmn_json is not None: + bpmn_json = json.loads(process_instance.bpmn_json) + bpmn_json["tasks"] = step_detail.task_json + process_instance.bpmn_json = json.dumps(bpmn_json) processor = ProcessInstanceProcessor(process_instance) @@ -1263,9 +1505,6 @@ def task_show(process_instance_id: int, task_id: str) -> flask.wrappers.Response task.form_ui_schema = ui_form_contents if task.properties and task.data and "instructionsForEndUser" in task.properties: - print( - f"task.properties['instructionsForEndUser']: {task.properties['instructionsForEndUser']}" - ) if task.properties["instructionsForEndUser"]: task.properties["instructionsForEndUser"] = render_jinja_template( task.properties["instructionsForEndUser"], task.data @@ -1288,7 +1527,7 @@ def task_submit( task_id, process_instance, processor=processor ) AuthorizationService.assert_user_can_complete_spiff_task( - processor, spiff_task, principal.user + process_instance.id, spiff_task, principal.user ) if spiff_task.state != TaskState.READY: @@ -1473,7 +1712,7 @@ def get_process_model(process_model_id: str) -> ProcessModelInfo: """Get_process_model.""" process_model = None try: - process_model = ProcessModelService().get_process_model(process_model_id) + process_model = ProcessModelService.get_process_model(process_model_id) except ProcessEntityNotFoundError as exception: raise ( ApiError( @@ -1580,9 +1819,26 @@ def get_spiff_task_from_process_instance( return spiff_task +# sample body: +# {"ref": "refs/heads/main", "repository": {"name": "sample-process-models", +# "full_name": "sartography/sample-process-models", "private": False .... }} +# test with: ngrok http 7000 +# where 7000 is the port the app is running on locally +def github_webhook_receive(body: Dict) -> Response: + """Github_webhook_receive.""" + auth_header = request.headers.get("X-Hub-Signature-256") + AuthorizationService.verify_sha256_token(auth_header) + result = GitService.handle_web_hook(body) + return Response( + json.dumps({"git_pull": result}), status=200, mimetype="application/json" + ) + + # # Methods for secrets CRUD - maybe move somewhere else: # + + def get_secret(key: str) -> Optional[str]: """Get_secret.""" return SecretService.get_secret(key) @@ -1712,7 +1968,12 @@ def _update_form_schema_with_task_data_as_needed( _update_form_schema_with_task_data_as_needed(o, task_data) -def update_task_data(process_instance_id: str, task_id: str, body: Dict) -> Response: +def update_task_data( + process_instance_id: str, + modified_process_model_identifier: str, + task_id: str, + body: Dict, +) -> Response: """Update task data.""" process_instance = ProcessInstanceModel.query.filter( ProcessInstanceModel.id == int(process_instance_id) diff --git a/src/spiffworkflow_backend/routes/user.py b/src/spiffworkflow_backend/routes/user.py index 5fe10e0a..2bbbc137 100644 --- a/src/spiffworkflow_backend/routes/user.py +++ b/src/spiffworkflow_backend/routes/user.py @@ -1,6 +1,7 @@ """User.""" import ast import base64 +import json from typing import Any from typing import Dict from typing import Optional @@ -58,7 +59,6 @@ def verify_token( decoded_token = get_decoded_token(token) if decoded_token is not None: - if "token_type" in decoded_token: token_type = decoded_token["token_type"] if token_type == "internal": # noqa: S105 @@ -68,11 +68,11 @@ def verify_token( current_app.logger.error( f"Exception in verify_token getting user from decoded internal token. {e}" ) - elif "iss" in decoded_token.keys(): try: - user_info = AuthenticationService.get_user_info_from_open_id(token) - except ApiError as ae: + if AuthenticationService.validate_id_token(token): + user_info = decoded_token + except ApiError as ae: # API Error is only thrown in the token is outdated. # Try to refresh the token user = UserService.get_user_by_service_and_service_id( "open_id", decoded_token["sub"] @@ -86,14 +86,9 @@ def verify_token( ) ) if auth_token and "error" not in auth_token: - # redirect to original url, with auth_token? - user_info = ( - AuthenticationService.get_user_info_from_open_id( - auth_token["access_token"] - ) - ) - if not user_info: - raise ae + # We have the user, but this code is a bit convoluted, and will later demand + # a user_info object so it can look up the user. Sorry to leave this crap here. + user_info = {"sub": user.service_id} else: raise ae else: @@ -203,6 +198,18 @@ def login(redirect_url: str = "/") -> Response: return redirect(login_redirect_url) +def parse_id_token(token: str) -> Any: + """Parse the id token.""" + parts = token.split(".") + if len(parts) != 3: + raise Exception("Incorrect id token format") + + payload = parts[1] + padded = payload + "=" * (4 - len(payload) % 4) + decoded = base64.b64decode(padded) + return json.loads(decoded) + + def login_return(code: str, state: str, session_state: str) -> Optional[Response]: """Login_return.""" state_dict = ast.literal_eval(base64.b64decode(state).decode("utf-8")) @@ -211,10 +218,9 @@ def login_return(code: str, state: str, session_state: str) -> Optional[Response if "id_token" in auth_token_object: id_token = auth_token_object["id_token"] + user_info = parse_id_token(id_token) + if AuthenticationService.validate_id_token(id_token): - user_info = AuthenticationService.get_user_info_from_open_id( - auth_token_object["access_token"] - ) if user_info and "error" not in user_info: user_model = AuthorizationService.create_user_from_sign_in(user_info) g.user = user_model.id @@ -332,15 +338,11 @@ def get_user_from_decoded_internal_token(decoded_token: dict) -> Optional[UserMo .filter(UserModel.service_id == service_id) .first() ) - # user: UserModel = UserModel.query.filter() if user: return user user = UserModel( username=service_id, - uid=service_id, service=service, service_id=service_id, - name="API User", ) - return user diff --git a/src/spiffworkflow_backend/scripts/add_user_to_group.py b/src/spiffworkflow_backend/scripts/add_user_to_group.py new file mode 100644 index 00000000..d3c77711 --- /dev/null +++ b/src/spiffworkflow_backend/scripts/add_user_to_group.py @@ -0,0 +1,43 @@ +"""Get_env.""" +from typing import Any + +from spiffworkflow_backend.models.group import GroupModel +from spiffworkflow_backend.models.group import GroupNotFoundError +from spiffworkflow_backend.models.script_attributes_context import ( + ScriptAttributesContext, +) +from spiffworkflow_backend.models.user import UserModel +from spiffworkflow_backend.models.user import UserNotFoundError +from spiffworkflow_backend.scripts.script import Script +from spiffworkflow_backend.services.user_service import UserService + + +class AddUserToGroup(Script): + """AddUserToGroup.""" + + def get_description(self) -> str: + """Get_description.""" + return """Add a given user to a given group.""" + + def run( + self, + script_attributes_context: ScriptAttributesContext, + *args: Any, + **kwargs: Any, + ) -> Any: + """Run.""" + username = args[0] + group_identifier = args[1] + user = UserModel.query.filter_by(username=username).first() + if user is None: + raise UserNotFoundError( + f"Script 'add_user_to_group' could not find a user with username: {username}" + ) + + group = GroupModel.query.filter_by(identifier=group_identifier).first() + if group is None: + raise GroupNotFoundError( + f"Script 'add_user_to_group' could not find group with identifier '{group_identifier}'." + ) + + UserService.add_user_to_group(user, group) diff --git a/src/spiffworkflow_backend/scripts/save_process_instance_metadata.py b/src/spiffworkflow_backend/scripts/save_process_instance_metadata.py new file mode 100644 index 00000000..d9c1959a --- /dev/null +++ b/src/spiffworkflow_backend/scripts/save_process_instance_metadata.py @@ -0,0 +1,42 @@ +"""Save process instance metadata.""" +from typing import Any + +from flask_bpmn.models.db import db + +from spiffworkflow_backend.models.process_instance_metadata import ( + ProcessInstanceMetadataModel, +) +from spiffworkflow_backend.models.script_attributes_context import ( + ScriptAttributesContext, +) +from spiffworkflow_backend.scripts.script import Script + + +class SaveProcessInstanceMetadata(Script): + """SaveProcessInstanceMetadata.""" + + def get_description(self) -> str: + """Get_description.""" + return """Save a given dict as process instance metadata (useful for creating reports).""" + + def run( + self, + script_attributes_context: ScriptAttributesContext, + *args: Any, + **kwargs: Any, + ) -> Any: + """Run.""" + metadata_dict = args[0] + for key, value in metadata_dict.items(): + pim = ProcessInstanceMetadataModel.query.filter_by( + process_instance_id=script_attributes_context.process_instance_id, + key=key, + ).first() + if pim is None: + pim = ProcessInstanceMetadataModel( + process_instance_id=script_attributes_context.process_instance_id, + key=key, + ) + pim.value = value + db.session.add(pim) + db.session.commit() diff --git a/src/spiffworkflow_backend/services/acceptance_test_fixtures.py b/src/spiffworkflow_backend/services/acceptance_test_fixtures.py index cfea3148..81488910 100644 --- a/src/spiffworkflow_backend/services/acceptance_test_fixtures.py +++ b/src/spiffworkflow_backend/services/acceptance_test_fixtures.py @@ -1,5 +1,4 @@ """Acceptance_test_fixtures.""" -import json import time from flask import current_app @@ -8,13 +7,15 @@ from tests.spiffworkflow_backend.helpers.base_test import BaseTest from spiffworkflow_backend.models.process_instance import ProcessInstanceModel from spiffworkflow_backend.models.process_instance import ProcessInstanceStatus +from spiffworkflow_backend.services.process_instance_service import ( + ProcessInstanceService, +) def load_acceptance_test_fixtures() -> list[ProcessInstanceModel]: """Load_fixtures.""" current_app.logger.debug("load_acceptance_test_fixtures() start") - test_process_group_id = "" - test_process_model_id = "acceptance-tests-group-one/acceptance-tests-model-1" + test_process_model_id = "misc/acceptance-tests-group-one/acceptance-tests-model-1" user = BaseTest.find_or_create_user() statuses = ProcessInstanceStatus.list() current_time = round(time.time()) @@ -28,16 +29,13 @@ def load_acceptance_test_fixtures() -> list[ProcessInstanceModel]: # suspended - 6 hours ago process_instances = [] for i in range(len(statuses)): - process_instance = ProcessInstanceModel( - status=statuses[i], - process_initiator=user, - process_model_identifier=test_process_model_id, - process_group_identifier=test_process_group_id, - updated_at_in_seconds=round(time.time()), - start_in_seconds=current_time - (3600 * i), - end_in_seconds=current_time - (3600 * i - 20), - bpmn_json=json.dumps({"i": i}), + + process_instance = ProcessInstanceService.create_process_instance_from_process_model_identifier( + test_process_model_id, user ) + process_instance.status = statuses[i] + process_instance.start_in_seconds = current_time - (3600 * i) + process_instance.end_in_seconds = current_time - (3600 * i - 20) db.session.add(process_instance) process_instances.append(process_instance) diff --git a/src/spiffworkflow_backend/services/authentication_service.py b/src/spiffworkflow_backend/services/authentication_service.py index 18f08d0f..f4bd357b 100644 --- a/src/spiffworkflow_backend/services/authentication_service.py +++ b/src/spiffworkflow_backend/services/authentication_service.py @@ -26,58 +26,35 @@ class AuthenticationProviderTypes(enum.Enum): class AuthenticationService: """AuthenticationService.""" + ENDPOINT_CACHE: dict = ( + {} + ) # We only need to find the openid endpoints once, then we can cache them. + @staticmethod - def get_open_id_args() -> tuple: - """Get_open_id_args.""" - open_id_server_url = current_app.config["OPEN_ID_SERVER_URL"] - open_id_client_id = current_app.config["OPEN_ID_CLIENT_ID"] - open_id_realm_name = current_app.config["OPEN_ID_REALM_NAME"] - open_id_client_secret_key = current_app.config[ - "OPEN_ID_CLIENT_SECRET_KEY" - ] # noqa: S105 - return ( - open_id_server_url, - open_id_client_id, - open_id_realm_name, - open_id_client_secret_key, - ) + def client_id() -> str: + """Returns the client id from the config.""" + return current_app.config.get("OPEN_ID_CLIENT_ID", "") + + @staticmethod + def server_url() -> str: + """Returns the server url from the config.""" + return current_app.config.get("OPEN_ID_SERVER_URL", "") + + @staticmethod + def secret_key() -> str: + """Returns the secret key from the config.""" + return current_app.config.get("OPEN_ID_CLIENT_SECRET_KEY", "") @classmethod - def get_user_info_from_open_id(cls, token: str) -> dict: - """The token is an auth_token.""" - ( - open_id_server_url, - open_id_client_id, - open_id_realm_name, - open_id_client_secret_key, - ) = cls.get_open_id_args() - - headers = {"Authorization": f"Bearer {token}"} - - request_url = f"{open_id_server_url}/realms/{open_id_realm_name}/protocol/openid-connect/userinfo" - try: - request_response = requests.get(request_url, headers=headers) - except Exception as e: - current_app.logger.error(f"Exception in get_user_info_from_id_token: {e}") - raise ApiError( - error_code="token_error", - message=f"Exception in get_user_info_from_id_token: {e}", - status_code=401, - ) from e - - if request_response.status_code == 401: - raise ApiError( - error_code="invalid_token", message="Please login", status_code=401 - ) - elif request_response.status_code == 200: - user_info: dict = json.loads(request_response.text) - return user_info - - raise ApiError( - error_code="user_info_error", - message="Cannot get user info in get_user_info_from_id_token", - status_code=401, - ) + def open_id_endpoint_for_name(cls, name: str) -> str: + """All openid systems provide a mapping of static names to the full path of that endpoint.""" + if name not in AuthenticationService.ENDPOINT_CACHE: + request_url = f"{cls.server_url()}/.well-known/openid-configuration" + response = requests.get(request_url) + AuthenticationService.ENDPOINT_CACHE = response.json() + if name not in AuthenticationService.ENDPOINT_CACHE: + raise Exception(f"Unknown OpenID Endpoint: {name}") + return AuthenticationService.ENDPOINT_CACHE.get(name, "") @staticmethod def get_backend_url() -> str: @@ -87,17 +64,10 @@ class AuthenticationService: def logout(self, id_token: str, redirect_url: Optional[str] = None) -> Response: """Logout.""" if redirect_url is None: - redirect_url = "/" - return_redirect_url = f"{self.get_backend_url()}/v1.0/logout_return" - ( - open_id_server_url, - open_id_client_id, - open_id_realm_name, - open_id_client_secret_key, - ) = AuthenticationService.get_open_id_args() + redirect_url = f"{self.get_backend_url()}/v1.0/logout_return" request_url = ( - f"{open_id_server_url}/realms/{open_id_realm_name}/protocol/openid-connect/logout?" - + f"post_logout_redirect_uri={return_redirect_url}&" + self.open_id_endpoint_for_name("end_session_endpoint") + + f"?post_logout_redirect_uri={redirect_url}&" + f"id_token_hint={id_token}" ) @@ -113,18 +83,12 @@ class AuthenticationService: self, state: str, redirect_url: str = "/v1.0/login_return" ) -> str: """Get_login_redirect_url.""" - ( - open_id_server_url, - open_id_client_id, - open_id_realm_name, - open_id_client_secret_key, - ) = AuthenticationService.get_open_id_args() return_redirect_url = f"{self.get_backend_url()}{redirect_url}" login_redirect_url = ( - f"{open_id_server_url}/realms/{open_id_realm_name}/protocol/openid-connect/auth?" - + f"state={state}&" + self.open_id_endpoint_for_name("authorization_endpoint") + + f"?state={state}&" + "response_type=code&" - + f"client_id={open_id_client_id}&" + + f"client_id={self.client_id()}&" + "scope=openid&" + f"redirect_uri={return_redirect_url}" ) @@ -134,14 +98,7 @@ class AuthenticationService: self, code: str, redirect_url: str = "/v1.0/login_return" ) -> dict: """Get_auth_token_object.""" - ( - open_id_server_url, - open_id_client_id, - open_id_realm_name, - open_id_client_secret_key, - ) = AuthenticationService.get_open_id_args() - - backend_basic_auth_string = f"{open_id_client_id}:{open_id_client_secret_key}" + backend_basic_auth_string = f"{self.client_id()}:{self.secret_key()}" backend_basic_auth_bytes = bytes(backend_basic_auth_string, encoding="ascii") backend_basic_auth = base64.b64encode(backend_basic_auth_bytes) headers = { @@ -154,7 +111,7 @@ class AuthenticationService: "redirect_uri": f"{self.get_backend_url()}{redirect_url}", } - request_url = f"{open_id_server_url}/realms/{open_id_realm_name}/protocol/openid-connect/token" + request_url = self.open_id_endpoint_for_name("token_endpoint") response = requests.post(request_url, data=data, headers=headers) auth_token_object: dict = json.loads(response.text) @@ -165,12 +122,6 @@ class AuthenticationService: """Https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation.""" valid = True now = time.time() - ( - open_id_server_url, - open_id_client_id, - open_id_realm_name, - open_id_client_secret_key, - ) = cls.get_open_id_args() try: decoded_token = jwt.decode(id_token, options={"verify_signature": False}) except Exception as e: @@ -179,15 +130,15 @@ class AuthenticationService: message="Cannot decode id_token", status_code=401, ) from e - if decoded_token["iss"] != f"{open_id_server_url}/realms/{open_id_realm_name}": + if decoded_token["iss"] != cls.server_url(): valid = False elif ( - open_id_client_id not in decoded_token["aud"] + cls.client_id() not in decoded_token["aud"] and "account" not in decoded_token["aud"] ): valid = False elif "azp" in decoded_token and decoded_token["azp"] not in ( - open_id_client_id, + cls.client_id(), "account", ): valid = False @@ -235,20 +186,14 @@ class AuthenticationService: refresh_token_object: RefreshTokenModel = RefreshTokenModel.query.filter( RefreshTokenModel.user_id == user_id ).first() - assert refresh_token_object # noqa: S101 - return refresh_token_object.token + if refresh_token_object: + return refresh_token_object.token + return None @classmethod def get_auth_token_from_refresh_token(cls, refresh_token: str) -> dict: - """Get a new auth_token from a refresh_token.""" - ( - open_id_server_url, - open_id_client_id, - open_id_realm_name, - open_id_client_secret_key, - ) = cls.get_open_id_args() - - backend_basic_auth_string = f"{open_id_client_id}:{open_id_client_secret_key}" + """Converts a refresh token to an Auth Token by calling the openid's auth endpoint.""" + backend_basic_auth_string = f"{cls.client_id()}:{cls.secret_key()}" backend_basic_auth_bytes = bytes(backend_basic_auth_string, encoding="ascii") backend_basic_auth = base64.b64encode(backend_basic_auth_bytes) headers = { @@ -259,11 +204,11 @@ class AuthenticationService: data = { "grant_type": "refresh_token", "refresh_token": refresh_token, - "client_id": open_id_client_id, - "client_secret": open_id_client_secret_key, + "client_id": cls.client_id(), + "client_secret": cls.secret_key(), } - request_url = f"{open_id_server_url}/realms/{open_id_realm_name}/protocol/openid-connect/token" + request_url = cls.open_id_endpoint_for_name("token_endpoint") response = requests.post(request_url, data=data, headers=headers) auth_token_object: dict = json.loads(response.text) diff --git a/src/spiffworkflow_backend/services/authorization_service.py b/src/spiffworkflow_backend/services/authorization_service.py index 29ee7884..9456f8f1 100644 --- a/src/spiffworkflow_backend/services/authorization_service.py +++ b/src/spiffworkflow_backend/services/authorization_service.py @@ -1,5 +1,9 @@ """Authorization_service.""" +import inspect import re +from hashlib import sha256 +from hmac import compare_digest +from hmac import HMAC from typing import Optional from typing import Union @@ -8,6 +12,7 @@ import yaml from flask import current_app from flask import g from flask import request +from flask import scaffold from flask_bpmn.api.api_error import ApiError from flask_bpmn.models.db import db from SpiffWorkflow.task import Task as SpiffTask # type: ignore @@ -23,10 +28,8 @@ from spiffworkflow_backend.models.principal import PrincipalModel from spiffworkflow_backend.models.user import UserModel from spiffworkflow_backend.models.user import UserNotFoundError from spiffworkflow_backend.models.user_group_assignment import UserGroupAssignmentModel +from spiffworkflow_backend.routes.openid_blueprint import openid_blueprint from spiffworkflow_backend.services.group_service import GroupService -from spiffworkflow_backend.services.process_instance_processor import ( - ProcessInstanceProcessor, -) from spiffworkflow_backend.services.user_service import UserService @@ -45,6 +48,27 @@ class UserDoesNotHaveAccessToTaskError(Exception): class AuthorizationService: """Determine whether a user has permission to perform their request.""" + # https://stackoverflow.com/a/71320673/6090676 + @classmethod + def verify_sha256_token(cls, auth_header: Optional[str]) -> None: + """Verify_sha256_token.""" + if auth_header is None: + raise ApiError( + error_code="unauthorized", + message="", + status_code=403, + ) + + received_sign = auth_header.split("sha256=")[-1].strip() + secret = current_app.config["GITHUB_WEBHOOK_SECRET"].encode() + expected_sign = HMAC(key=secret, msg=request.data, digestmod=sha256).hexdigest() + if not compare_digest(received_sign, expected_sign): + raise ApiError( + error_code="unauthorized", + message="", + status_code=403, + ) + @classmethod def has_permission( cls, principals: list[PrincipalModel], permission: str, target_uri: str @@ -232,7 +256,11 @@ class AuthorizationService: def should_disable_auth_for_request(cls) -> bool: """Should_disable_auth_for_request.""" swagger_functions = ["get_json_spec"] - authentication_exclusion_list = ["status", "authentication_callback"] + authentication_exclusion_list = [ + "status", + "authentication_callback", + "github_webhook_receive", + ] if request.method == "OPTIONS": return True @@ -244,6 +272,7 @@ class AuthorizationService: return True api_view_function = current_app.view_functions[request.endpoint] + module = inspect.getmodule(api_view_function) if ( api_view_function and api_view_function.__name__.startswith("login") @@ -251,6 +280,8 @@ class AuthorizationService: or api_view_function.__name__.startswith("console_ui_") or api_view_function.__name__ in authentication_exclusion_list or api_view_function.__name__ in swagger_functions + or module == openid_blueprint + or module == scaffold # don't check permissions for static assets ): return True @@ -393,25 +424,25 @@ class AuthorizationService: @staticmethod def assert_user_can_complete_spiff_task( - processor: ProcessInstanceProcessor, + process_instance_id: int, spiff_task: SpiffTask, user: UserModel, ) -> bool: """Assert_user_can_complete_spiff_task.""" active_task = ActiveTaskModel.query.filter_by( task_name=spiff_task.task_spec.name, - process_instance_id=processor.process_instance_model.id, + process_instance_id=process_instance_id, ).first() if active_task is None: raise ActiveTaskNotFoundError( f"Could find an active task with task name '{spiff_task.task_spec.name}'" - f" for process instance '{processor.process_instance_model.id}'" + f" for process instance '{process_instance_id}'" ) if user not in active_task.potential_owners: raise UserDoesNotHaveAccessToTaskError( f"User {user.username} does not have access to update task'{spiff_task.task_spec.name}'" - f" for process instance '{processor.process_instance_model.id}'" + f" for process instance '{process_instance_id}'" ) return True diff --git a/src/spiffworkflow_backend/services/background_processing_service.py b/src/spiffworkflow_backend/services/background_processing_service.py index 08a2b02d..1771c2c8 100644 --- a/src/spiffworkflow_backend/services/background_processing_service.py +++ b/src/spiffworkflow_backend/services/background_processing_service.py @@ -14,7 +14,7 @@ class BackgroundProcessingService: """__init__.""" self.app = app - def run(self) -> None: + def process_waiting_process_instances(self) -> None: """Since this runs in a scheduler, we need to specify the app context as well.""" with self.app.app_context(): ProcessInstanceService.do_waiting() diff --git a/src/spiffworkflow_backend/services/data_setup_service.py b/src/spiffworkflow_backend/services/data_setup_service.py index 412c4b82..c9c0647e 100644 --- a/src/spiffworkflow_backend/services/data_setup_service.py +++ b/src/spiffworkflow_backend/services/data_setup_service.py @@ -26,7 +26,7 @@ class DataSetupService: current_app.logger.debug("DataSetupService.save_all_process_models() start") failing_process_models = [] - process_models = ProcessModelService().get_process_models() + process_models = ProcessModelService.get_process_models(recursive=True) SpecFileService.clear_caches() for process_model in process_models: current_app.logger.debug(f"Process Model: {process_model.display_name}") diff --git a/src/spiffworkflow_backend/services/error_handling_service.py b/src/spiffworkflow_backend/services/error_handling_service.py index 99e4fbe8..1e8b38f2 100644 --- a/src/spiffworkflow_backend/services/error_handling_service.py +++ b/src/spiffworkflow_backend/services/error_handling_service.py @@ -34,7 +34,7 @@ class ErrorHandlingService: self, _processor: ProcessInstanceProcessor, _error: Union[ApiError, Exception] ) -> None: """On unhandled exceptions, set instance.status based on model.fault_or_suspend_on_exception.""" - process_model = ProcessModelService().get_process_model( + process_model = ProcessModelService.get_process_model( _processor.process_model_identifier ) if process_model.fault_or_suspend_on_exception == "suspend": diff --git a/src/spiffworkflow_backend/services/file_system_service.py b/src/spiffworkflow_backend/services/file_system_service.py index cbe007d6..a2a9181d 100644 --- a/src/spiffworkflow_backend/services/file_system_service.py +++ b/src/spiffworkflow_backend/services/file_system_service.py @@ -1,6 +1,8 @@ """File_system_service.""" import os +from contextlib import contextmanager from datetime import datetime +from typing import Generator from typing import List from typing import Optional @@ -23,18 +25,40 @@ class FileSystemService: PROCESS_GROUP_JSON_FILE = "process_group.json" PROCESS_MODEL_JSON_FILE = "process_model.json" + # https://stackoverflow.com/a/24176022/6090676 + @staticmethod + @contextmanager + def cd(newdir: str) -> Generator: + """Cd.""" + prevdir = os.getcwd() + os.chdir(os.path.expanduser(newdir)) + try: + yield + finally: + os.chdir(prevdir) + @staticmethod def root_path() -> str: """Root_path.""" # fixme: allow absolute files dir_name = current_app.config["BPMN_SPEC_ABSOLUTE_DIR"] app_root = current_app.root_path - return os.path.join(app_root, "..", dir_name) + return os.path.abspath(os.path.join(app_root, "..", dir_name)) + + @staticmethod + def id_string_to_relative_path(id_string: str) -> str: + """Id_string_to_relative_path.""" + return id_string.replace("/", os.sep) @staticmethod def process_group_path(name: str) -> str: """Category_path.""" - return os.path.abspath(os.path.join(FileSystemService.root_path(), name)) + return os.path.abspath( + os.path.join( + FileSystemService.root_path(), + FileSystemService.id_string_to_relative_path(name), + ) + ) @staticmethod def full_path_from_relative_path(relative_path: str) -> str: diff --git a/src/spiffworkflow_backend/services/git_service.py b/src/spiffworkflow_backend/services/git_service.py index 815e4cad..f187a47c 100644 --- a/src/spiffworkflow_backend/services/git_service.py +++ b/src/spiffworkflow_backend/services/git_service.py @@ -1,56 +1,246 @@ """Git_service.""" import os +import shutil +import subprocess # noqa we need the subprocess module to safely run the git commands +import uuid +from typing import Optional +from typing import Union from flask import current_app +from flask import g +from spiffworkflow_backend.config import ConfigurationError from spiffworkflow_backend.models.process_model import ProcessModelInfo from spiffworkflow_backend.services.file_system_service import FileSystemService +class MissingGitConfigsError(Exception): + """MissingGitConfigsError.""" + + +class InvalidGitWebhookBodyError(Exception): + """InvalidGitWebhookBodyError.""" + + +class GitCloneUrlMismatchError(Exception): + """GitCloneUrlMismatchError.""" + + +class GitCommandError(Exception): + """GitCommandError.""" + + +# TOOD: check for the existence of git and configs on bootup if publishing is enabled class GitService: """GitService.""" - @staticmethod - def get_current_revision() -> str: + @classmethod + def get_current_revision(cls) -> str: """Get_current_revision.""" bpmn_spec_absolute_dir = current_app.config["BPMN_SPEC_ABSOLUTE_DIR"] # The value includes a carriage return character at the end, so we don't grab the last character - current_git_revision = os.popen( # noqa: S605 - f"cd {bpmn_spec_absolute_dir} && git rev-parse --short HEAD" - ).read()[ - :-1 - ] # noqa: S605 - return current_git_revision + with FileSystemService.cd(bpmn_spec_absolute_dir): + return cls.run_shell_command_to_get_stdout( + ["git", "rev-parse", "--short", "HEAD"] + ) - @staticmethod + @classmethod def get_instance_file_contents_for_revision( - process_model: ProcessModelInfo, revision: str - ) -> bytes: + cls, process_model: ProcessModelInfo, revision: str + ) -> str: """Get_instance_file_contents_for_revision.""" bpmn_spec_absolute_dir = current_app.config["BPMN_SPEC_ABSOLUTE_DIR"] process_model_relative_path = FileSystemService.process_model_relative_path( process_model ) - shell_cd_command = f"cd {bpmn_spec_absolute_dir}" - shell_git_command = f"git show {revision}:{process_model_relative_path}/{process_model.primary_file_name}" - shell_command = f"{shell_cd_command} && {shell_git_command}" - # git show 78ae5eb:category_number_one/script-task/script-task.bpmn - file_contents: str = os.popen(shell_command).read()[:-1] # noqa: S605 - assert file_contents # noqa: S101 - return file_contents.encode("utf-8") + with FileSystemService.cd(bpmn_spec_absolute_dir): + shell_command = [ + "git", + "show", + f"{revision}:{process_model_relative_path}/{process_model.primary_file_name}", + ] + return cls.run_shell_command_to_get_stdout(shell_command) - @staticmethod - def commit(message: str) -> str: + @classmethod + def commit(cls, message: str, repo_path: Optional[str] = None) -> str: """Commit.""" - bpmn_spec_absolute_dir = current_app.config["BPMN_SPEC_ABSOLUTE_DIR"] + repo_path_to_use = repo_path + if repo_path is None: + repo_path_to_use = current_app.config["BPMN_SPEC_ABSOLUTE_DIR"] + if repo_path_to_use is None: + raise ConfigurationError("BPMN_SPEC_ABSOLUTE_DIR config must be set") + git_username = "" git_email = "" - if ( - current_app.config["GIT_COMMIT_USERNAME"] - and current_app.config["GIT_COMMIT_EMAIL"] - ): - git_username = current_app.config["GIT_COMMIT_USERNAME"] - git_email = current_app.config["GIT_COMMIT_EMAIL"] - shell_command = f"./bin/git_commit_bpmn_models_repo '{bpmn_spec_absolute_dir}' '{message}' '{git_username}' '{git_email}'" - output = os.popen(shell_command).read() # noqa: S605 - return output + if current_app.config["GIT_USERNAME"] and current_app.config["GIT_USER_EMAIL"]: + git_username = current_app.config["GIT_USERNAME"] + git_email = current_app.config["GIT_USER_EMAIL"] + shell_command_path = os.path.join( + current_app.root_path, "..", "..", "bin", "git_commit_bpmn_models_repo" + ) + shell_command = [ + shell_command_path, + repo_path_to_use, + message, + git_username, + git_email, + ] + return cls.run_shell_command_to_get_stdout(shell_command) + + @classmethod + def check_for_configs(cls) -> None: + """Check_for_configs.""" + if current_app.config["GIT_BRANCH_TO_PUBLISH_TO"] is None: + raise MissingGitConfigsError( + "Missing config for GIT_BRANCH_TO_PUBLISH_TO. " + "This is required for publishing process models" + ) + if current_app.config["GIT_CLONE_URL_FOR_PUBLISHING"] is None: + raise MissingGitConfigsError( + "Missing config for GIT_CLONE_URL_FOR_PUBLISHING. " + "This is required for publishing process models" + ) + + @classmethod + def run_shell_command_as_boolean(cls, command: list[str]) -> bool: + """Run_shell_command_as_boolean.""" + # we know result will be a bool here + result: bool = cls.run_shell_command(command, return_success_state=True) # type: ignore + return result + + @classmethod + def run_shell_command_to_get_stdout(cls, command: list[str]) -> str: + """Run_shell_command_to_get_stdout.""" + # we know result will be a CompletedProcess here + result: subprocess.CompletedProcess[bytes] = cls.run_shell_command( + command, return_success_state=False + ) # type: ignore + return result.stdout.decode("utf-8") + + @classmethod + def run_shell_command( + cls, command: list[str], return_success_state: bool = False + ) -> Union[subprocess.CompletedProcess[bytes], bool]: + """Run_shell_command.""" + # this is fine since we pass the commands directly + result = subprocess.run(command, check=False, capture_output=True) # noqa + if return_success_state: + return result.returncode == 0 + + if result.returncode != 0: + stdout = result.stdout.decode("utf-8") + stderr = result.stderr.decode("utf-8") + raise GitCommandError( + f"Failed to execute git command: {command} " + f"Stdout: {stdout} " + f"Stderr: {stderr} " + ) + + return result + + # only supports github right now + @classmethod + def handle_web_hook(cls, webhook: dict) -> bool: + """Handle_web_hook.""" + cls.check_for_configs() + + if "repository" not in webhook or "clone_url" not in webhook["repository"]: + raise InvalidGitWebhookBodyError( + f"Cannot find required keys of 'repository:clone_url' from webhook body: {webhook}" + ) + + clone_url = webhook["repository"]["clone_url"] + if clone_url != current_app.config["GIT_CLONE_URL_FOR_PUBLISHING"]: + raise GitCloneUrlMismatchError( + f"Configured clone url does not match clone url from webhook: {clone_url}" + ) + + if "ref" not in webhook: + raise InvalidGitWebhookBodyError( + f"Could not find the 'ref' arg in the webhook boy: {webhook}" + ) + + if current_app.config["GIT_BRANCH"] is None: + raise MissingGitConfigsError( + "Missing config for GIT_BRANCH. " + "This is required for updating the repository as a result of the webhook" + ) + + ref = webhook["ref"] + git_branch = current_app.config["GIT_BRANCH"] + if ref != f"refs/heads/{git_branch}": + return False + + with FileSystemService.cd(current_app.config["BPMN_SPEC_ABSOLUTE_DIR"]): + cls.run_shell_command(["git", "pull"]) + return True + + @classmethod + def publish(cls, process_model_id: str, branch_to_update: str) -> str: + """Publish.""" + cls.check_for_configs() + source_process_model_root = FileSystemService.root_path() + source_process_model_path = os.path.join( + source_process_model_root, process_model_id + ) + unique_hex = uuid.uuid4().hex + clone_dir = f"sample-process-models.{unique_hex}" + + # clone new instance of sample-process-models, checkout branch_to_update + # we are adding a guid to this so the flake8 issue has been mitigated + destination_process_root = f"/tmp/{clone_dir}" # noqa + + git_clone_url = current_app.config["GIT_CLONE_URL_FOR_PUBLISHING"].replace( + "https://", + f"https://{current_app.config['GIT_USERNAME']}:{current_app.config['GIT_USER_PASSWORD']}@", + ) + cmd = ["git", "clone", git_clone_url, destination_process_root] + + cls.run_shell_command(cmd) + with FileSystemService.cd(destination_process_root): + # create publish branch from branch_to_update + cls.run_shell_command(["git", "checkout", branch_to_update]) + branch_to_pull_request = f"publish-{process_model_id}" + + # check if branch exists and checkout appropriately + command = [ + "git", + "show-ref", + "--verify", + f"refs/remotes/origin/{branch_to_pull_request}", + ] + if cls.run_shell_command_as_boolean(command): + cls.run_shell_command(["git", "checkout", branch_to_pull_request]) + else: + cls.run_shell_command(["git", "checkout", "-b", branch_to_pull_request]) + + # copy files from process model into the new publish branch + destination_process_model_path = os.path.join( + destination_process_root, process_model_id + ) + if os.path.exists(destination_process_model_path): + shutil.rmtree(destination_process_model_path) + shutil.copytree(source_process_model_path, destination_process_model_path) + + # add and commit files to branch_to_pull_request, then push + commit_message = ( + f"Request to publish changes to {process_model_id}, " + f"from {g.user.username} on {current_app.config['ENV_IDENTIFIER']}" + ) + cls.commit(commit_message, destination_process_root) + cls.run_shell_command( + ["git", "push", "--set-upstream", "origin", branch_to_pull_request] + ) + + # build url for github page to open PR + git_remote = cls.run_shell_command_to_get_stdout( + ["git", "config", "--get", "remote.origin.url"] + ) + remote_url = git_remote.strip().replace(".git", "") + pr_url = f"{remote_url}/compare/{branch_to_update}...{branch_to_pull_request}?expand=1" + + # try to clean up + if os.path.exists(destination_process_root): + shutil.rmtree(destination_process_root) + + return pr_url diff --git a/src/spiffworkflow_backend/services/logging_service.py b/src/spiffworkflow_backend/services/logging_service.py index b93e8665..dd34cb3f 100644 --- a/src/spiffworkflow_backend/services/logging_service.py +++ b/src/spiffworkflow_backend/services/logging_service.py @@ -216,7 +216,9 @@ class DBHandler(logging.Handler): bpmn_task_type = record.task_type if hasattr(record, "task_type") else None # type: ignore timestamp = record.created message = record.msg if hasattr(record, "msg") else None - current_user_id = record.current_user_id if hasattr(record, "current_user_id") else None # type: ignore + current_user_id = ( + record.current_user_id if hasattr(record, "current_user_id") else None # type: ignore + ) spiff_step = ( record.spiff_step # type: ignore if hasattr(record, "spiff_step") and record.spiff_step is not None # type: ignore diff --git a/src/spiffworkflow_backend/services/message_service.py b/src/spiffworkflow_backend/services/message_service.py index 216a66a5..cfb42c83 100644 --- a/src/spiffworkflow_backend/services/message_service.py +++ b/src/spiffworkflow_backend/services/message_service.py @@ -117,7 +117,7 @@ class MessageService: user: UserModel, ) -> ProcessInstanceModel: """Process_message_triggerable_process_model.""" - process_instance_receive = ProcessInstanceService.create_process_instance( + process_instance_receive = ProcessInstanceService.create_process_instance_from_process_model_identifier( message_triggerable_process_model.process_model_identifier, user, ) diff --git a/src/spiffworkflow_backend/services/process_instance_processor.py b/src/spiffworkflow_backend/services/process_instance_processor.py index be32a2f0..ffe69fd7 100644 --- a/src/spiffworkflow_backend/services/process_instance_processor.py +++ b/src/spiffworkflow_backend/services/process_instance_processor.py @@ -81,6 +81,9 @@ from spiffworkflow_backend.models.message_instance import MessageInstanceModel from spiffworkflow_backend.models.message_instance import MessageModel from spiffworkflow_backend.models.process_instance import ProcessInstanceModel from spiffworkflow_backend.models.process_instance import ProcessInstanceStatus +from spiffworkflow_backend.models.process_instance_metadata import ( + ProcessInstanceMetadataModel, +) from spiffworkflow_backend.models.process_model import ProcessModelInfo from spiffworkflow_backend.models.script_attributes_context import ( ScriptAttributesContext, @@ -97,6 +100,7 @@ from spiffworkflow_backend.services.service_task_service import ServiceTaskDeleg from spiffworkflow_backend.services.spec_file_service import SpecFileService from spiffworkflow_backend.services.user_service import UserService + # Sorry about all this crap. I wanted to move this thing to another file, but # importing a bunch of types causes circular imports. @@ -178,9 +182,14 @@ class CustomBpmnScriptEngine(PythonScriptEngine): # type: ignore ) return Script.generate_augmented_list(script_attributes_context) - def evaluate(self, task: SpiffTask, expression: str) -> Any: + def evaluate( + self, + task: SpiffTask, + expression: str, + external_methods: Optional[dict[str, Any]] = None, + ) -> Any: """Evaluate.""" - return self._evaluate(expression, task.data, task) + return self._evaluate(expression, task.data, task, external_methods) def _evaluate( self, @@ -349,7 +358,9 @@ class ProcessInstanceProcessor: check_sub_specs(test_spec, 5) self.process_model_identifier = process_instance_model.process_model_identifier - # self.process_group_identifier = process_instance_model.process_group_identifier + self.process_model_display_name = ( + process_instance_model.process_model_display_name + ) try: self.bpmn_process_instance = self.__get_bpmn_process_instance( @@ -359,21 +370,8 @@ class ProcessInstanceProcessor: subprocesses=subprocesses, ) self.bpmn_process_instance.script_engine = self._script_engine - self.add_user_info_to_process_instance(self.bpmn_process_instance) - if self.PROCESS_INSTANCE_ID_KEY not in self.bpmn_process_instance.data: - if not process_instance_model.id: - db.session.add(process_instance_model) - # If the model is new, and has no id, save it, write it into the process_instance model - # and save it again. In this way, the workflow process is always aware of the - # database model to which it is associated, and scripts running within the model - # can then load data as needed. - self.bpmn_process_instance.data[ - ProcessInstanceProcessor.PROCESS_INSTANCE_ID_KEY - ] = process_instance_model.id - self.save() - except MissingSpecError as ke: raise ApiError( error_code="unexpected_process_instance_structure", @@ -387,7 +385,7 @@ class ProcessInstanceProcessor: cls, process_model_identifier: str ) -> Tuple[BpmnProcessSpec, IdToBpmnProcessSpecMapping]: """Get_process_model_and_subprocesses.""" - process_model_info = ProcessModelService().get_process_model( + process_model_info = ProcessModelService.get_process_model( process_model_identifier ) if process_model_info is None: @@ -553,13 +551,8 @@ class ProcessInstanceProcessor: """SaveSpiffStepDetails.""" bpmn_json = self.serialize() wf_json = json.loads(bpmn_json) - task_json = "{}" - if "tasks" in wf_json: - task_json = json.dumps(wf_json["tasks"]) + task_json = wf_json["tasks"] - # TODO want to just save the tasks, something wasn't immediately working - # so after the flow works with the full wf_json revisit this - task_json = wf_json return { "process_instance_id": self.process_instance_model.id, "spiff_step": self.process_instance_model.spiff_step or 1, @@ -587,6 +580,41 @@ class ProcessInstanceProcessor: db.session.add(details_model) db.session.commit() + def extract_metadata(self, process_model_info: ProcessModelInfo) -> None: + """Extract_metadata.""" + metadata_extraction_paths = process_model_info.metadata_extraction_paths + if metadata_extraction_paths is None: + return + if len(metadata_extraction_paths) <= 0: + return + + current_data = self.get_current_data() + for metadata_extraction_path in metadata_extraction_paths: + key = metadata_extraction_path["key"] + path = metadata_extraction_path["path"] + path_segments = path.split(".") + data_for_key = current_data + for path_segment in path_segments: + if path_segment in data_for_key: + data_for_key = data_for_key[path_segment] + else: + data_for_key = None # type: ignore + break + + if data_for_key is not None: + pim = ProcessInstanceMetadataModel.query.filter_by( + process_instance_id=self.process_instance_model.id, + key=key, + ).first() + if pim is None: + pim = ProcessInstanceMetadataModel( + process_instance_id=self.process_instance_model.id, + key=key, + ) + pim.value = data_for_key + db.session.add(pim) + db.session.commit() + def save(self) -> None: """Saves the current state of this processor to the database.""" self.process_instance_model.bpmn_json = self.serialize() @@ -606,17 +634,22 @@ class ProcessInstanceProcessor: if self.bpmn_process_instance.is_completed(): self.process_instance_model.end_in_seconds = round(time.time()) - active_tasks = ActiveTaskModel.query.filter_by( - process_instance_id=self.process_instance_model.id - ).all() - if len(active_tasks) > 0: - for at in active_tasks: - db.session.delete(at) - db.session.add(self.process_instance_model) db.session.commit() + active_tasks = ActiveTaskModel.query.filter_by( + process_instance_id=self.process_instance_model.id + ).all() ready_or_waiting_tasks = self.get_all_ready_or_waiting_tasks() + process_model_display_name = "" + process_model_info = self.process_model_service.get_process_model( + self.process_instance_model.process_model_identifier + ) + if process_model_info is not None: + process_model_display_name = process_model_info.display_name + + self.extract_metadata(process_model_info) + for ready_or_waiting_task in ready_or_waiting_tasks: # filter out non-usertasks task_spec = ready_or_waiting_task.task_spec @@ -635,34 +668,41 @@ class ProcessInstanceProcessor: if "formUiSchemaFilename" in properties: ui_form_file_name = properties["formUiSchemaFilename"] - process_model_display_name = "" - process_model_info = self.process_model_service.get_process_model( - self.process_instance_model.process_model_identifier - ) - if process_model_info is not None: - process_model_display_name = process_model_info.display_name + active_task = None + for at in active_tasks: + if at.task_id == str(ready_or_waiting_task.id): + active_task = at + active_tasks.remove(at) - active_task = ActiveTaskModel( - process_instance_id=self.process_instance_model.id, - process_model_display_name=process_model_display_name, - form_file_name=form_file_name, - ui_form_file_name=ui_form_file_name, - task_id=str(ready_or_waiting_task.id), - task_name=ready_or_waiting_task.task_spec.name, - task_title=ready_or_waiting_task.task_spec.description, - task_type=ready_or_waiting_task.task_spec.__class__.__name__, - task_status=ready_or_waiting_task.get_state_name(), - lane_assignment_id=potential_owner_hash["lane_assignment_id"], - ) - db.session.add(active_task) - db.session.commit() - - for potential_owner_id in potential_owner_hash["potential_owner_ids"]: - active_task_user = ActiveTaskUserModel( - user_id=potential_owner_id, active_task_id=active_task.id + if active_task is None: + active_task = ActiveTaskModel( + process_instance_id=self.process_instance_model.id, + process_model_display_name=process_model_display_name, + form_file_name=form_file_name, + ui_form_file_name=ui_form_file_name, + task_id=str(ready_or_waiting_task.id), + task_name=ready_or_waiting_task.task_spec.name, + task_title=ready_or_waiting_task.task_spec.description, + task_type=ready_or_waiting_task.task_spec.__class__.__name__, + task_status=ready_or_waiting_task.get_state_name(), + lane_assignment_id=potential_owner_hash["lane_assignment_id"], ) - db.session.add(active_task_user) - db.session.commit() + db.session.add(active_task) + db.session.commit() + + for potential_owner_id in potential_owner_hash[ + "potential_owner_ids" + ]: + active_task_user = ActiveTaskUserModel( + user_id=potential_owner_id, active_task_id=active_task.id + ) + db.session.add(active_task_user) + db.session.commit() + + if len(active_tasks) > 0: + for at in active_tasks: + db.session.delete(at) + db.session.commit() @staticmethod def get_parser() -> MyCustomParser: @@ -675,7 +715,7 @@ class ProcessInstanceProcessor: bpmn_process_identifier: str, ) -> Optional[str]: """Backfill_missing_spec_reference_records.""" - process_models = ProcessModelService().get_process_models() + process_models = ProcessModelService.get_process_models(recursive=True) for process_model in process_models: try: refs = SpecFileService.reference_map( @@ -1152,8 +1192,8 @@ class ProcessInstanceProcessor: def get_current_data(self) -> dict[str, Any]: """Get the current data for the process. - Return either most recent task data or the process data - if the process instance is complete + Return either the most recent task data or--if the process instance is complete-- + the process data. """ if self.process_instance_model.status == "complete": return self.get_data() diff --git a/src/spiffworkflow_backend/services/process_instance_report_service.py b/src/spiffworkflow_backend/services/process_instance_report_service.py index a521c1a3..84d5d675 100644 --- a/src/spiffworkflow_backend/services/process_instance_report_service.py +++ b/src/spiffworkflow_backend/services/process_instance_report_service.py @@ -2,6 +2,9 @@ from dataclasses import dataclass from typing import Optional +import sqlalchemy +from flask_bpmn.models.db import db + from spiffworkflow_backend.models.process_instance_report import ( ProcessInstanceReportModel, ) @@ -18,6 +21,9 @@ class ProcessInstanceReportFilter: end_from: Optional[int] = None end_to: Optional[int] = None process_status: Optional[list[str]] = None + initiated_by_me: Optional[bool] = None + with_tasks_completed_by_me: Optional[bool] = None + with_tasks_completed_by_my_group: Optional[bool] = None def to_dict(self) -> dict[str, str]: """To_dict.""" @@ -35,6 +41,16 @@ class ProcessInstanceReportFilter: d["end_to"] = str(self.end_to) if self.process_status is not None: d["process_status"] = ",".join(self.process_status) + if self.initiated_by_me is not None: + d["initiated_by_me"] = str(self.initiated_by_me).lower() + if self.with_tasks_completed_by_me is not None: + d["with_tasks_completed_by_me"] = str( + self.with_tasks_completed_by_me + ).lower() + if self.with_tasks_completed_by_my_group is not None: + d["with_tasks_completed_by_my_group"] = str( + self.with_tasks_completed_by_my_group + ).lower() return d @@ -44,49 +60,65 @@ class ProcessInstanceReportService: @classmethod def report_with_identifier( - cls, user: UserModel, report_identifier: Optional[str] = None + cls, + user: UserModel, + report_id: Optional[int] = None, + report_identifier: Optional[str] = None, ) -> ProcessInstanceReportModel: """Report_with_filter.""" + if report_id is not None: + process_instance_report = ProcessInstanceReportModel.query.filter_by( + id=report_id, created_by_id=user.id + ).first() + if process_instance_report is not None: + return process_instance_report # type: ignore + if report_identifier is None: - return ProcessInstanceReportModel.default_report(user) + report_identifier = "default" + process_instance_report = ProcessInstanceReportModel.query.filter_by( + identifier=report_identifier, created_by_id=user.id + ).first() + + if process_instance_report is not None: + return process_instance_report # type: ignore # TODO replace with system reports that are loaded on launch (or similar) temp_system_metadata_map = { + "default": { + "columns": cls.builtin_column_options(), + "filter_by": [], + "order_by": ["-start_in_seconds", "-id"], + }, "system_report_instances_initiated_by_me": { "columns": [ + {"Header": "id", "accessor": "id"}, { - "Header": "process_model_identifier", - "accessor": "process_model_identifier", + "Header": "process_model_display_name", + "accessor": "process_model_display_name", }, {"Header": "start_in_seconds", "accessor": "start_in_seconds"}, - {"Header": "id", "accessor": "id"}, {"Header": "end_in_seconds", "accessor": "end_in_seconds"}, {"Header": "status", "accessor": "status"}, ], + "filter_by": [{"field_name": "initiated_by_me", "field_value": True}], + "order_by": ["-start_in_seconds", "-id"], }, "system_report_instances_with_tasks_completed_by_me": { - "columns": [ - {"Header": "start_in_seconds", "accessor": "start_in_seconds"}, - {"Header": "end_in_seconds", "accessor": "end_in_seconds"}, - {"Header": "status", "accessor": "status"}, - {"Header": "id", "accessor": "id"}, - { - "Header": "process_model_identifier", - "accessor": "process_model_identifier", - }, + "columns": cls.builtin_column_options(), + "filter_by": [ + {"field_name": "with_tasks_completed_by_me", "field_value": True} ], + "order_by": ["-start_in_seconds", "-id"], }, "system_report_instances_with_tasks_completed_by_my_groups": { - "columns": [ + "columns": cls.builtin_column_options(), + "filter_by": [ { - "Header": "process_model_identifier", - "accessor": "process_model_identifier", - }, - {"Header": "start_in_seconds", "accessor": "start_in_seconds"}, - {"Header": "end_in_seconds", "accessor": "end_in_seconds"}, - {"Header": "status", "accessor": "status"}, - {"Header": "id", "accessor": "id"}, + "field_name": "with_tasks_completed_by_my_group", + "field_value": True, + } ], + "order_by": ["-start_in_seconds", "-id"], }, } @@ -96,7 +128,7 @@ class ProcessInstanceReportService: report_metadata=temp_system_metadata_map[report_identifier], ) - return process_instance_report + return process_instance_report # type: ignore @classmethod def filter_by_to_dict( @@ -119,6 +151,10 @@ class ProcessInstanceReportService: """Filter_from_metadata.""" filters = cls.filter_by_to_dict(process_instance_report) + def bool_value(key: str) -> Optional[bool]: + """Bool_value.""" + return bool(filters[key]) if key in filters else None + def int_value(key: str) -> Optional[int]: """Int_value.""" return int(filters[key]) if key in filters else None @@ -133,6 +169,11 @@ class ProcessInstanceReportService: end_from = int_value("end_from") end_to = int_value("end_to") process_status = list_value("process_status") + initiated_by_me = bool_value("initiated_by_me") + with_tasks_completed_by_me = bool_value("with_tasks_completed_by_me") + with_tasks_completed_by_my_group = bool_value( + "with_tasks_completed_by_my_group" + ) report_filter = ProcessInstanceReportFilter( process_model_identifier, @@ -141,6 +182,9 @@ class ProcessInstanceReportService: end_from, end_to, process_status, + initiated_by_me, + with_tasks_completed_by_me, + with_tasks_completed_by_my_group, ) return report_filter @@ -155,6 +199,9 @@ class ProcessInstanceReportService: end_from: Optional[int] = None, end_to: Optional[int] = None, process_status: Optional[str] = None, + initiated_by_me: Optional[bool] = None, + with_tasks_completed_by_me: Optional[bool] = None, + with_tasks_completed_by_my_group: Optional[bool] = None, ) -> ProcessInstanceReportFilter: """Filter_from_metadata_with_overrides.""" report_filter = cls.filter_from_metadata(process_instance_report) @@ -171,5 +218,53 @@ class ProcessInstanceReportService: report_filter.end_to = end_to if process_status is not None: report_filter.process_status = process_status.split(",") + if initiated_by_me is not None: + report_filter.initiated_by_me = initiated_by_me + if with_tasks_completed_by_me is not None: + report_filter.with_tasks_completed_by_me = with_tasks_completed_by_me + if with_tasks_completed_by_my_group is not None: + report_filter.with_tasks_completed_by_my_group = ( + with_tasks_completed_by_my_group + ) return report_filter + + @classmethod + def add_metadata_columns_to_process_instance( + cls, + process_instance_sqlalchemy_rows: list[sqlalchemy.engine.row.Row], # type: ignore + metadata_columns: list[dict], + ) -> list[dict]: + """Add_metadata_columns_to_process_instance.""" + results = [] + for process_instance in process_instance_sqlalchemy_rows: + process_instance_dict = process_instance["ProcessInstanceModel"].serialized + for metadata_column in metadata_columns: + if metadata_column["accessor"] not in process_instance_dict: + process_instance_dict[ + metadata_column["accessor"] + ] = process_instance[metadata_column["accessor"]] + + results.append(process_instance_dict) + return results + + @classmethod + def get_column_names_for_model(cls, model: db.Model) -> list[str]: # type: ignore + """Get_column_names_for_model.""" + return [i.name for i in model.__table__.columns] + + @classmethod + def builtin_column_options(cls) -> list[dict]: + """Builtin_column_options.""" + return [ + {"Header": "Id", "accessor": "id", "filterable": False}, + { + "Header": "Process", + "accessor": "process_model_display_name", + "filterable": False, + }, + {"Header": "Start", "accessor": "start_in_seconds", "filterable": False}, + {"Header": "End", "accessor": "end_in_seconds", "filterable": False}, + {"Header": "Username", "accessor": "username", "filterable": False}, + {"Header": "Status", "accessor": "status", "filterable": False}, + ] diff --git a/src/spiffworkflow_backend/services/process_instance_service.py b/src/spiffworkflow_backend/services/process_instance_service.py index 80271801..46bd252b 100644 --- a/src/spiffworkflow_backend/services/process_instance_service.py +++ b/src/spiffworkflow_backend/services/process_instance_service.py @@ -12,6 +12,7 @@ from spiffworkflow_backend.models.active_task import ActiveTaskModel from spiffworkflow_backend.models.process_instance import ProcessInstanceApi from spiffworkflow_backend.models.process_instance import ProcessInstanceModel from spiffworkflow_backend.models.process_instance import ProcessInstanceStatus +from spiffworkflow_backend.models.process_model import ProcessModelInfo from spiffworkflow_backend.models.task import MultiInstanceType from spiffworkflow_backend.models.task import Task from spiffworkflow_backend.models.user import UserModel @@ -28,9 +29,10 @@ class ProcessInstanceService: TASK_STATE_LOCKED = "locked" - @staticmethod + @classmethod def create_process_instance( - process_model_identifier: str, + cls, + process_model: ProcessModelInfo, user: UserModel, ) -> ProcessInstanceModel: """Get_process_instance_from_spec.""" @@ -38,8 +40,8 @@ class ProcessInstanceService: process_instance_model = ProcessInstanceModel( status=ProcessInstanceStatus.not_started.value, process_initiator=user, - process_model_identifier=process_model_identifier, - process_group_identifier="", + process_model_identifier=process_model.id, + process_model_display_name=process_model.display_name, start_in_seconds=round(time.time()), bpmn_version_control_type="git", bpmn_version_control_identifier=current_git_revision, @@ -48,6 +50,16 @@ class ProcessInstanceService: db.session.commit() return process_instance_model + @classmethod + def create_process_instance_from_process_model_identifier( + cls, + process_model_identifier: str, + user: UserModel, + ) -> ProcessInstanceModel: + """Create_process_instance_from_process_model_identifier.""" + process_model = ProcessModelService.get_process_model(process_model_identifier) + return cls.create_process_instance(process_model, user) + @staticmethod def do_waiting() -> None: """Do_waiting.""" @@ -88,20 +100,15 @@ class ProcessInstanceService: process_model = process_model_service.get_process_model( processor.process_model_identifier ) - is_review_value = process_model.is_review if process_model else False - title_value = process_model.display_name if process_model else "" + process_model.display_name if process_model else "" process_instance_api = ProcessInstanceApi( id=processor.get_process_instance_id(), status=processor.get_status(), next_task=None, - # navigation=navigation, process_model_identifier=processor.process_model_identifier, - process_group_identifier="", - # total_tasks=len(navigation), + process_model_display_name=processor.process_model_display_name, completed_tasks=processor.process_instance_model.completed_tasks, updated_at_in_seconds=processor.process_instance_model.updated_at_in_seconds, - is_review=is_review_value, - title=title_value, ) next_task_trying_again = next_task @@ -197,7 +204,7 @@ class ProcessInstanceService: a multi-instance task. """ AuthorizationService.assert_user_can_complete_spiff_task( - processor, spiff_task, user + processor.process_instance_model.id, spiff_task, user ) dot_dct = ProcessInstanceService.create_dot_dict(data) @@ -315,22 +322,3 @@ class ProcessInstanceService: ) return task - - @staticmethod - def serialize_flat_with_task_data( - process_instance: ProcessInstanceModel, - ) -> dict[str, Any]: - """Serialize_flat_with_task_data.""" - results = {} - try: - original_status = process_instance.status - processor = ProcessInstanceProcessor(process_instance) - process_instance.data = processor.get_current_data() - results = process_instance.serialized_flat - # this process seems to mutate the status of the process_instance which - # can result in different results than expected from process_instance_list, - # so set the status back to the expected value - results["status"] = original_status - except ApiError: - results = process_instance.serialized - return results diff --git a/src/spiffworkflow_backend/services/process_model_service.py b/src/spiffworkflow_backend/services/process_model_service.py index 2431289c..d4fa5647 100644 --- a/src/spiffworkflow_backend/services/process_model_service.py +++ b/src/spiffworkflow_backend/services/process_model_service.py @@ -2,6 +2,7 @@ import json import os import shutil +from glob import glob from typing import Any from typing import List from typing import Optional @@ -17,7 +18,9 @@ from spiffworkflow_backend.models.process_group import ProcessGroupSchema from spiffworkflow_backend.models.process_instance import ProcessInstanceModel from spiffworkflow_backend.models.process_model import ProcessModelInfo from spiffworkflow_backend.models.process_model import ProcessModelInfoSchema +from spiffworkflow_backend.services.authorization_service import AuthorizationService from spiffworkflow_backend.services.file_system_service import FileSystemService +from spiffworkflow_backend.services.user_service import UserService T = TypeVar("T") @@ -34,20 +37,54 @@ class ProcessModelService(FileSystemService): GROUP_SCHEMA = ProcessGroupSchema() PROCESS_MODEL_SCHEMA = ProcessModelInfoSchema() - def is_group(self, path: str) -> bool: + @classmethod + def is_group(cls, path: str) -> bool: """Is_group.""" - group_json_path = os.path.join(path, self.PROCESS_GROUP_JSON_FILE) + group_json_path = os.path.join(path, cls.PROCESS_GROUP_JSON_FILE) if os.path.exists(group_json_path): return True return False - def is_model(self, path: str) -> bool: + @classmethod + def is_group_identifier(cls, process_group_identifier: str) -> bool: + """Is_group_identifier.""" + if os.path.exists(FileSystemService.root_path()): + process_group_path = os.path.abspath( + os.path.join( + FileSystemService.root_path(), + FileSystemService.id_string_to_relative_path( + process_group_identifier + ), + ) + ) + return cls.is_group(process_group_path) + + return False + + @classmethod + def is_model(cls, path: str) -> bool: """Is_model.""" - model_json_path = os.path.join(path, self.PROCESS_MODEL_JSON_FILE) + model_json_path = os.path.join(path, cls.PROCESS_MODEL_JSON_FILE) if os.path.exists(model_json_path): return True return False + @classmethod + def is_model_identifier(cls, process_model_identifier: str) -> bool: + """Is_model_identifier.""" + if os.path.exists(FileSystemService.root_path()): + process_model_path = os.path.abspath( + os.path.join( + FileSystemService.root_path(), + FileSystemService.id_string_to_relative_path( + process_model_identifier + ), + ) + ) + return cls.is_model(process_model_path) + + return False + @staticmethod def write_json_file( file_path: str, json_data: dict, indent: int = 4, sort_keys: bool = True @@ -67,37 +104,38 @@ class ProcessModelService(FileSystemService): end = start + per_page return items[start:end] - def add_process_model(self, process_model: ProcessModelInfo) -> None: + @classmethod + def add_process_model(cls, process_model: ProcessModelInfo) -> None: """Add_spec.""" - display_order = self.next_display_order(process_model) - process_model.display_order = display_order - self.save_process_model(process_model) + cls.save_process_model(process_model) + @classmethod def update_process_model( - self, process_model: ProcessModelInfo, attributes_to_update: dict + cls, process_model: ProcessModelInfo, attributes_to_update: dict ) -> None: """Update_spec.""" for atu_key, atu_value in attributes_to_update.items(): if hasattr(process_model, atu_key): setattr(process_model, atu_key, atu_value) - self.save_process_model(process_model) + cls.save_process_model(process_model) - def save_process_model(self, process_model: ProcessModelInfo) -> None: + @classmethod + def save_process_model(cls, process_model: ProcessModelInfo) -> None: """Save_process_model.""" process_model_path = os.path.abspath( os.path.join(FileSystemService.root_path(), process_model.id) ) os.makedirs(process_model_path, exist_ok=True) json_path = os.path.abspath( - os.path.join(process_model_path, self.PROCESS_MODEL_JSON_FILE) + os.path.join(process_model_path, cls.PROCESS_MODEL_JSON_FILE) ) process_model_id = process_model.id # we don't save id in the json file # this allows us to move models around on the filesystem # the id is determined by its location on the filesystem delattr(process_model, "id") - json_data = self.PROCESS_MODEL_SCHEMA.dump(process_model) - self.write_json_file(json_path, json_data) + json_data = cls.PROCESS_MODEL_SCHEMA.dump(process_model) + cls.write_json_file(json_path, json_data) process_model.id = process_model_id def process_model_delete(self, process_model_id: str) -> None: @@ -110,22 +148,36 @@ class ProcessModelService(FileSystemService): error_code="existing_instances", message=f"We cannot delete the model `{process_model_id}`, there are existing instances that depend on it.", ) - self.get_process_model(process_model_id) - # path = self.workflow_path(process_model) - path = f"{FileSystemService.root_path()}/{process_model_id}" + process_model = self.get_process_model(process_model_id) + path = self.workflow_path(process_model) shutil.rmtree(path) + def process_model_move( + self, original_process_model_id: str, new_location: str + ) -> ProcessModelInfo: + """Process_model_move.""" + process_model = self.get_process_model(original_process_model_id) + original_model_path = self.workflow_path(process_model) + _, model_id = os.path.split(original_model_path) + new_relative_path = os.path.join(new_location, model_id) + new_model_path = os.path.abspath( + os.path.join(FileSystemService.root_path(), new_relative_path) + ) + shutil.move(original_model_path, new_model_path) + new_process_model = self.get_process_model(new_relative_path) + return new_process_model + @classmethod def get_process_model_from_relative_path( cls, relative_path: str ) -> ProcessModelInfo: """Get_process_model_from_relative_path.""" process_group_identifier, _ = os.path.split(relative_path) - process_group = cls().get_process_group(process_group_identifier) path = os.path.join(FileSystemService.root_path(), relative_path) - return cls().__scan_process_model(path, process_group=process_group) + return cls.__scan_process_model(path) - def get_process_model(self, process_model_id: str) -> ProcessModelInfo: + @classmethod + def get_process_model(cls, process_model_id: str) -> ProcessModelInfo: """Get a process model from a model and group id. process_model_id is the full path to the model--including groups. @@ -136,108 +188,139 @@ class ProcessModelService(FileSystemService): model_path = os.path.abspath( os.path.join(FileSystemService.root_path(), process_model_id) ) - if self.is_model(model_path): - process_model = self.get_process_model_from_relative_path(process_model_id) - return process_model - - # group_path, model_id = os.path.split(process_model_id) - # if group_path is not None: - # process_group = self.get_process_group(group_path) - # if process_group is not None: - # for process_model in process_group.process_models: - # if process_model_id == process_model.id: - # return process_model - # with os.scandir(FileSystemService.root_path()) as process_group_dirs: - # for item in process_group_dirs: - # process_group_dir = item - # if item.is_dir(): - # with os.scandir(item.path) as spec_dirs: - # for sd in spec_dirs: - # if sd.name == process_model_id: - # # Now we have the process_group directory, and spec directory - # process_group = self.__scan_process_group( - # process_group_dir - # ) - # return self.__scan_process_model(sd.path, sd.name, process_group) + if cls.is_model(model_path): + return cls.get_process_model_from_relative_path(process_model_id) raise ProcessEntityNotFoundError("process_model_not_found") + @classmethod def get_process_models( - self, process_group_id: Optional[str] = None + cls, + process_group_id: Optional[str] = None, + recursive: Optional[bool] = False, + filter_runnable_by_user: Optional[bool] = False, ) -> List[ProcessModelInfo]: """Get process models.""" - process_groups = [] - if process_group_id is None: - process_groups = self.get_process_groups() - else: - process_group = self.get_process_group(process_group_id) - if process_group is not None: - process_groups.append(process_group) - process_models = [] - for process_group in process_groups: - process_models.extend(process_group.process_models) + root_path = FileSystemService.root_path() + if process_group_id: + awesome_id = process_group_id.replace("/", os.sep) + root_path = os.path.join(root_path, awesome_id) + + process_model_glob = os.path.join(root_path, "*", "process_model.json") + if recursive: + process_model_glob = os.path.join(root_path, "**", "process_model.json") + + for file in glob(process_model_glob, recursive=True): + process_model_relative_path = os.path.relpath( + file, start=FileSystemService.root_path() + ) + process_model = cls.get_process_model_from_relative_path( + os.path.dirname(process_model_relative_path) + ) + process_models.append(process_model) process_models.sort() + + if filter_runnable_by_user: + user = UserService.current_user() + new_process_model_list = [] + for process_model in process_models: + uri = f"/v1.0/process-models/{process_model.id.replace('/', ':')}/process-instances" + result = AuthorizationService.user_has_permission( + user=user, permission="create", target_uri=uri + ) + if result: + new_process_model_list.append(process_model) + return new_process_model_list + return process_models + @classmethod + def get_parent_group_array(cls, process_identifier: str) -> list[dict]: + """Get_parent_group_array.""" + full_group_id_path = None + parent_group_array = [] + for process_group_id_segment in process_identifier.split("/")[0:-1]: + if full_group_id_path is None: + full_group_id_path = process_group_id_segment + else: + full_group_id_path = os.path.join(full_group_id_path, process_group_id_segment) # type: ignore + parent_group = ProcessModelService.get_process_group(full_group_id_path) + if parent_group: + parent_group_array.append( + {"id": parent_group.id, "display_name": parent_group.display_name} + ) + return parent_group_array + + @classmethod def get_process_groups( - self, process_group_id: Optional[str] = None + cls, process_group_id: Optional[str] = None ) -> list[ProcessGroup]: - """Returns the process_groups as a list in display order.""" - process_groups = self.__scan_process_groups(process_group_id) + """Returns the process_groups.""" + process_groups = cls.__scan_process_groups(process_group_id) process_groups.sort() return process_groups - def get_process_group(self, process_group_id: str) -> ProcessGroup: + @classmethod + def get_process_group( + cls, process_group_id: str, find_direct_nested_items: bool = True + ) -> ProcessGroup: """Look for a given process_group, and return it.""" if os.path.exists(FileSystemService.root_path()): process_group_path = os.path.abspath( - os.path.join(FileSystemService.root_path(), process_group_id) + os.path.join( + FileSystemService.root_path(), + FileSystemService.id_string_to_relative_path(process_group_id), + ) ) - if self.is_group(process_group_path): - return self.__scan_process_group(process_group_path) - # nested_groups = [] - # process_group_dir = os.scandir(process_group_path) - # for item in process_group_dir: - # if self.is_group(item.path): - # nested_group = self.get_process_group(os.path.join(process_group_path, item.path)) - # nested_groups.append(nested_group) - # elif self.is_model(item.path): - # print("get_process_group: ") - # return self.__scan_process_group(process_group_path) - # with os.scandir(FileSystemService.root_path()) as directory_items: - # for item in directory_items: - # if item.is_dir() and item.name == process_group_id: - # return self.__scan_process_group(item) + if cls.is_group(process_group_path): + return cls.find_or_create_process_group( + process_group_path, + find_direct_nested_items=find_direct_nested_items, + ) raise ProcessEntityNotFoundError( "process_group_not_found", f"Process Group Id: {process_group_id}" ) - def add_process_group(self, process_group: ProcessGroup) -> ProcessGroup: + @classmethod + def add_process_group(cls, process_group: ProcessGroup) -> ProcessGroup: """Add_process_group.""" - display_order = len(self.get_process_groups()) - process_group.display_order = display_order - return self.update_process_group(process_group) + return cls.update_process_group(process_group) - def update_process_group(self, process_group: ProcessGroup) -> ProcessGroup: + @classmethod + def update_process_group(cls, process_group: ProcessGroup) -> ProcessGroup: """Update_process_group.""" - cat_path = self.process_group_path(process_group.id) + cat_path = cls.process_group_path(process_group.id) os.makedirs(cat_path, exist_ok=True) - json_path = os.path.join(cat_path, self.PROCESS_GROUP_JSON_FILE) + json_path = os.path.join(cat_path, cls.PROCESS_GROUP_JSON_FILE) serialized_process_group = process_group.serialized # we don't store `id` in the json files # this allows us to move groups around on the filesystem del serialized_process_group["id"] - self.write_json_file(json_path, serialized_process_group) + cls.write_json_file(json_path, serialized_process_group) return process_group + def process_group_move( + self, original_process_group_id: str, new_location: str + ) -> ProcessGroup: + """Process_group_move.""" + original_group_path = self.process_group_path(original_process_group_id) + _, original_group_id = os.path.split(original_group_path) + new_root = os.path.join(FileSystemService.root_path(), new_location) + new_group_path = os.path.abspath( + os.path.join(FileSystemService.root_path(), new_root, original_group_id) + ) + destination = shutil.move(original_group_path, new_group_path) + new_process_group = self.get_process_group(destination) + return new_process_group + def __get_all_nested_models(self, group_path: str) -> list: """__get_all_nested_models.""" all_nested_models = [] for _root, dirs, _files in os.walk(group_path): for dir in dirs: model_dir = os.path.join(group_path, dir) - if ProcessModelService().is_model(model_dir): + if ProcessModelService.is_model(model_dir): process_model = self.get_process_model(model_dir) all_nested_models.append(process_model) return all_nested_models @@ -273,8 +356,9 @@ class ProcessModelService(FileSystemService): index += 1 return process_groups + @classmethod def __scan_process_groups( - self, process_group_id: Optional[str] = None + cls, process_group_id: Optional[str] = None ) -> list[ProcessGroup]: """__scan_process_groups.""" if not os.path.exists(FileSystemService.root_path()): @@ -288,14 +372,17 @@ class ProcessModelService(FileSystemService): process_groups = [] for item in directory_items: # if item.is_dir() and not item.name[0] == ".": - if item.is_dir() and self.is_group(item): # type: ignore - scanned_process_group = self.__scan_process_group(item.path) + if item.is_dir() and cls.is_group(item): # type: ignore + scanned_process_group = cls.find_or_create_process_group(item.path) process_groups.append(scanned_process_group) return process_groups - def __scan_process_group(self, dir_path: str) -> ProcessGroup: + @classmethod + def find_or_create_process_group( + cls, dir_path: str, find_direct_nested_items: bool = True + ) -> ProcessGroup: """Reads the process_group.json file, and any nested directories.""" - cat_path = os.path.join(dir_path, self.PROCESS_GROUP_JSON_FILE) + cat_path = os.path.join(dir_path, cls.PROCESS_GROUP_JSON_FILE) if os.path.exists(cat_path): with open(cat_path) as cat_json: data = json.load(cat_json) @@ -316,40 +403,41 @@ class ProcessModelService(FileSystemService): display_order=10000, admin=False, ) - self.write_json_file(cat_path, self.GROUP_SCHEMA.dump(process_group)) + cls.write_json_file(cat_path, cls.GROUP_SCHEMA.dump(process_group)) # we don't store `id` in the json files, so we add it in here process_group.id = process_group_id - with os.scandir(dir_path) as nested_items: - process_group.process_models = [] - process_group.process_groups = [] - for nested_item in nested_items: - if nested_item.is_dir(): - # TODO: check whether this is a group or model - if self.is_group(nested_item.path): - # This is a nested group - process_group.process_groups.append( - self.__scan_process_group(nested_item.path) - ) - elif self.is_model(nested_item.path): - process_group.process_models.append( - self.__scan_process_model( - nested_item.path, - nested_item.name, - process_group=process_group, + + if find_direct_nested_items: + with os.scandir(dir_path) as nested_items: + process_group.process_models = [] + process_group.process_groups = [] + for nested_item in nested_items: + if nested_item.is_dir(): + # TODO: check whether this is a group or model + if cls.is_group(nested_item.path): + # This is a nested group + process_group.process_groups.append( + cls.find_or_create_process_group(nested_item.path) ) - ) - process_group.process_models.sort() - # process_group.process_groups.sort() + elif ProcessModelService.is_model(nested_item.path): + process_group.process_models.append( + cls.__scan_process_model( + nested_item.path, + nested_item.name, + ) + ) + process_group.process_models.sort() + # process_group.process_groups.sort() return process_group + @classmethod def __scan_process_model( - self, + cls, path: str, name: Optional[str] = None, - process_group: Optional[ProcessGroup] = None, ) -> ProcessModelInfo: """__scan_process_model.""" - json_file_path = os.path.join(path, self.PROCESS_MODEL_JSON_FILE) + json_file_path = os.path.join(path, cls.PROCESS_MODEL_JSON_FILE) if os.path.exists(json_file_path): with open(json_file_path) as wf_json: @@ -377,13 +465,10 @@ class ProcessModelService(FileSystemService): display_name=name, description="", display_order=0, - is_review=False, ) - self.write_json_file( - json_file_path, self.PROCESS_MODEL_SCHEMA.dump(process_model_info) + cls.write_json_file( + json_file_path, cls.PROCESS_MODEL_SCHEMA.dump(process_model_info) ) # we don't store `id` in the json files, so we add it in here process_model_info.id = name - if process_group: - process_model_info.process_group = process_group.id return process_model_info diff --git a/src/spiffworkflow_backend/services/secret_service.py b/src/spiffworkflow_backend/services/secret_service.py index 42f401c1..e4dee491 100644 --- a/src/spiffworkflow_backend/services/secret_service.py +++ b/src/spiffworkflow_backend/services/secret_service.py @@ -19,15 +19,15 @@ from spiffworkflow_backend.models.secret_model import SecretModel class SecretService: """SecretService.""" - def encrypt_key(self, plain_key: str) -> str: - """Encrypt_key.""" - # flask_secret = current_app.secret_key - # print("encrypt_key") - ... + # def encrypt_key(self, plain_key: str) -> str: + # """Encrypt_key.""" + # # flask_secret = current_app.secret_key + # # print("encrypt_key") + # ... - def decrypt_key(self, encrypted_key: str) -> str: - """Decrypt key.""" - ... + # def decrypt_key(self, encrypted_key: str) -> str: + # """Decrypt key.""" + # ... @staticmethod def add_secret( @@ -65,7 +65,7 @@ class SecretService: def update_secret( key: str, value: str, - user_id: int, + user_id: Optional[int] = None, create_if_not_exists: Optional[bool] = False, ) -> None: """Does this pass pre commit?""" @@ -79,6 +79,12 @@ class SecretService: db.session.rollback() raise e elif create_if_not_exists: + if user_id is None: + raise ApiError( + error_code="update_secret_error_no_user_id", + message=f"Cannot update secret with key: {key}. Missing user id.", + status_code=404, + ) SecretService.add_secret(key=key, value=value, user_id=user_id) else: raise ApiError( diff --git a/src/spiffworkflow_backend/services/service_task_service.py b/src/spiffworkflow_backend/services/service_task_service.py index 97ce1495..15e25a75 100644 --- a/src/spiffworkflow_backend/services/service_task_service.py +++ b/src/spiffworkflow_backend/services/service_task_service.py @@ -8,6 +8,7 @@ from flask import g from spiffworkflow_backend.services.file_system_service import FileSystemService from spiffworkflow_backend.services.secret_service import SecretService +from spiffworkflow_backend.services.user_service import UserService class ConnectorProxyError(Exception): @@ -65,7 +66,8 @@ class ServiceTaskDelegate: secret_key = parsed_response["auth"] refreshed_token_set = json.dumps(parsed_response["refreshed_token_set"]) - SecretService().update_secret(secret_key, refreshed_token_set, g.user.id) + user_id = g.user.id if UserService.has_user() else None + SecretService().update_secret(secret_key, refreshed_token_set, user_id) return json.dumps(parsed_response["api_response"]) diff --git a/src/spiffworkflow_backend/services/spec_file_service.py b/src/spiffworkflow_backend/services/spec_file_service.py index f02e3a6f..c69f41c3 100644 --- a/src/spiffworkflow_backend/services/spec_file_service.py +++ b/src/spiffworkflow_backend/services/spec_file_service.py @@ -171,12 +171,11 @@ class SpecFileService(FileSystemService): ref.is_primary = True if ref.is_primary: - ProcessModelService().update_process_model( + ProcessModelService.update_process_model( process_model_info, { "primary_process_id": ref.identifier, "primary_file_name": file_name, - "is_review": ref.has_lanes, }, ) SpecFileService.update_caches(ref) @@ -322,7 +321,6 @@ class SpecFileService(FileSystemService): message_triggerable_process_model = MessageTriggerableProcessModel( message_model_id=message_model.id, process_model_identifier=ref.process_model_id, - process_group_identifier="process_group_identifier", ) db.session.add(message_triggerable_process_model) db.session.commit() @@ -330,8 +328,6 @@ class SpecFileService(FileSystemService): if ( message_triggerable_process_model.process_model_identifier != ref.process_model_id - # or message_triggerable_process_model.process_group_identifier - # != process_model_info.process_group_id ): raise ValidationException( f"Message model is already used to start process model {ref.process_model_id}" diff --git a/tests/data/hello_world/hello_world.bpmn b/tests/data/hello_world/hello_world.bpmn index 1e5bc853..4be5adba 100644 --- a/tests/data/hello_world/hello_world.bpmn +++ b/tests/data/hello_world/hello_world.bpmn @@ -19,7 +19,11 @@ Flow_0bazl8x Flow_1mcaszp - a = 1 + a = 1 +b = 2 +outer = {} +outer["inner"] = 'sweet1' + Flow_1mcaszp diff --git a/tests/data/nested-task-data-structure/nested-task-data-structure.bpmn b/tests/data/nested-task-data-structure/nested-task-data-structure.bpmn new file mode 100644 index 00000000..7452216a --- /dev/null +++ b/tests/data/nested-task-data-structure/nested-task-data-structure.bpmn @@ -0,0 +1,56 @@ + + + + + Flow_1ohrjz9 + + + + Flow_1flxgry + + + + Flow_1ohrjz9 + Flow_18gs4jt + outer = {} +invoice_number = 123 +outer["inner"] = 'sweet1' +outer['time'] = time.time_ns() + + + + Flow_18gs4jt + Flow_1flxgry + outer["inner"] = 'sweet2' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/data/save_process_instance_metadata/save_process_instance_metadata.bpmn b/tests/data/save_process_instance_metadata/save_process_instance_metadata.bpmn new file mode 100644 index 00000000..2c72b08d --- /dev/null +++ b/tests/data/save_process_instance_metadata/save_process_instance_metadata.bpmn @@ -0,0 +1,52 @@ + + + + + Flow_1j4jzft + + + + Flow_01xr2ac + + + Flow_1j4jzft + Flow_10xyk22 + save_process_instance_metadata({"key1": "value1"}) + + + + Flow_10xyk22 + Flow_01xr2ac + save_process_instance_metadata({"key2": "value2", "key3": "value3"}) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/spiffworkflow_backend/helpers/base_test.py b/tests/spiffworkflow_backend/helpers/base_test.py index 44c99908..48982fc6 100644 --- a/tests/spiffworkflow_backend/helpers/base_test.py +++ b/tests/spiffworkflow_backend/helpers/base_test.py @@ -140,7 +140,7 @@ class BaseTest: process_group_path = os.path.abspath( os.path.join(FileSystemService.root_path(), process_group_id) ) - if ProcessModelService().is_group(process_group_path): + if ProcessModelService.is_group(process_group_path): if exception_notification_addresses is None: exception_notification_addresses = [] @@ -149,7 +149,6 @@ class BaseTest: id=process_model_id, display_name=process_model_display_name, description=process_model_description, - is_review=False, primary_process_id=primary_process_id, primary_file_name=primary_file_name, fault_or_suspend_on_exception=fault_or_suspend_on_exception, @@ -253,9 +252,20 @@ class BaseTest: There must be an existing process model to instantiate. """ + if not ProcessModelService.is_model_identifier(test_process_model_id): + dirname = os.path.dirname(test_process_model_id) + if not ProcessModelService.is_group_identifier(dirname): + process_group = ProcessGroup(id=dirname, display_name=dirname) + ProcessModelService.add_process_group(process_group) + basename = os.path.basename(test_process_model_id) + load_test_spec( + process_model_id=test_process_model_id, + process_model_source_directory=basename, + bpmn_file_name=basename, + ) modified_process_model_id = test_process_model_id.replace("/", ":") response = client.post( - f"/v1.0/process-models/{modified_process_model_id}/process-instances", + f"/v1.0/process-instances/{modified_process_model_id}", headers=headers, ) assert response.status_code == 201 @@ -284,7 +294,7 @@ class BaseTest: status=status, process_initiator=user, process_model_identifier=process_model.id, - process_group_identifier="", + process_model_display_name=process_model.display_name, updated_at_in_seconds=round(time.time()), start_in_seconds=current_time - (3600 * 1), end_in_seconds=current_time - (3600 * 1 - 20), @@ -347,3 +357,16 @@ class BaseTest: target_uri=target_uri, ) assert has_permission is expected_result + + def modify_process_identifier_for_path_param(self, identifier: str) -> str: + """Identifier.""" + if "\\" in identifier: + raise Exception(f"Found backslash in identifier: {identifier}") + + return identifier.replace("/", ":") + + def un_modify_modified_process_identifier_for_path_param( + self, modified_identifier: str + ) -> str: + """Un_modify_modified_process_model_id.""" + return modified_identifier.replace(":", "/") diff --git a/tests/spiffworkflow_backend/helpers/example_data.py b/tests/spiffworkflow_backend/helpers/example_data.py index befd2602..4b0ee5fc 100644 --- a/tests/spiffworkflow_backend/helpers/example_data.py +++ b/tests/spiffworkflow_backend/helpers/example_data.py @@ -36,10 +36,8 @@ class ExampleDataLoader: display_name=display_name, description=description, display_order=display_order, - is_review=False, ) - workflow_spec_service = ProcessModelService() - workflow_spec_service.add_process_model(spec) + ProcessModelService.add_process_model(spec) bpmn_file_name_with_extension = bpmn_file_name if not bpmn_file_name_with_extension: @@ -88,7 +86,7 @@ class ExampleDataLoader: ) spec.primary_process_id = references[0].identifier spec.primary_file_name = filename - ProcessModelService().save_process_model(spec) + ProcessModelService.save_process_model(spec) finally: if file: file.close() diff --git a/tests/spiffworkflow_backend/integration/test_logging_service.py b/tests/spiffworkflow_backend/integration/test_logging_service.py index 97dafaf3..f9dd4452 100644 --- a/tests/spiffworkflow_backend/integration/test_logging_service.py +++ b/tests/spiffworkflow_backend/integration/test_logging_service.py @@ -51,13 +51,13 @@ class TestLoggingService(BaseTest): assert response.json is not None process_instance_id = response.json["id"] response = client.post( - f"/v1.0/process-instances/{process_instance_id}/run", + f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}/run", headers=headers, ) assert response.status_code == 200 log_response = client.get( - f"/v1.0/process-instances/{process_instance_id}/logs", + f"/v1.0/logs/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}", headers=headers, ) assert log_response.status_code == 200 diff --git a/tests/spiffworkflow_backend/integration/test_nested_groups.py b/tests/spiffworkflow_backend/integration/test_nested_groups.py index 3a12acf6..3983f9be 100644 --- a/tests/spiffworkflow_backend/integration/test_nested_groups.py +++ b/tests/spiffworkflow_backend/integration/test_nested_groups.py @@ -46,7 +46,7 @@ class TestNestedGroups(BaseTest): process_instance_id = response.json["id"] client.post( - f"/v1.0/process-instances/{process_instance_id}/run", + f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}/run", headers=self.logged_in_headers(with_super_admin_user), ) process_instance = ProcessInstanceService().get_process_instance( diff --git a/tests/spiffworkflow_backend/integration/test_openid_blueprint.py b/tests/spiffworkflow_backend/integration/test_openid_blueprint.py new file mode 100644 index 00000000..20a0bb67 --- /dev/null +++ b/tests/spiffworkflow_backend/integration/test_openid_blueprint.py @@ -0,0 +1,61 @@ +"""Test_authentication.""" +from flask import Flask +from flask.testing import FlaskClient +from tests.spiffworkflow_backend.helpers.base_test import BaseTest + + +class TestFlaskOpenId(BaseTest): + """An integrated Open ID that responds to openID requests. + + By referencing a build in YAML file. Useful for + local development, testing, demos etc... + """ + + def test_discovery_of_endpoints( + self, + app: Flask, + client: FlaskClient, + with_db_and_bpmn_file_cleanup: None, + ) -> None: + """Test discovery endpoints.""" + response = client.get("/openid/.well-known/openid-configuration") + discovered_urls = response.json + assert "http://localhost/openid" == discovered_urls["issuer"] + assert ( + "http://localhost/openid/auth" == discovered_urls["authorization_endpoint"] + ) + assert "http://localhost/openid/token" == discovered_urls["token_endpoint"] + + def test_get_login_page( + self, + app: Flask, + client: FlaskClient, + with_db_and_bpmn_file_cleanup: None, + ) -> None: + """It should be possible to get to a login page.""" + data = {"state": {"bubblegum": 1, "daydream": 2}} + response = client.get("/openid/auth", query_string=data) + assert b"

Login

" in response.data + assert b"bubblegum" in response.data + + def test_get_token( + self, + app: Flask, + client: FlaskClient, + with_db_and_bpmn_file_cleanup: None, + ) -> None: + """It should be possible to get a token.""" + code = ( + "c3BpZmZ3b3JrZmxvdy1iYWNrZW5kOkpYZVFFeG0wSmhRUEx1bWdIdElJcWY1MmJEYWxIejBx" + ) + headers = { + "Content-Type": "application/x-www-form-urlencoded", + "Authorization": f"Basic {code}", + } + data = { + "grant_type": "authorization_code", + "code": code, + "redirect_url": "http://localhost:7000/v1.0/login_return", + } + response = client.post("/openid/token", data=data, headers=headers) + assert response diff --git a/tests/spiffworkflow_backend/integration/test_process_api.py b/tests/spiffworkflow_backend/integration/test_process_api.py index fbbf7deb..0070c5c9 100644 --- a/tests/spiffworkflow_backend/integration/test_process_api.py +++ b/tests/spiffworkflow_backend/integration/test_process_api.py @@ -20,6 +20,9 @@ from spiffworkflow_backend.models.group import GroupModel from spiffworkflow_backend.models.process_group import ProcessGroup from spiffworkflow_backend.models.process_instance import ProcessInstanceModel from spiffworkflow_backend.models.process_instance import ProcessInstanceStatus +from spiffworkflow_backend.models.process_instance_metadata import ( + ProcessInstanceMetadataModel, +) from spiffworkflow_backend.models.process_instance_report import ( ProcessInstanceReportModel, ) @@ -133,12 +136,12 @@ class TestProcessApi(BaseTest): process_model_description=model_description, user=with_super_admin_user, ) - process_model = ProcessModelService().get_process_model( + process_model = ProcessModelService.get_process_model( process_model_identifier, ) assert model_display_name == process_model.display_name assert 0 == process_model.display_order - assert 1 == len(ProcessModelService().get_process_groups()) + assert 1 == len(ProcessModelService.get_process_groups()) # add bpmn file to the model bpmn_file_name = "sample.bpmn" @@ -155,9 +158,7 @@ class TestProcessApi(BaseTest): user=with_super_admin_user, ) # get the model, assert that primary is set - process_model = ProcessModelService().get_process_model( - process_model_identifier - ) + process_model = ProcessModelService.get_process_model(process_model_identifier) assert process_model.primary_file_name == bpmn_file_name assert process_model.primary_process_id == "sample" @@ -208,9 +209,7 @@ class TestProcessApi(BaseTest): headers=self.logged_in_headers(with_super_admin_user), ) assert response.status_code == 200 - process_model = ProcessModelService().get_process_model( - process_model_identifier - ) + process_model = ProcessModelService.get_process_model(process_model_identifier) assert process_model.primary_file_name == bpmn_file_name assert process_model.primary_process_id == terminal_primary_process_id @@ -236,9 +235,7 @@ class TestProcessApi(BaseTest): ) # assert we have a model - process_model = ProcessModelService().get_process_model( - process_model_identifier - ) + process_model = ProcessModelService.get_process_model(process_model_identifier) assert process_model is not None assert process_model.id == process_model_identifier @@ -254,7 +251,7 @@ class TestProcessApi(BaseTest): # assert we no longer have a model with pytest.raises(ProcessEntityNotFoundError): - ProcessModelService().get_process_model(process_model_identifier) + ProcessModelService.get_process_model(process_model_identifier) def test_process_model_delete_with_instances( self, @@ -327,19 +324,18 @@ class TestProcessApi(BaseTest): process_model_id=process_model_identifier, user=with_super_admin_user, ) - process_model = ProcessModelService().get_process_model( - process_model_identifier - ) + process_model = ProcessModelService.get_process_model(process_model_identifier) assert process_model.id == process_model_identifier assert process_model.display_name == "Cooooookies" - assert process_model.is_review is False assert process_model.primary_file_name is None assert process_model.primary_process_id is None process_model.display_name = "Updated Display Name" process_model.primary_file_name = "superduper.bpmn" process_model.primary_process_id = "superduper" - process_model.is_review = True # not in the include list, so get ignored + process_model.metadata_extraction_paths = [ + {"key": "extraction1", "path": "path1"} + ] modified_process_model_identifier = process_model_identifier.replace("/", ":") response = client.put( @@ -353,7 +349,44 @@ class TestProcessApi(BaseTest): assert response.json["display_name"] == "Updated Display Name" assert response.json["primary_file_name"] == "superduper.bpmn" assert response.json["primary_process_id"] == "superduper" - assert response.json["is_review"] is False + assert response.json["metadata_extraction_paths"] == [ + {"key": "extraction1", "path": "path1"} + ] + + def test_process_model_list_all( + self, + app: Flask, + client: FlaskClient, + with_db_and_bpmn_file_cleanup: None, + with_super_admin_user: UserModel, + ) -> None: + """Test_process_model_list_all.""" + group_id = "test_group/test_sub_group" + self.create_process_group(client, with_super_admin_user, group_id) + + # add 5 models to the group + for i in range(5): + process_model_identifier = f"{group_id}/test_model_{i}" + model_display_name = f"Test Model {i}" + model_description = f"Test Model {i} Description" + 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, + ) + + # get all models + response = client.get( + "/v1.0/process-models?per_page=1000&recursive=true", + headers=self.logged_in_headers(with_super_admin_user), + ) + assert response.json is not None + assert len(response.json["results"]) == 5 + assert response.json["pagination"]["count"] == 5 + assert response.json["pagination"]["total"] == 5 + assert response.json["pagination"]["pages"] == 1 def test_process_model_list( self, @@ -515,7 +548,7 @@ class TestProcessApi(BaseTest): assert result.description == "Test Description" # Check what is persisted - persisted = ProcessModelService().get_process_group("test") + persisted = ProcessModelService.get_process_group("test") assert persisted.display_name == "Another Test Category" assert persisted.id == "test" assert persisted.description == "Test Description" @@ -537,7 +570,7 @@ class TestProcessApi(BaseTest): process_group_id, display_name=process_group_display_name, ) - persisted = ProcessModelService().get_process_group(process_group_id) + persisted = ProcessModelService.get_process_group(process_group_id) assert persisted is not None assert persisted.id == process_group_id @@ -547,7 +580,7 @@ class TestProcessApi(BaseTest): ) with pytest.raises(ProcessEntityNotFoundError): - ProcessModelService().get_process_group(process_group_id) + ProcessModelService.get_process_group(process_group_id) def test_process_group_update( self, @@ -563,7 +596,7 @@ class TestProcessApi(BaseTest): self.create_process_group( client, with_super_admin_user, group_id, display_name=group_display_name ) - process_group = ProcessModelService().get_process_group(group_id) + process_group = ProcessModelService.get_process_group(group_id) assert process_group.display_name == group_display_name @@ -577,7 +610,7 @@ class TestProcessApi(BaseTest): ) assert response.status_code == 200 - process_group = ProcessModelService().get_process_group(group_id) + process_group = ProcessModelService.get_process_group(group_id) assert process_group.display_name == "Modified Display Name" def test_process_group_list( @@ -879,7 +912,7 @@ class TestProcessApi(BaseTest): modified_process_model_identifier = process_model_identifier.replace("/", ":") response = client.post( - f"/v1.0/process-models/{modified_process_model_identifier}/process-instances", + f"/v1.0/process-instances/{modified_process_model_identifier}", headers=self.logged_in_headers(with_super_admin_user), ) assert response.status_code == 201 @@ -944,6 +977,43 @@ class TestProcessApi(BaseTest): assert response.json is not None assert response.json["id"] == process_group_id assert response.json["process_models"][0]["id"] == process_model_identifier + assert response.json["parent_groups"] == [] + + def test_get_process_group_show_when_nested( + self, + app: Flask, + client: FlaskClient, + with_db_and_bpmn_file_cleanup: None, + with_super_admin_user: UserModel, + ) -> None: + """Test_get_process_group_show_when_nested.""" + self.create_group_and_model_with_bpmn( + client=client, + user=with_super_admin_user, + process_group_id="test_group_one", + process_model_id="simple_form", + bpmn_file_location="simple_form", + ) + + self.create_group_and_model_with_bpmn( + client=client, + user=with_super_admin_user, + process_group_id="test_group_one/test_group_two", + process_model_id="call_activity_nested", + bpmn_file_location="call_activity_nested", + ) + + response = client.get( + "/v1.0/process-groups/test_group_one:test_group_two", + headers=self.logged_in_headers(with_super_admin_user), + ) + + assert response.status_code == 200 + assert response.json is not None + assert response.json["id"] == "test_group_one/test_group_two" + assert response.json["parent_groups"] == [ + {"display_name": "test_group_one", "id": "test_group_one"} + ] def test_get_process_model_when_found( self, @@ -962,11 +1032,15 @@ class TestProcessApi(BaseTest): f"/v1.0/process-models/{modified_process_model_identifier}", headers=self.logged_in_headers(with_super_admin_user), ) + assert response.status_code == 200 assert response.json is not None assert response.json["id"] == process_model_identifier assert len(response.json["files"]) == 1 assert response.json["files"][0]["name"] == "random_fact.bpmn" + assert response.json["parent_groups"] == [ + {"display_name": "test_group", "id": "test_group"} + ] def test_get_process_model_when_not_found( self, @@ -1034,7 +1108,7 @@ class TestProcessApi(BaseTest): assert response.json is not None process_instance_id = response.json["id"] response = client.post( - f"/v1.0/process-instances/{process_instance_id}/run", + f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}/run", headers=self.logged_in_headers(with_super_admin_user), ) @@ -1066,7 +1140,9 @@ class TestProcessApi(BaseTest): process_group_id=process_group_id, process_model_id=process_model_id, ) - modified_process_model_identifier = process_model_identifier.replace("/", ":") + modified_process_model_identifier = ( + self.modify_process_identifier_for_path_param(process_model_identifier) + ) headers = self.logged_in_headers(with_super_admin_user) create_response = self.create_process_instance_from_process_model_id( client, process_model_identifier, headers @@ -1074,14 +1150,15 @@ class TestProcessApi(BaseTest): assert create_response.json is not None process_instance_id = create_response.json["id"] client.post( - f"/v1.0/process-instances/{process_instance_id}/run", + f"/v1.0/process-instances/{modified_process_model_identifier}/{process_instance_id}/run", headers=self.logged_in_headers(with_super_admin_user), ) show_response = client.get( - f"/v1.0/process-models/{modified_process_model_identifier}/process-instances/{process_instance_id}", + f"/v1.0/process-instances/{modified_process_model_identifier}/{process_instance_id}", headers=self.logged_in_headers(with_super_admin_user), ) assert show_response.json is not None + assert show_response.status_code == 200 file_system_root = FileSystemService.root_path() file_path = ( f"{file_system_root}/{process_model_identifier}/{process_model_id}.bpmn" @@ -1177,7 +1254,7 @@ class TestProcessApi(BaseTest): process_instance_id = response.json["id"] response = client.post( - f"/v1.0/process-instances/{process_instance_id}/run", + f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}/run", headers=self.logged_in_headers(with_super_admin_user), ) @@ -1237,14 +1314,14 @@ class TestProcessApi(BaseTest): process_instance_id = response.json["id"] response = client.post( - f"/v1.0/process-instances/{process_instance_id}/run", + f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}/run", headers=self.logged_in_headers(with_super_admin_user), ) assert response.status_code == 200 assert response.json is not None response = client.post( - f"/v1.0/process-instances/{process_instance_id}/terminate", + f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}/terminate", headers=self.logged_in_headers(with_super_admin_user), ) assert response.status_code == 200 @@ -1285,13 +1362,13 @@ class TestProcessApi(BaseTest): process_instance_id = response.json["id"] response = client.post( - f"/v1.0/process-instances/{process_instance_id}/run", + f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}/run", headers=self.logged_in_headers(with_super_admin_user), ) assert response.json is not None delete_response = client.delete( - f"/v1.0/process-instances/{process_instance_id}", + f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}", headers=self.logged_in_headers(with_super_admin_user), ) assert delete_response.status_code == 200 @@ -1324,7 +1401,7 @@ class TestProcessApi(BaseTest): process_instance_id = response.json["id"] response = client.post( - f"/v1.0/process-instances/{process_instance_id}/run", + f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}/run", headers=self.logged_in_headers(with_super_admin_user), ) @@ -1481,7 +1558,7 @@ class TestProcessApi(BaseTest): status=ProcessInstanceStatus[statuses[i]].value, process_initiator=with_super_admin_user, process_model_identifier=process_model_identifier, - process_group_identifier="test_process_group_id", + process_model_display_name=process_model_identifier, updated_at_in_seconds=round(time.time()), start_in_seconds=(1000 * i) + 1000, end_in_seconds=(1000 * i) + 2000, @@ -1656,14 +1733,14 @@ class TestProcessApi(BaseTest): ], } - ProcessInstanceReportModel.create_with_attributes( + report = ProcessInstanceReportModel.create_with_attributes( identifier="sure", report_metadata=report_metadata, user=with_super_admin_user, ) response = client.get( - "/v1.0/process-instances/reports/sure", + f"/v1.0/process-instances/reports/{report.id}", headers=self.logged_in_headers(with_super_admin_user), ) assert response.status_code == 200 @@ -1702,14 +1779,14 @@ class TestProcessApi(BaseTest): ], } - ProcessInstanceReportModel.create_with_attributes( + report = ProcessInstanceReportModel.create_with_attributes( identifier="sure", report_metadata=report_metadata, user=with_super_admin_user, ) response = client.get( - "/v1.0/process-instances/reports/sure?grade_level=1", + f"/v1.0/process-instances/reports/{report.id}?grade_level=1", headers=self.logged_in_headers(with_super_admin_user), ) assert response.status_code == 200 @@ -1724,9 +1801,9 @@ class TestProcessApi(BaseTest): with_super_admin_user: UserModel, setup_process_instances_for_reports: list[ProcessInstanceModel], ) -> None: - """Test_process_instance_report_show_with_default_list.""" + """Test_process_instance_report_show_with_bad_identifier.""" response = client.get( - "/v1.0/process-instances/reports/sure?grade_level=1", + "/v1.0/process-instances/reports/13000000?grade_level=1", headers=self.logged_in_headers(with_super_admin_user), ) assert response.status_code == 404 @@ -1783,7 +1860,7 @@ class TestProcessApi(BaseTest): assert process.status == "not_started" response = client.post( - f"/v1.0/process-instances/{process_instance_id}/run", + f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}/run", headers=self.logged_in_headers(with_super_admin_user), ) assert response.status_code == 400 @@ -1827,10 +1904,8 @@ class TestProcessApi(BaseTest): process_instance_id = self.setup_testing_instance( client, process_model_identifier, with_super_admin_user ) - process_model = ProcessModelService().get_process_model( - process_model_identifier - ) - ProcessModelService().update_process_model( + process_model = ProcessModelService.get_process_model(process_model_identifier) + ProcessModelService.update_process_model( process_model, {"fault_or_suspend_on_exception": NotificationType.suspend.value}, ) @@ -1844,7 +1919,7 @@ class TestProcessApi(BaseTest): assert process.status == "not_started" response = client.post( - f"/v1.0/process-instances/{process_instance_id}/run", + f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}/run", headers=self.logged_in_headers(with_super_admin_user), ) assert response.status_code == 400 @@ -1882,10 +1957,8 @@ class TestProcessApi(BaseTest): client, process_model_identifier, with_super_admin_user ) - process_model = ProcessModelService().get_process_model( - process_model_identifier - ) - ProcessModelService().update_process_model( + process_model = ProcessModelService.get_process_model(process_model_identifier) + ProcessModelService.update_process_model( process_model, {"exception_notification_addresses": ["with_super_admin_user@example.com"]}, ) @@ -1894,7 +1967,7 @@ class TestProcessApi(BaseTest): with mail.record_messages() as outbox: response = client.post( - f"/v1.0/process-instances/{process_instance_id}/run", + f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}/run", headers=self.logged_in_headers(with_super_admin_user), ) assert response.status_code == 400 @@ -2079,7 +2152,7 @@ class TestProcessApi(BaseTest): assert response.json is not None process_instance_id = response.json["id"] response = client.post( - f"/v1.0/process-instances/{process_instance_id}/run", + f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}/run", headers=self.logged_in_headers(initiator_user), ) assert response.status_code == 200 @@ -2284,7 +2357,7 @@ class TestProcessApi(BaseTest): process_instance_id = response.json["id"] client.post( - f"/v1.0/process-instances/{process_instance_id}/run", + f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}/run", headers=self.logged_in_headers(with_super_admin_user), ) @@ -2294,7 +2367,7 @@ class TestProcessApi(BaseTest): assert process_instance.status == "user_input_required" client.post( - f"/v1.0/process-instances/{process_instance_id}/suspend", + f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}/suspend", headers=self.logged_in_headers(with_super_admin_user), ) process_instance = ProcessInstanceService().get_process_instance( @@ -2304,7 +2377,7 @@ class TestProcessApi(BaseTest): # TODO: Why can I run a suspended process instance? response = client.post( - f"/v1.0/process-instances/{process_instance_id}/run", + f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}/run", headers=self.logged_in_headers(with_super_admin_user), ) @@ -2369,3 +2442,425 @@ class TestProcessApi(BaseTest): ) print("test_script_unit_test_run") + + def setup_initial_groups_for_move_tests( + self, client: FlaskClient, with_super_admin_user: UserModel + ) -> None: + """Setup_initial_groups_for_move_tests.""" + groups = ["group_a", "group_b", "group_b/group_bb"] + # setup initial groups + for group in groups: + self.create_process_group( + client, with_super_admin_user, group, display_name=group + ) + # make sure initial groups exist + for group in groups: + persisted = ProcessModelService.get_process_group(group) + assert persisted is not None + assert persisted.id == group + + def test_move_model( + self, + app: Flask, + client: FlaskClient, + with_db_and_bpmn_file_cleanup: None, + with_super_admin_user: UserModel, + ) -> None: + """Test_move_model.""" + self.setup_initial_groups_for_move_tests(client, with_super_admin_user) + + process_model_id = "test_model" + original_location = "group_a" + original_process_model_path = f"{original_location}/{process_model_id}" + + # add model to `group_a` + self.create_process_model_with_api( + client, + original_process_model_path, + user=with_super_admin_user, + process_model_display_name=process_model_id, + process_model_description=process_model_id, + ) + persisted = ProcessModelService.get_process_model(original_process_model_path) + assert persisted is not None + assert persisted.id == original_process_model_path + + # move model to `group_b/group_bb` + new_location = "group_b/group_bb" + new_process_model_path = f"{new_location}/{process_model_id}" + modified_original_process_model_id = original_process_model_path.replace( + "/", ":" + ) + + response = client.put( + f"/v1.0/process-models/{modified_original_process_model_id}/move?new_location={new_location}", + headers=self.logged_in_headers(with_super_admin_user), + ) + assert response.status_code == 201 + assert response.json["id"] == new_process_model_path + + # make sure the original model does not exist + with pytest.raises(ProcessEntityNotFoundError) as e: + ProcessModelService.get_process_model(original_process_model_path) + assert e.value.args[0] == "process_model_not_found" + + # make sure the new model does exist + new_process_model = ProcessModelService.get_process_model( + new_process_model_path + ) + assert new_process_model is not None + assert new_process_model.id == new_process_model_path + + def test_move_group( + self, + app: Flask, + client: FlaskClient, + with_db_and_bpmn_file_cleanup: None, + with_super_admin_user: UserModel, + ) -> None: + """Test_move_group.""" + self.setup_initial_groups_for_move_tests(client, with_super_admin_user) + + # add sub group to `group_a` + sub_group_id = "sub_group" + original_location = "group_a" + original_sub_path = f"{original_location}/{sub_group_id}" + self.create_process_group( + client, with_super_admin_user, original_sub_path, display_name=sub_group_id + ) + # make sure original subgroup exists + persisted = ProcessModelService.get_process_group(original_sub_path) + assert persisted is not None + assert persisted.id == original_sub_path + + # move sub_group to `group_b/group_bb` + new_location = "group_b/group_bb" + new_sub_path = f"{new_location}/{sub_group_id}" + modified_original_process_group_id = original_sub_path.replace("/", ":") + response = client.put( + f"/v1.0/process-groups/{modified_original_process_group_id}/move?new_location={new_location}", + headers=self.logged_in_headers(with_super_admin_user), + ) + assert response.status_code == 201 + assert response.json["id"] == new_sub_path + + # make sure the original subgroup does not exist + with pytest.raises(ProcessEntityNotFoundError) as e: + ProcessModelService.get_process_group(original_sub_path) + + assert e.value.args[0] == "process_group_not_found" + assert e.value.args[1] == f"Process Group Id: {original_sub_path}" + + # make sure the new subgroup does exist + new_process_group = ProcessModelService.get_process_group(new_sub_path) + assert new_process_group.id == new_sub_path + + # this doesn't work in CI + # assert "Initial Commit" in output + # def test_process_model_publish( + # self, + # app: Flask, + # client: FlaskClient, + # with_db_and_bpmn_file_cleanup: None, + # with_super_admin_user: UserModel, + # ) -> None: + # """Test_process_model_publish.""" + # bpmn_root = FileSystemService.root_path() + # shell_command = ["git", "init", "--initial-branch=main", bpmn_root] + # output = GitService.run_shell_command_to_get_stdout(shell_command) + # assert output == f"Initialized empty Git repository in {bpmn_root}/.git/\n" + # with FileSystemService.cd(bpmn_root): + # output = GitService.run_shell_command_to_get_stdout(["git", "status"]) + # assert "On branch main" in output + # assert "No commits yet" in output + # assert ( + # 'nothing to commit (create/copy files and use "git add" to track)' + # in output + # ) + # + # process_group_id = "test_group" + # self.create_process_group( + # client, with_super_admin_user, process_group_id, process_group_id + # ) + # + # sub_process_group_id = "test_group/test_sub_group" + # process_model_id = "hello_world" + # bpmn_file_name = "hello_world.bpmn" + # bpmn_file_location = "hello_world" + # process_model_identifier = self.create_group_and_model_with_bpmn( + # client=client, + # user=with_super_admin_user, + # process_group_id=sub_process_group_id, + # process_model_id=process_model_id, + # bpmn_file_name=bpmn_file_name, + # bpmn_file_location=bpmn_file_location, + # ) + # process_model_absolute_dir = os.path.join( + # bpmn_root, process_model_identifier + # ) + # + # output = GitService.run_shell_command_to_get_stdout(["git", "status"]) + # test_string = 'Untracked files:\n (use "git add ..." to include in what will be committed)\n\ttest_group' + # assert test_string in output + # + # os.system("git add .") + # output = os.popen("git commit -m 'Initial Commit'").read() + # assert "Initial Commit" in output + # assert "4 files changed" in output + # assert "test_group/process_group.json" in output + # assert "test_group/test_sub_group/hello_world/hello_world.bpmn" in output + # assert "test_group/test_sub_group/hello_world/process_model.json" in output + # assert "test_group/test_sub_group/process_group.json" in output + # + # output = GitService.run_shell_command_to_get_stdout(["git", "status"]) + # assert "On branch main" in output + # assert "nothing to commit" in output + # assert "working tree clean" in output + # + # output = os.popen("git branch --list").read() # noqa: S605 + # assert output == "* main\n" + # os.system("git branch staging") + # output = os.popen("git branch --list").read() # noqa: S605 + # assert output == "* main\n staging\n" + # + # os.system("git checkout staging") + # + # output = GitService.run_shell_command_to_get_stdout(["git", "status"]) + # assert "On branch staging" in output + # assert "nothing to commit" in output + # assert "working tree clean" in output + # + # # process_model = ProcessModelService.get_process_model(process_model_identifier) + # + # listing = os.listdir(process_model_absolute_dir) + # assert len(listing) == 2 + # assert "hello_world.bpmn" in listing + # assert "process_model.json" in listing + # + # os.system("git checkout main") + # + # output = GitService.run_shell_command_to_get_stdout(["git", "status"]) + # assert "On branch main" in output + # assert "nothing to commit" in output + # assert "working tree clean" in output + # + # file_data = b"abc123" + # new_file_path = os.path.join(process_model_absolute_dir, "new_file.txt") + # with open(new_file_path, "wb") as f_open: + # f_open.write(file_data) + # + # output = GitService.run_shell_command_to_get_stdout(["git", "status"]) + # assert "On branch main" in output + # assert "Untracked files:" in output + # assert "test_group/test_sub_group/hello_world/new_file.txt" in output + # + # os.system( + # "git add test_group/test_sub_group/hello_world/new_file.txt" + # ) # noqa: S605 + # output = os.popen("git commit -m 'add new_file.txt'").read() # noqa: S605 + # + # assert "add new_file.txt" in output + # assert "1 file changed, 1 insertion(+)" in output + # assert "test_group/test_sub_group/hello_world/new_file.txt" in output + # + # listing = os.listdir(process_model_absolute_dir) + # assert len(listing) == 3 + # assert "hello_world.bpmn" in listing + # assert "process_model.json" in listing + # assert "new_file.txt" in listing + # + # # modified_process_model_id = process_model_identifier.replace("/", ":") + # # response = client.post( + # # f"/v1.0/process-models/{modified_process_model_id}/publish?branch_to_update=staging", + # # headers=self.logged_in_headers(with_super_admin_user), + # # ) + # + # print("test_process_model_publish") + + def test_can_get_process_instance_list_with_report_metadata( + self, + app: Flask, + client: FlaskClient, + with_db_and_bpmn_file_cleanup: None, + with_super_admin_user: UserModel, + ) -> None: + """Test_can_get_process_instance_list_with_report_metadata.""" + process_model = load_test_spec( + process_model_id="save_process_instance_metadata/save_process_instance_metadata", + bpmn_file_name="save_process_instance_metadata.bpmn", + process_model_source_directory="save_process_instance_metadata", + ) + process_instance = self.create_process_instance_from_process_model( + process_model=process_model, user=with_super_admin_user + ) + + processor = ProcessInstanceProcessor(process_instance) + processor.do_engine_steps(save=True) + process_instance_metadata = ProcessInstanceMetadataModel.query.filter_by( + process_instance_id=process_instance.id + ).all() + assert len(process_instance_metadata) == 3 + + report_metadata = { + "columns": [ + {"Header": "ID", "accessor": "id"}, + {"Header": "Status", "accessor": "status"}, + {"Header": "Key One", "accessor": "key1"}, + {"Header": "Key Two", "accessor": "key2"}, + ], + "order_by": ["status"], + "filter_by": [], + } + process_instance_report = ProcessInstanceReportModel.create_with_attributes( + identifier="sure", + report_metadata=report_metadata, + user=with_super_admin_user, + ) + + response = client.get( + f"/v1.0/process-instances?report_identifier={process_instance_report.identifier}", + headers=self.logged_in_headers(with_super_admin_user), + ) + + assert response.json is not None + assert response.status_code == 200 + + assert len(response.json["results"]) == 1 + assert response.json["results"][0]["status"] == "complete" + assert response.json["results"][0]["id"] == process_instance.id + assert response.json["results"][0]["key1"] == "value1" + assert response.json["results"][0]["key2"] == "value2" + assert response.json["pagination"]["count"] == 1 + assert response.json["pagination"]["pages"] == 1 + assert response.json["pagination"]["total"] == 1 + + def test_can_get_process_instance_report_column_list( + self, + app: Flask, + client: FlaskClient, + with_db_and_bpmn_file_cleanup: None, + with_super_admin_user: UserModel, + ) -> None: + """Test_can_get_process_instance_list_with_report_metadata.""" + process_model = load_test_spec( + process_model_id="save_process_instance_metadata/save_process_instance_metadata", + bpmn_file_name="save_process_instance_metadata.bpmn", + process_model_source_directory="save_process_instance_metadata", + ) + process_instance = self.create_process_instance_from_process_model( + process_model=process_model, user=with_super_admin_user + ) + + processor = ProcessInstanceProcessor(process_instance) + processor.do_engine_steps(save=True) + process_instance_metadata = ProcessInstanceMetadataModel.query.filter_by( + process_instance_id=process_instance.id + ).all() + assert len(process_instance_metadata) == 3 + + response = client.get( + "/v1.0/process-instances/reports/columns", + headers=self.logged_in_headers(with_super_admin_user), + ) + + assert response.json is not None + assert response.status_code == 200 + assert response.json == [ + {"Header": "Id", "accessor": "id", "filterable": False}, + { + "Header": "Process", + "accessor": "process_model_display_name", + "filterable": False, + }, + {"Header": "Start", "accessor": "start_in_seconds", "filterable": False}, + {"Header": "End", "accessor": "end_in_seconds", "filterable": False}, + {"Header": "Username", "accessor": "username", "filterable": False}, + {"Header": "Status", "accessor": "status", "filterable": False}, + {"Header": "key1", "accessor": "key1", "filterable": True}, + {"Header": "key2", "accessor": "key2", "filterable": True}, + {"Header": "key3", "accessor": "key3", "filterable": True}, + ] + + def test_process_instance_list_can_order_by_metadata( + self, + app: Flask, + client: FlaskClient, + with_db_and_bpmn_file_cleanup: None, + with_super_admin_user: UserModel, + ) -> None: + """Test_process_instance_list_can_order_by_metadata.""" + self.create_process_group( + client, with_super_admin_user, "test_group", "test_group" + ) + process_model = load_test_spec( + "test_group/hello_world", + process_model_source_directory="nested-task-data-structure", + ) + ProcessModelService.update_process_model( + process_model, + { + "metadata_extraction_paths": [ + {"key": "time_ns", "path": "outer.time"}, + ] + }, + ) + + process_instance_one = self.create_process_instance_from_process_model( + process_model + ) + processor = ProcessInstanceProcessor(process_instance_one) + processor.do_engine_steps(save=True) + assert process_instance_one.status == "complete" + process_instance_two = self.create_process_instance_from_process_model( + process_model + ) + processor = ProcessInstanceProcessor(process_instance_two) + processor.do_engine_steps(save=True) + assert process_instance_two.status == "complete" + + report_metadata = { + "columns": [ + {"Header": "id", "accessor": "id"}, + {"Header": "Time", "accessor": "time_ns"}, + ], + "order_by": ["time_ns"], + } + report_one = ProcessInstanceReportModel.create_with_attributes( + identifier="report_one", + report_metadata=report_metadata, + user=with_super_admin_user, + ) + + response = client.get( + f"/v1.0/process-instances?report_id={report_one.id}", + headers=self.logged_in_headers(with_super_admin_user), + ) + assert response.status_code == 200 + assert response.json is not None + assert len(response.json["results"]) == 2 + assert response.json["results"][0]["id"] == process_instance_one.id + assert response.json["results"][1]["id"] == process_instance_two.id + + report_metadata = { + "columns": [ + {"Header": "id", "accessor": "id"}, + {"Header": "Time", "accessor": "time_ns"}, + ], + "order_by": ["-time_ns"], + } + report_two = ProcessInstanceReportModel.create_with_attributes( + identifier="report_two", + report_metadata=report_metadata, + user=with_super_admin_user, + ) + + response = client.get( + f"/v1.0/process-instances?report_id={report_two.id}", + headers=self.logged_in_headers(with_super_admin_user), + ) + + assert response.status_code == 200 + assert response.json is not None + assert len(response.json["results"]) == 2 + assert response.json["results"][1]["id"] == process_instance_one.id + assert response.json["results"][0]["id"] == process_instance_two.id diff --git a/tests/spiffworkflow_backend/integration/test_secret_service.py b/tests/spiffworkflow_backend/integration/test_secret_service.py index 071ef6cc..c71f67f2 100644 --- a/tests/spiffworkflow_backend/integration/test_secret_service.py +++ b/tests/spiffworkflow_backend/integration/test_secret_service.py @@ -52,7 +52,7 @@ class SecretServiceTestHelpers(BaseTest): process_model_description=self.test_process_model_description, user=user, ) - process_model_info = ProcessModelService().get_process_model( + process_model_info = ProcessModelService.get_process_model( process_model_identifier ) return process_model_info diff --git a/tests/spiffworkflow_backend/scripts/test_save_process_instance_metadata.py b/tests/spiffworkflow_backend/scripts/test_save_process_instance_metadata.py new file mode 100644 index 00000000..96eb6297 --- /dev/null +++ b/tests/spiffworkflow_backend/scripts/test_save_process_instance_metadata.py @@ -0,0 +1,45 @@ +"""Test_get_localtime.""" +from flask.app import Flask +from flask.testing import FlaskClient +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_metadata import ( + ProcessInstanceMetadataModel, +) +from spiffworkflow_backend.models.user import UserModel +from spiffworkflow_backend.services.process_instance_processor import ( + ProcessInstanceProcessor, +) + + +class TestSaveProcessInstanceMetadata(BaseTest): + """TestSaveProcessInstanceMetadata.""" + + def test_can_save_process_instance_metadata( + self, + app: Flask, + client: FlaskClient, + with_db_and_bpmn_file_cleanup: None, + with_super_admin_user: UserModel, + ) -> None: + """Test_can_save_process_instance_metadata.""" + initiator_user = self.find_or_create_user("initiator_user") + self.create_process_group( + client, with_super_admin_user, "test_group", "test_group" + ) + process_model = load_test_spec( + process_model_id="save_process_instance_metadata/save_process_instance_metadata", + bpmn_file_name="save_process_instance_metadata.bpmn", + process_model_source_directory="save_process_instance_metadata", + ) + process_instance = self.create_process_instance_from_process_model( + process_model=process_model, user=initiator_user + ) + processor = ProcessInstanceProcessor(process_instance) + processor.do_engine_steps(save=True) + + process_instance_metadata = ProcessInstanceMetadataModel.query.filter_by( + process_instance_id=process_instance.id + ).all() + assert len(process_instance_metadata) == 3 diff --git a/tests/spiffworkflow_backend/unit/test_acceptance_test_fixtures.py b/tests/spiffworkflow_backend/unit/test_acceptance_test_fixtures.py index 1d515712..c738c7f6 100644 --- a/tests/spiffworkflow_backend/unit/test_acceptance_test_fixtures.py +++ b/tests/spiffworkflow_backend/unit/test_acceptance_test_fixtures.py @@ -1,13 +1,38 @@ """Test_acceptance_test_fixtures.""" +import os + from flask.app import Flask +from spiffworkflow_backend.models.process_group import ProcessGroup +from spiffworkflow_backend.models.process_model import ProcessModelInfo from spiffworkflow_backend.services.acceptance_test_fixtures import ( load_acceptance_test_fixtures, ) +from spiffworkflow_backend.services.process_model_service import ProcessModelService def test_start_dates_are_one_hour_apart(app: Flask) -> None: """Test_start_dates_are_one_hour_apart.""" + process_model_identifier = ( + "misc/acceptance-tests-group-one/acceptance-tests-model-1" + ) + group_identifier = os.path.dirname(process_model_identifier) + parent_group_identifier = os.path.dirname(group_identifier) + if not ProcessModelService.is_group(parent_group_identifier): + process_group = ProcessGroup( + id=parent_group_identifier, display_name=parent_group_identifier + ) + ProcessModelService.add_process_group(process_group) + if not ProcessModelService.is_group(group_identifier): + process_group = ProcessGroup(id=group_identifier, display_name=group_identifier) + ProcessModelService.add_process_group(process_group) + if not ProcessModelService.is_model(process_model_identifier): + process_model = ProcessModelInfo( + id=process_model_identifier, + display_name=process_model_identifier, + description="hey", + ) + ProcessModelService.add_process_model(process_model) process_instances = load_acceptance_test_fixtures() assert len(process_instances) > 2 diff --git a/tests/spiffworkflow_backend/unit/test_authorization_service.py b/tests/spiffworkflow_backend/unit/test_authorization_service.py index 36f07743..00622a1f 100644 --- a/tests/spiffworkflow_backend/unit/test_authorization_service.py +++ b/tests/spiffworkflow_backend/unit/test_authorization_service.py @@ -113,7 +113,7 @@ class TestAuthorizationService(BaseTest): bpmn_file_location="model_with_lanes", ) - process_model = ProcessModelService().get_process_model( + process_model = ProcessModelService.get_process_model( process_model_id=process_model_identifier ) process_instance = self.create_process_instance_from_process_model( diff --git a/tests/spiffworkflow_backend/unit/test_message_instance.py b/tests/spiffworkflow_backend/unit/test_message_instance.py index 0292032e..2c091eeb 100644 --- a/tests/spiffworkflow_backend/unit/test_message_instance.py +++ b/tests/spiffworkflow_backend/unit/test_message_instance.py @@ -44,7 +44,7 @@ class TestMessageInstance(BaseTest): client, with_super_admin_user ) - process_model = ProcessModelService().get_process_model( + process_model = ProcessModelService.get_process_model( process_model_id=process_model_identifier ) process_instance = self.create_process_instance_from_process_model( @@ -81,7 +81,7 @@ class TestMessageInstance(BaseTest): client, with_super_admin_user ) - process_model = ProcessModelService().get_process_model( + process_model = ProcessModelService.get_process_model( process_model_id=process_model_identifier ) process_instance = self.create_process_instance_from_process_model( @@ -127,7 +127,7 @@ class TestMessageInstance(BaseTest): client, with_super_admin_user ) - process_model = ProcessModelService().get_process_model( + process_model = ProcessModelService.get_process_model( process_model_id=process_model_identifier ) process_instance = self.create_process_instance_from_process_model( @@ -174,7 +174,7 @@ class TestMessageInstance(BaseTest): client, with_super_admin_user ) - process_model = ProcessModelService().get_process_model( + process_model = ProcessModelService.get_process_model( process_model_id=process_model_identifier ) process_instance = self.create_process_instance_from_process_model( diff --git a/tests/spiffworkflow_backend/unit/test_message_service.py b/tests/spiffworkflow_backend/unit/test_message_service.py index aa1f2805..c012e287 100644 --- a/tests/spiffworkflow_backend/unit/test_message_service.py +++ b/tests/spiffworkflow_backend/unit/test_message_service.py @@ -47,7 +47,7 @@ class TestMessageService(BaseTest): bpmn_file_name="message_sender.bpmn", ) - process_instance_sender = ProcessInstanceService.create_process_instance( + process_instance_sender = ProcessInstanceService.create_process_instance_from_process_model_identifier( process_model_sender.id, with_super_admin_user, ) @@ -154,7 +154,7 @@ class TestMessageService(BaseTest): user = self.find_or_create_user() - process_instance_sender = ProcessInstanceService.create_process_instance( + process_instance_sender = ProcessInstanceService.create_process_instance_from_process_model_identifier( process_model_sender.id, user, # process_group_identifier=process_model_sender.process_group_id, diff --git a/tests/spiffworkflow_backend/unit/test_process_group.py b/tests/spiffworkflow_backend/unit/test_process_group.py index 6c3ad0ad..5cf8945f 100644 --- a/tests/spiffworkflow_backend/unit/test_process_group.py +++ b/tests/spiffworkflow_backend/unit/test_process_group.py @@ -9,8 +9,7 @@ def test_there_is_at_least_one_group_after_we_create_one( app: Flask, with_db_and_bpmn_file_cleanup: None ) -> None: """Test_there_is_at_least_one_group_after_we_create_one.""" - process_model_service = ProcessModelService() process_group = ProcessGroup(id="hey", display_name="sure") - process_model_service.add_process_group(process_group) - process_groups = ProcessModelService().get_process_groups() + ProcessModelService.add_process_group(process_group) + process_groups = ProcessModelService.get_process_groups() assert len(process_groups) > 0 diff --git a/tests/spiffworkflow_backend/unit/test_process_instance_processor.py b/tests/spiffworkflow_backend/unit/test_process_instance_processor.py index f0de77aa..3e010795 100644 --- a/tests/spiffworkflow_backend/unit/test_process_instance_processor.py +++ b/tests/spiffworkflow_backend/unit/test_process_instance_processor.py @@ -161,6 +161,7 @@ class TestProcessInstanceProcessor(BaseTest): ) processor = ProcessInstanceProcessor(process_instance) processor.do_engine_steps(save=True) + processor.save() assert len(process_instance.active_tasks) == 1 active_task = process_instance.active_tasks[0] @@ -241,3 +242,42 @@ class TestProcessInstanceProcessor(BaseTest): ) assert process_instance.status == ProcessInstanceStatus.complete.value + + def test_does_not_recreate_active_tasks_on_multiple_saves( + self, + app: Flask, + client: FlaskClient, + with_db_and_bpmn_file_cleanup: None, + with_super_admin_user: UserModel, + ) -> None: + """Test_sets_permission_correctly_on_active_task_when_using_dict.""" + self.create_process_group( + client, with_super_admin_user, "test_group", "test_group" + ) + initiator_user = self.find_or_create_user("initiator_user") + finance_user_three = self.find_or_create_user("testuser3") + assert initiator_user.principal is not None + assert finance_user_three.principal is not None + AuthorizationService.import_permissions_from_yaml_file() + + finance_group = GroupModel.query.filter_by(identifier="Finance Team").first() + assert finance_group is not None + + process_model = load_test_spec( + process_model_id="test_group/model_with_lanes", + bpmn_file_name="lanes_with_owner_dict.bpmn", + process_model_source_directory="model_with_lanes", + ) + process_instance = self.create_process_instance_from_process_model( + process_model=process_model, user=initiator_user + ) + processor = ProcessInstanceProcessor(process_instance) + processor.do_engine_steps(save=True) + assert len(process_instance.active_tasks) == 1 + initial_active_task_id = process_instance.active_tasks[0].id + + # save again to ensure we go attempt to process the active tasks again + processor.save() + + assert len(process_instance.active_tasks) == 1 + assert initial_active_task_id == process_instance.active_tasks[0].id diff --git a/tests/spiffworkflow_backend/unit/test_process_instance_report.py b/tests/spiffworkflow_backend/unit/test_process_instance_report.py index 48239507..0a5985f2 100644 --- a/tests/spiffworkflow_backend/unit/test_process_instance_report.py +++ b/tests/spiffworkflow_backend/unit/test_process_instance_report.py @@ -37,7 +37,7 @@ def test_generate_report_with_filter_by_with_variable_substitution( with_db_and_bpmn_file_cleanup: None, setup_process_instances_for_reports: list[ProcessInstanceModel], ) -> None: - """Test_user_can_be_given_permission_to_administer_process_group.""" + """Test_generate_report_with_filter_by_with_variable_substitution.""" process_instances = setup_process_instances_for_reports report_metadata = { "filter_by": [ @@ -61,7 +61,7 @@ def test_generate_report_with_order_by_and_one_field( with_db_and_bpmn_file_cleanup: None, setup_process_instances_for_reports: list[ProcessInstanceModel], ) -> None: - """Test_user_can_be_given_permission_to_administer_process_group.""" + """Test_generate_report_with_order_by_and_one_field.""" process_instances = setup_process_instances_for_reports report_metadata = {"order_by": ["test_score"]} results = do_report_with_metadata_and_instances(report_metadata, process_instances) @@ -75,7 +75,7 @@ def test_generate_report_with_order_by_and_two_fields( with_db_and_bpmn_file_cleanup: None, setup_process_instances_for_reports: list[ProcessInstanceModel], ) -> None: - """Test_user_can_be_given_permission_to_administer_process_group.""" + """Test_generate_report_with_order_by_and_two_fields.""" process_instances = setup_process_instances_for_reports report_metadata = {"order_by": ["grade_level", "test_score"]} results = do_report_with_metadata_and_instances(report_metadata, process_instances) @@ -89,7 +89,7 @@ def test_generate_report_with_order_by_desc( with_db_and_bpmn_file_cleanup: None, setup_process_instances_for_reports: list[ProcessInstanceModel], ) -> None: - """Test_user_can_be_given_permission_to_administer_process_group.""" + """Test_generate_report_with_order_by_desc.""" process_instances = setup_process_instances_for_reports report_metadata = {"order_by": ["grade_level", "-test_score"]} results = do_report_with_metadata_and_instances(report_metadata, process_instances) @@ -103,7 +103,7 @@ def test_generate_report_with_columns( with_db_and_bpmn_file_cleanup: None, setup_process_instances_for_reports: list[ProcessInstanceModel], ) -> None: - """Test_user_can_be_given_permission_to_administer_process_group.""" + """Test_generate_report_with_columns.""" process_instances = setup_process_instances_for_reports report_metadata = { "columns": [ diff --git a/tests/spiffworkflow_backend/unit/test_process_model.py b/tests/spiffworkflow_backend/unit/test_process_model.py index 09421bc7..9eb6901b 100644 --- a/tests/spiffworkflow_backend/unit/test_process_model.py +++ b/tests/spiffworkflow_backend/unit/test_process_model.py @@ -5,12 +5,16 @@ from flask_bpmn.models.db import db from tests.spiffworkflow_backend.helpers.base_test import BaseTest from tests.spiffworkflow_backend.helpers.test_data import load_test_spec +from spiffworkflow_backend.models.process_instance_metadata import ( + ProcessInstanceMetadataModel, +) from spiffworkflow_backend.models.process_model import ProcessModelInfo from spiffworkflow_backend.models.spec_reference import SpecReferenceCache from spiffworkflow_backend.models.user import UserModel from spiffworkflow_backend.services.process_instance_processor import ( ProcessInstanceProcessor, ) +from spiffworkflow_backend.services.process_model_service import ProcessModelService class TestProcessModel(BaseTest): @@ -122,6 +126,53 @@ class TestProcessModel(BaseTest): processor.do_engine_steps(save=True) assert process_instance.status == "complete" + def test_extract_metadata( + self, + app: Flask, + client: FlaskClient, + with_db_and_bpmn_file_cleanup: None, + with_super_admin_user: UserModel, + ) -> None: + """Test_can_run_process_model_with_call_activities.""" + self.create_process_group( + client, with_super_admin_user, "test_group", "test_group" + ) + process_model = load_test_spec( + "test_group/hello_world", + process_model_source_directory="nested-task-data-structure", + ) + ProcessModelService.update_process_model( + process_model, + { + "metadata_extraction_paths": [ + {"key": "awesome_var", "path": "outer.inner"}, + {"key": "invoice_number", "path": "invoice_number"}, + ] + }, + ) + + process_instance = self.create_process_instance_from_process_model( + process_model + ) + processor = ProcessInstanceProcessor(process_instance) + processor.do_engine_steps(save=True) + assert process_instance.status == "complete" + + process_instance_metadata_awesome_var = ( + ProcessInstanceMetadataModel.query.filter_by( + process_instance_id=process_instance.id, key="awesome_var" + ).first() + ) + assert process_instance_metadata_awesome_var is not None + assert process_instance_metadata_awesome_var.value == "sweet2" + process_instance_metadata_awesome_var = ( + ProcessInstanceMetadataModel.query.filter_by( + process_instance_id=process_instance.id, key="invoice_number" + ).first() + ) + assert process_instance_metadata_awesome_var is not None + assert process_instance_metadata_awesome_var.value == "123" + def create_test_process_model(self, id: str, display_name: str) -> ProcessModelInfo: """Create_test_process_model.""" return ProcessModelInfo( diff --git a/tests/spiffworkflow_backend/unit/test_process_model_service.py b/tests/spiffworkflow_backend/unit/test_process_model_service.py index 7127eb41..7392bdfd 100644 --- a/tests/spiffworkflow_backend/unit/test_process_model_service.py +++ b/tests/spiffworkflow_backend/unit/test_process_model_service.py @@ -32,7 +32,7 @@ class TestProcessModelService(BaseTest): primary_process_id = process_model.primary_process_id assert primary_process_id == "Process_HelloWorld" - ProcessModelService().update_process_model( + ProcessModelService.update_process_model( process_model, {"display_name": "new_name"} ) diff --git a/tests/spiffworkflow_backend/unit/test_spec_file_service.py b/tests/spiffworkflow_backend/unit/test_spec_file_service.py index 9f5c5f8a..3cc353b5 100644 --- a/tests/spiffworkflow_backend/unit/test_spec_file_service.py +++ b/tests/spiffworkflow_backend/unit/test_spec_file_service.py @@ -188,7 +188,7 @@ class TestSpecFileService(BaseTest): # , # process_model_source_directory="call_activity_nested", # ) - process_model_info = ProcessModelService().get_process_model( + process_model_info = ProcessModelService.get_process_model( process_model_identifier ) files = SpecFileService.get_files(process_model_info) diff --git a/tests/spiffworkflow_backend/unit/test_various_bpmn_constructs.py b/tests/spiffworkflow_backend/unit/test_various_bpmn_constructs.py index aa91fcfd..26656143 100644 --- a/tests/spiffworkflow_backend/unit/test_various_bpmn_constructs.py +++ b/tests/spiffworkflow_backend/unit/test_various_bpmn_constructs.py @@ -28,7 +28,7 @@ class TestVariousBpmnConstructs(BaseTest): "timer_intermediate_catch_event", ) - process_model = ProcessModelService().get_process_model( + process_model = ProcessModelService.get_process_model( process_model_id=process_model_identifier )