diff --git a/spiffworkflow-backend/bin/local_development_environment_setup b/spiffworkflow-backend/bin/local_development_environment_setup index 5feee70cb..5e8a6a787 100755 --- a/spiffworkflow-backend/bin/local_development_environment_setup +++ b/spiffworkflow-backend/bin/local_development_environment_setup @@ -16,14 +16,13 @@ PORT="${SPIFFWORKFLOW_BACKEND_PORT:-7000}" use_local_open_id="false" acceptance_test_mode="false" -arg_1="${1:-}" - use_local_open_id="true" -if [[ "$arg_1" == "keycloak" ]]; then +# always check $@ so we can pass in multiple args +if [[ "${1:-}" == "keycloak" ]]; then use_local_open_id="false" shift fi -if [[ "$arg_1" == "acceptance" ]]; then +if [[ "${1:-}" == "acceptance" ]]; then acceptance_test_mode="true" shift fi diff --git a/spiffworkflow-backend/docker-compose.yml b/spiffworkflow-backend/docker-compose.yml index f4534cc6d..6b208c685 100644 --- a/spiffworkflow-backend/docker-compose.yml +++ b/spiffworkflow-backend/docker-compose.yml @@ -56,7 +56,7 @@ services: - SPIFFWORKFLOW_BACKEND_DATABASE_URI=mysql+mysqldb://root:${SPIFFWORKFLOW_BACKEND_MYSQL_ROOT_DATABASE:-my-secret-pw}@127.0.0.1:7003/${SPIFFWORKFLOW_BACKEND_DATABASE_NAME:-spiffworkflow_backend_development} - SPIFFWORKFLOW_BACKEND_ENV=${SPIFFWORKFLOW_BACKEND_ENV:-local_development} - SPIFFWORKFLOW_BACKEND_LOAD_FIXTURE_DATA=${SPIFFWORKFLOW_BACKEND_LOAD_FIXTURE_DATA:-false} - - SPIFFWORKFLOW_BACKEND_OPEN_ID_SERVER_URL=${SPIFFWORKFLOW_BACKEND_OPEN_ID_SERVER_URL:-http://localhost:7002/realms/spiffworkflow} + - SPIFFWORKFLOW_BACKEND_OPEN_ID_SERVER_URL=${SPIFFWORKFLOW_BACKEND_OPEN_ID_SERVER_URL:-http://localhost:7002/realms/spiffworkflow-local} - SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME=${SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME:-acceptance_tests.yml} - SPIFFWORKFLOW_BACKEND_PORT=7000 # the background scheduler picks up process instances that we set in a certain state and then runs them like running not_started instances diff --git a/spiffworkflow-backend/keycloak/bin/start_keycloak b/spiffworkflow-backend/keycloak/bin/start_keycloak index 75b3a8a8c..b155c2e34 100755 --- a/spiffworkflow-backend/keycloak/bin/start_keycloak +++ b/spiffworkflow-backend/keycloak/bin/start_keycloak @@ -8,7 +8,7 @@ function remove_traps() { } function error_handler() { - >&2 echo "Exited with BAD EXIT CODE '${2}' in ${0} script at line: ${1}." + echo >&2 "Exited with BAD EXIT CODE '${2}' in ${0} script at line: ${1}." exit "$2" } setup_traps @@ -17,10 +17,10 @@ set -o errtrace -o errexit -o nounset -o pipefail realm_name="${1:-}" if [[ -z "$realm_name" ]]; then - realm_name="spiffworkflow" + realm_name="spiffworkflow-local" fi -if ! docker network inspect spiffworkflow > /dev/null 2>&1; then +if ! docker network inspect spiffworkflow >/dev/null 2>&1; then docker network create spiffworkflow fi @@ -53,7 +53,10 @@ docker run \ -D--spi-theme-cache-themes=false \ -D--spi-theme-cache-templates=false -script_dir="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" +script_dir="$( + cd -- "$(dirname "$0")" >/dev/null 2>&1 + pwd -P +)" cp "${script_dir}/../realm_exports/${realm_name}-realm.json" /tmp/${realm_name}-realm.json spiff_subdomain="for-local-dev.spiffworkflow.org" perl -pi -e "s/replace-me-with-spiff-backend-host-and-path/${spiff_subdomain}/g" /tmp/${realm_name}-realm.json @@ -80,11 +83,9 @@ if [ "${TURN_OFF_SSL:-}" == "true" ]; then echo 'turned off SSL requirement' fi - docker stop keycloak docker start keycloak - # to export: # /opt/keycloak/bin/kc.sh export --dir /tmp/hey --users realm_file # change any js policies to role policies - just copy the config of one and change the type to role diff --git a/spiffworkflow-backend/keycloak/realm_exports/spiffworkflow-local-realm.json b/spiffworkflow-backend/keycloak/realm_exports/spiffworkflow-local-realm.json new file mode 100644 index 000000000..ac65d29aa --- /dev/null +++ b/spiffworkflow-backend/keycloak/realm_exports/spiffworkflow-local-realm.json @@ -0,0 +1,2951 @@ +{ + "id": "spiffworkflow-local", + "realm": "spiffworkflow-local", + "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-local", + "attributes": {} + }, + { + "id": "9f474167-5707-4c10-8f9e-bb54ec715cd3", + "name": "uma_authorization", + "description": "${role_uma_authorization}", + "composite": false, + "clientRole": false, + "containerId": "spiffworkflow-local", + "attributes": {} + }, + { + "id": "6738d143-2d1d-4458-8a98-01ea003fde14", + "name": "admin", + "composite": false, + "clientRole": false, + "containerId": "spiffworkflow-local", + "attributes": {} + }, + { + "id": "6cbcdea5-0083-469d-9576-1d245fb3cdfd", + "name": "repeat-form-role-realm", + "composite": false, + "clientRole": false, + "containerId": "spiffworkflow-local", + "attributes": {} + }, + { + "id": "b5a92aee-82d2-4687-8282-365df4df21a9", + "name": "offline_access", + "description": "${role_offline-access}", + "composite": false, + "clientRole": false, + "containerId": "spiffworkflow-local", + "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"] + } + } + ], + "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"] + } + }, + "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"] + } + }, + "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-local" + }, + "requiredCredentials": ["password"], + "otpPolicyType": "totp", + "otpPolicyAlgorithm": "HmacSHA1", + "otpPolicyInitialCounter": 0, + "otpPolicyDigits": 6, + "otpPolicyLookAheadWindow": 1, + "otpPolicyPeriod": 30, + "otpPolicyCodeReusable": false, + "otpSupportedApplications": ["totpAppFreeOTPName", "totpAppGoogleName"], + "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": "26b0e310-ea33-4cea-8bfd-a32dc6bc11d4", + "createdTimestamp": 1676302139599, + "username": "admin", + "enabled": true, + "totp": false, + "emailVerified": false, + "email": "admin@example.com", + "credentials": [ + { + "id": "644ff652-31d1-4349-9340-ce3b5fb76b5c", + "type": "password", + "createdDate": 1676302139645, + "secretData": "{\"value\":\"P6nrEJgmgdL+HbNA9tPkOyHYaLAqLg6cXh27ACgVQiOox4qwNE8Iv1L4ssispV4gsmuHiY+alio/2UMuyMGvEw==\",\"salt\":\"g214ckcAyig59U/3Oqwq4w==\",\"additionalParameters\":{}}", + "credentialData": "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } + ], + "disableableCredentialTypes": [], + "requiredActions": [], + "realmRoles": ["default-roles-spiffworkflow"], + "clientRoles": { + "realm-management": ["realm-admin"] + }, + "notBefore": 0, + "groups": [] + }, + { + "id": "4c436296-8471-4105-b551-80eee96b43bb", + "createdTimestamp": 1657139858075, + "username": "ciadmin1", + "enabled": true, + "totp": false, + "emailVerified": false, + "firstName": "", + "lastName": "", + "email": "ciadmin1@example.com", + "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": [] + }, + { + "id": "7b9767ac-24dc-43b0-838f-29e16b4fd14e", + "createdTimestamp": 1675718483773, + "username": "dan", + "enabled": true, + "totp": false, + "emailVerified": false, + "email": "dan@example.com", + "attributes": { + "spiffworkflow-employeeid": ["115"] + }, + "credentials": [ + { + "id": "8bc911c7-02fa-43fd-8e61-a313721ffc89", + "type": "password", + "createdDate": 1675718483819, + "secretData": "{\"value\":\"pQ/vKJoOG0sNGeNsjm+kVVnGImIjhLMBTjw1lRXj3X5ffW22EzXF7+kYrTBgXnTmFUql2INVpIl838vYmg80RQ==\",\"salt\":\"a0zK/X/EO3VhXUr8Z3P1uA==\",\"additionalParameters\":{}}", + "credentialData": "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } + ], + "disableableCredentialTypes": [], + "requiredActions": [], + "realmRoles": ["default-roles-spiffworkflow"], + "notBefore": 0, + "groups": [] + }, + { + "id": "3873c0ba-349c-4bec-8be2-5ced8acd56ec", + "createdTimestamp": 1675718483992, + "username": "elizabeth", + "enabled": true, + "totp": false, + "emailVerified": false, + "email": "elizabeth@example.com", + "credentials": [ + { + "id": "13dd277b-b5e4-4ff7-aba8-2d648858224e", + "type": "password", + "createdDate": 1675718484034, + "secretData": "{\"value\":\"qN84NNHkapbSOpYDUnEqemMFUiGJSbCdyUfsPJyinbzFWltqnrmgAQ/9j5dJweyJS2uXTEQgOFcV83LmqmIT4Q==\",\"salt\":\"A+gRdmehei+Ob6wNe/cszA==\",\"additionalParameters\":{}}", + "credentialData": "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } + ], + "disableableCredentialTypes": [], + "requiredActions": [], + "realmRoles": ["default-roles-spiffworkflow"], + "notBefore": 0, + "groups": [] + }, + { + "id": "06f69a51-780c-4d3b-9c32-2c93d01d1e28", + "createdTimestamp": 1675718484199, + "username": "jason", + "enabled": true, + "totp": false, + "emailVerified": false, + "email": "jason@example.com", + "credentials": [ + { + "id": "f2330ab2-30ad-483c-af05-190a839bba21", + "type": "password", + "createdDate": 1675718484243, + "secretData": "{\"value\":\"M/HQLqkHOWMCCnUg6Qd5pPdag6apDosVMkPTm+mDbbNa36mwkEvOmVYD48OSwwruUYiMV3dKByBBx8/QZqqbsA==\",\"salt\":\"R1V9v/uSxAXrS0o4Gh9wcw==\",\"additionalParameters\":{}}", + "credentialData": "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } + ], + "disableableCredentialTypes": [], + "requiredActions": [], + "realmRoles": ["default-roles-spiffworkflow"], + "notBefore": 0, + "groups": [] + }, + { + "id": "0333aaa7-cfe8-4dd2-a8c7-cbefc9ea9c8e", + "createdTimestamp": 1675718484299, + "username": "jon", + "enabled": true, + "totp": false, + "emailVerified": false, + "email": "jon@example.com", + "credentials": [ + { + "id": "9dfd7471-8b2a-422f-a1c4-cd2d17f2eebe", + "type": "password", + "createdDate": 1675718484347, + "secretData": "{\"value\":\"wIKYqLU64kVmqRixyS6ctMv3WrP8ADHGY0uIlV+T03Q6Um8jWvxFeyxK+jczQip9/Fe3cnyO3ofDo3IZNeNSEg==\",\"salt\":\"dS4upVR1o9fhRF/Doz/KaA==\",\"additionalParameters\":{}}", + "credentialData": "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } + ], + "disableableCredentialTypes": [], + "requiredActions": [], + "realmRoles": ["default-roles-spiffworkflow"], + "notBefore": 0, + "groups": [] + }, + { + "id": "cdec7198-19f5-4788-aceb-e8c68f4c277f", + "createdTimestamp": 1675718484460, + "username": "kevin", + "enabled": true, + "totp": false, + "emailVerified": false, + "email": "kevin@example.com", + "credentials": [ + { + "id": "c1729585-5811-4b56-976c-c3370d1386f5", + "type": "password", + "createdDate": 1675718484506, + "secretData": "{\"value\":\"bUeoB4qNIQe3nbBytEw269xMmYO+/8rq0GnwNVgwjiEHfeya2t3g+mu3yg0ZmahxUgdh1iTfx9Ja6VGDtGk7Ug==\",\"salt\":\"fbm81DCMYdMXF+NB/92OBw==\",\"additionalParameters\":{}}", + "credentialData": "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } + ], + "disableableCredentialTypes": [], + "requiredActions": [], + "realmRoles": ["default-roles-spiffworkflow"], + "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}", + "description": "", + "rootUrl": "", + "adminUrl": "", + "baseUrl": "", + "surrogateAuthRequired": false, + "enabled": true, + "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", + "name": "", + "description": "", + "rootUrl": "", + "adminUrl": "", + "baseUrl": "", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "JXeQExm0JhQPLumgHtIIqf52bDalHz0q", + "redirectUris": [ + "http://localhost:7000/*", + "https://replace-me-with-spiff-backend-host-and-path/*", + "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-frontend-host-and-path/*##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" + }, + "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" + } + }, + { + "id": "a7692d41-b905-4049-9004-f6bea690051d", + "name": "spiffworkflow-employeeid", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "aggregate.attrs": "false", + "userinfo.token.claim": "true", + "multivalued": "false", + "user.attribute": "spiffworkflow-employeeid", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "spiffworkflow-employeeid" + } + } + ], + "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" + } + }, + { + "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-sha256-pairwise-sub-mapper", + "oidc-address-mapper", + "oidc-full-name-mapper", + "saml-user-attribute-mapper", + "oidc-usermodel-property-mapper", + "saml-role-list-mapper", + "saml-user-property-mapper", + "oidc-usermodel-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-property-mapper", + "saml-role-list-mapper", + "oidc-address-mapper", + "saml-user-attribute-mapper", + "oidc-sha256-pairwise-sub-mapper", + "oidc-usermodel-attribute-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": "62d7bb2a-5919-48b2-a9f9-511ecf5474c7", + "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": "7675760b-666a-4b8c-a9b8-da1e01c207fe", + "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": "34e18ea8-f515-46dc-9dbf-5b79f8154564", + "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": "933e581c-56d8-4614-b2a3-d2db10397ea0", + "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": "0986dc8c-4bcf-477f-8ba2-3cac02ea656f", + "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": "534381e4-b0b9-43b2-9ac5-9f1e006b5920", + "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": "922e84ab-85db-494a-8a8c-84d3b0c675f4", + "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": "24b1b409-b6fc-44dc-9a97-93b2f4a78c89", + "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": "c015a916-a45b-4797-a466-2399164da6fe", + "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": "fc7aec31-855b-4993-b770-57660ff0524f", + "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": "9769d765-42c8-4391-a7ec-aa24f0e84040", + "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": "49a937cc-9d51-43d0-a379-67aaae38c51a", + "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": "1a766b69-7ead-442a-84a4-083cd84949cd", + "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": "e4ac0543-cfb6-4232-947d-52b8615e0629", + "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": "86247ee8-b507-406b-9d32-3c68c80084a5", + "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": "70ef5a26-e3bb-4ba7-a05a-d205b0a3836c", + "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": "89abf09a-bfb4-4dea-b164-ca7c563b4009", + "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": "52d31bf0-dcb6-4b01-a252-b2ba705df036", + "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": "22041b6b-6d9e-43eb-8d2a-94a3052c49aa", + "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": "153aaf25-b6d9-42b4-9740-f63c94c16626", + "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": "e0075b39-a2ad-47de-9ee6-e61073387e71", + "alias": "create unique user config", + "config": { + "require.password.update.after.registration": "false" + } + }, + { + "id": "aa24bff3-bd25-4b2a-973f-63fea5c21dd1", + "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", + "realmReusableOtpCode": "false", + "cibaExpiresIn": "120", + "oauth2DeviceCodeLifespan": "600", + "actionTokenGeneratedByUserLifespan-idp-verify-account-via-email": "", + "parRequestUriLifespan": "60", + "clientSessionMaxLifespan": "0" + }, + "keycloakVersion": "20.0.1", + "userManagedAccessAllowed": false, + "clientProfiles": { + "profiles": [] + }, + "clientPolicies": { + "policies": [] + } +} diff --git a/spiffworkflow-backend/migrations/versions/ac125644907a_.py b/spiffworkflow-backend/migrations/versions/ac125644907a_.py new file mode 100644 index 000000000..24b66168b --- /dev/null +++ b/spiffworkflow-backend/migrations/versions/ac125644907a_.py @@ -0,0 +1,43 @@ +"""empty message + +Revision ID: ac125644907a +Revises: ffef09e6ddf1 +Create Date: 2024-07-09 15:47:15.355999 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'ac125644907a' +down_revision = 'ffef09e6ddf1' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('process_instance_migration_detail', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('process_instance_event_id', sa.Integer(), nullable=False), + sa.Column('initial_git_revision', sa.String(length=64), nullable=True), + sa.Column('target_git_revision', sa.String(length=64), nullable=True), + sa.Column('initial_bpmn_process_hash', sa.String(length=64), nullable=False), + sa.Column('target_bpmn_process_hash', sa.String(length=64), nullable=False), + sa.ForeignKeyConstraint(['process_instance_event_id'], ['process_instance_event.id'], ), + sa.PrimaryKeyConstraint('id') + ) + with op.batch_alter_table('process_instance_migration_detail', schema=None) as batch_op: + batch_op.create_index(batch_op.f('ix_process_instance_migration_detail_process_instance_event_id'), ['process_instance_event_id'], unique=False) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('process_instance_migration_detail', schema=None) as batch_op: + batch_op.drop_index(batch_op.f('ix_process_instance_migration_detail_process_instance_event_id')) + + op.drop_table('process_instance_migration_detail') + # ### end Alembic commands ### diff --git a/spiffworkflow-backend/poetry.lock b/spiffworkflow-backend/poetry.lock index 682075b33..c8a74d09c 100644 --- a/spiffworkflow-backend/poetry.lock +++ b/spiffworkflow-backend/poetry.lock @@ -2900,7 +2900,7 @@ doc = ["sphinx", "sphinx_rtd_theme"] type = "git" url = "https://github.com/sartography/SpiffWorkflow" reference = "main" -resolved_reference = "a7ddab83831eb98371a26d94ecc6978287b965c5" +resolved_reference = "10f0a1905f976f7770bc7e6a3f48e273d0e827a6" [[package]] name = "spiffworkflow-connector-command" diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml index ed8ffed31..dda9b7d90 100755 --- a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml +++ b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml @@ -1509,6 +1509,12 @@ paths: description: The unique id of an existing process instance. schema: type: integer + - name: target_bpmn_process_hash + in: query + required: false + description: The full bpmn process hash of the target version of the process model. + schema: + type: string post: operationId: spiffworkflow_backend.routes.process_instances_controller.process_instance_migrate summary: Migrate a process instance to the new version of its process model. @@ -2929,6 +2935,33 @@ paths: schema: $ref: "#/components/schemas/ProcessInstanceLog" + /process-instance-events/{modified_process_model_identifier}/{process_instance_id}/migration: + parameters: + - name: process_instance_id + in: path + required: true + description: the id of the process instance + schema: + type: integer + - name: modified_process_model_identifier + in: path + required: true + description: The process_model_id, modified to replace slashes (/) + schema: + type: string + get: + tags: + - Process Instance Events + operationId: spiffworkflow_backend.routes.process_instance_events_controller.process_instance_migration_event_list + summary: returns a list of migration events associated with the process instance + responses: + "200": + description: list of logs + content: + application/json: + schema: + $ref: "#/components/schemas/ProcessInstanceLog" + /logs/typeahead-filter-values/{modified_process_model_identifier}/{process_instance_id}: parameters: - name: process_instance_id diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/config/default.py b/spiffworkflow-backend/src/spiffworkflow_backend/config/default.py index 113ba61b0..ab4ee1000 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/config/default.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/config/default.py @@ -145,7 +145,7 @@ def config_from_env(variable_name: str, *, default: str | bool | int | None = No { "identifier": "default", "label": "Default", - "uri": "http://localhost:7002/realms/spiffworkflow", + "uri": "http://localhost:7002/realms/spiffworkflow-local", "client_id": "spiffworkflow-backend", "client_secret": "JXeQExm0JhQPLumgHtIIqf52bDalHz0q", "additional_valid_client_ids": None, diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/acceptance_tests.yml b/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/acceptance_tests.yml index 228b1f5ad..74d2f0b7b 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/acceptance_tests.yml +++ b/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/acceptance_tests.yml @@ -1,13 +1,13 @@ users: ciadmin1: service: local_open_id - email: ciadmin1@spiffworkflow.org + email: ciadmin1@example.com password: ciadmin1 preferred_username: ciadmin1 groups: admin: - users: [ciadmin1@spiffworkflow.org] + users: [ciadmin1@example.com] spiff_public: users: [] diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/exceptions/error.py b/spiffworkflow-backend/src/spiffworkflow_backend/exceptions/error.py index b8c258392..674a76cc9 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/exceptions/error.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/exceptions/error.py @@ -14,6 +14,10 @@ class ProcessInstanceMigrationError(Exception): pass +class ProcessInstanceMigrationUnnecessaryError(Exception): + pass + + class TokenExpiredError(Exception): pass diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance_event.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance_event.py index a7932d2e7..8b91793d6 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance_event.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance_event.py @@ -1,5 +1,6 @@ from __future__ import annotations +from dataclasses import dataclass from typing import Any from sqlalchemy import ForeignKey @@ -29,6 +30,7 @@ class ProcessInstanceEventType(SpiffEnum): task_skipped = "task_skipped" +@dataclass class ProcessInstanceEventModel(SpiffworkflowBaseDBModel): __tablename__ = "process_instance_event" id: int = db.Column(db.Integer, primary_key=True) @@ -43,6 +45,9 @@ class ProcessInstanceEventModel(SpiffworkflowBaseDBModel): user_id = db.Column(ForeignKey(UserModel.id), nullable=True, index=True) # type: ignore error_details = relationship("ProcessInstanceErrorDetailModel", back_populates="process_instance_event", cascade="delete") # type: ignore + migration_details = relationship( + "ProcessInstanceMigrationDetailModel", back_populates="process_instance_event", cascade="delete" + ) # type: ignore @validates("event_type") def validate_event_type(self, key: str, value: Any) -> Any: diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance_migration_detail.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance_migration_detail.py new file mode 100644 index 000000000..a37d8d5b4 --- /dev/null +++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance_migration_detail.py @@ -0,0 +1,29 @@ +from dataclasses import dataclass +from typing import TypedDict + +from sqlalchemy import ForeignKey +from sqlalchemy.orm import relationship + +from spiffworkflow_backend.models.db import SpiffworkflowBaseDBModel +from spiffworkflow_backend.models.db import db + + +class ProcessInstanceMigrationDetailDict(TypedDict): + initial_git_revision: str | None + target_git_revision: str | None + initial_bpmn_process_hash: str + target_bpmn_process_hash: str + + +@dataclass +class ProcessInstanceMigrationDetailModel(SpiffworkflowBaseDBModel): + __tablename__ = "process_instance_migration_detail" + id: int = db.Column(db.Integer, primary_key=True) + + process_instance_event_id: int = db.Column(ForeignKey("process_instance_event.id"), nullable=False, index=True) + process_instance_event = relationship("ProcessInstanceEventModel") # type: ignore + + initial_git_revision: str | None = db.Column(db.String(64), nullable=True) + target_git_revision: str | None = db.Column(db.String(64), nullable=True) + initial_bpmn_process_hash: str = db.Column(db.String(64), nullable=False) + target_bpmn_process_hash: str = db.Column(db.String(64), nullable=False) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_instance_events_controller.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_instance_events_controller.py index ba1c18018..57de79c21 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_instance_events_controller.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_instance_events_controller.py @@ -10,6 +10,7 @@ from spiffworkflow_backend.models.db import db from spiffworkflow_backend.models.process_instance_event import ProcessInstanceEventModel from spiffworkflow_backend.models.process_instance_event import ProcessInstanceEventType +from spiffworkflow_backend.models.process_instance_migration_detail import ProcessInstanceMigrationDetailModel from spiffworkflow_backend.models.task import TaskModel # noqa: F401 from spiffworkflow_backend.models.task_definition import TaskDefinitionModel from spiffworkflow_backend.models.user import UserModel @@ -27,7 +28,6 @@ def log_list( task_type: str | None = None, event_type: str | None = None, ) -> flask.wrappers.Response: - # to make sure the process instance exists process_instance = _find_process_instance_by_id_or_raise(process_instance_id) log_query = ( @@ -164,3 +164,37 @@ def error_detail_show( error_details = process_instance_event.error_details[0] return make_response(jsonify(error_details), 200) + + +def process_instance_migration_event_list( + modified_process_model_identifier: str, + process_instance_id: int, +) -> flask.wrappers.Response: + process_instance = _find_process_instance_by_id_or_raise(process_instance_id) + + logs = ( + db.session.query(ProcessInstanceEventModel, ProcessInstanceMigrationDetailModel, UserModel) + .filter( + ProcessInstanceEventModel.process_instance_id == process_instance.id, + ProcessInstanceEventModel.event_type == ProcessInstanceEventType.process_instance_migrated.value, + ) + .join(ProcessInstanceMigrationDetailModel) # type: ignore + .outerjoin(UserModel, UserModel.id == ProcessInstanceEventModel.user_id) + .order_by(ProcessInstanceEventModel.timestamp.desc(), ProcessInstanceEventModel.id.desc()) # type: ignore + .add_columns( + ProcessInstanceEventModel.id, + ProcessInstanceEventModel.timestamp, + UserModel.username, + ProcessInstanceMigrationDetailModel.initial_bpmn_process_hash, + ProcessInstanceMigrationDetailModel.target_bpmn_process_hash, + ProcessInstanceMigrationDetailModel.initial_git_revision, + ProcessInstanceMigrationDetailModel.initial_git_revision, + ) + .all() + ) + + response_json = { + "results": logs, + } + + return make_response(jsonify(response_json), 200) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_instances_controller.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_instances_controller.py index 1e4d9da6f..18fb913a6 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_instances_controller.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_instances_controller.py @@ -1,3 +1,4 @@ +from spiffworkflow_backend.exceptions.error import ProcessInstanceMigrationUnnecessaryError from spiffworkflow_backend.exceptions.error import ProcessInstanceMigrationError, ProcessInstanceMigrationNotSafeError from spiffworkflow_backend.helpers.spiff_enum import ProcessInstanceExecutionMode @@ -552,13 +553,19 @@ def process_instance_check_can_migrate( modified_process_model_identifier: str, ) -> flask.wrappers.Response: process_instance = _find_process_instance_by_id_or_raise(process_instance_id) - can_migrate = True + return_dict: dict = { + "can_migrate": True, + "process_instance_id": process_instance.id, + "current_git_revision": process_instance.bpmn_version_control_identifier, + "current_bpmn_process_hash": process_instance.bpmn_process.bpmn_process_definition.full_process_model_hash, + } try: ProcessInstanceService.check_process_instance_can_be_migrated(process_instance) - except ProcessInstanceMigrationNotSafeError: - can_migrate = False + except (ProcessInstanceMigrationNotSafeError, ProcessInstanceMigrationUnnecessaryError) as exception: + return_dict["can_migrate"] = False + return_dict["exception_class"] = exception.__class__.__name__ return Response( - json.dumps({"can_migrate": can_migrate, "process_instance_id": process_instance.id}), + json.dumps(return_dict), status=200, mimetype="application/json", ) @@ -567,13 +574,16 @@ def process_instance_check_can_migrate( def process_instance_migrate( process_instance_id: int, modified_process_model_identifier: str, + target_bpmn_process_hash: str | None = None, ) -> flask.wrappers.Response: process_instance = _find_process_instance_by_id_or_raise(process_instance_id) if process_instance.status != "suspended": raise ProcessInstanceMigrationError( f"The process instance needs to be suspended to migrate it. It is currently: {process_instance.status}" ) - ProcessInstanceService.migrate_process_instance_to_newest_model_version(process_instance, user=g.user) + ProcessInstanceService.migrate_process_instance( + process_instance, user=g.user, target_bpmn_process_hash=target_bpmn_process_hash + ) return Response(json.dumps({"ok": True}), status=200, mimetype="application/json") diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py index ec80a18f9..cb2678002 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py @@ -70,6 +70,7 @@ class PermissionToAssign: }, {"path": "/process-data", "relevant_permissions": ["read"]}, {"path": "/process-data-file-download", "relevant_permissions": ["read"]}, + {"path": "/process-instance-events", "relevant_permissions": ["read"]}, {"path": "/process-instance-migrate", "relevant_permissions": ["create"]}, {"path": "/process-instance-suspend", "relevant_permissions": ["create"]}, {"path": "/process-instance-terminate", "relevant_permissions": ["create"]}, @@ -559,6 +560,7 @@ def get_permissions_to_assign( f"/logs/{process_related_path_segment}", f"/logs/typeahead-filter-values/{process_related_path_segment}", f"/process-data-file-download/{process_related_path_segment}", + f"/process-instance-events/{process_related_path_segment}", f"/event-error-details/{process_related_path_segment}", ]: permissions_to_assign.append(PermissionToAssign(permission="read", target_uri=target_uri)) @@ -662,6 +664,7 @@ def set_support_permissions(cls) -> list[PermissionToAssign]: permissions_to_assign.append(PermissionToAssign(permission="read", target_uri="/logs/*")) permissions_to_assign.append(PermissionToAssign(permission="read", target_uri="/process-data-file-download/*")) permissions_to_assign.append(PermissionToAssign(permission="read", target_uri="/process-data/*")) + permissions_to_assign.append(PermissionToAssign(permission="read", target_uri="/process-instance-events/*")) permissions_to_assign.append(PermissionToAssign(permission="read", target_uri="/task-data/*")) for permission in ["create", "read", "update", "delete"]: diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py index 1cb6a20fa..1c1f816ab 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py @@ -789,23 +789,25 @@ def _get_tasks_dict( @classmethod def _get_full_bpmn_process_dict( cls, - process_instance_model: ProcessInstanceModel, bpmn_definition_to_task_definitions_mappings: dict, - task_model_mapping: dict[str, TaskModel], bpmn_subprocess_mapping: dict[str, BpmnProcessModel], + task_model_mapping: dict[str, TaskModel], + spiff_serializer_version: str | None = None, + bpmn_process_definition: BpmnProcessDefinitionModel | None = None, + bpmn_process: BpmnProcessModel | None = None, + bpmn_process_definition_id: int | None = None, include_task_data_for_completed_tasks: bool = False, include_completed_subprocesses: bool = False, ) -> dict: - if process_instance_model.bpmn_process_definition_id is None: + if bpmn_process_definition_id is None: return {} spiff_bpmn_process_dict: dict = { - "serializer_version": process_instance_model.spiff_serializer_version, + "serializer_version": spiff_serializer_version, "spec": {}, "subprocess_specs": {}, "subprocesses": {}, } - bpmn_process_definition = process_instance_model.bpmn_process_definition if bpmn_process_definition is not None: spiff_bpmn_process_dict["spec"] = cls._get_definition_dict_for_bpmn_process_definition( bpmn_process_definition, @@ -817,7 +819,6 @@ def _get_full_bpmn_process_dict( bpmn_definition_to_task_definitions_mappings, ) - bpmn_process = process_instance_model.bpmn_process if bpmn_process is not None: single_bpmn_process_dict = cls._get_bpmn_process_dict( bpmn_process, @@ -903,12 +904,15 @@ def __get_bpmn_process_instance( try: full_bpmn_process_dict = ProcessInstanceProcessor._get_full_bpmn_process_dict( - process_instance_model, - bpmn_definition_to_task_definitions_mappings, + bpmn_definition_to_task_definitions_mappings=bpmn_definition_to_task_definitions_mappings, include_completed_subprocesses=include_completed_subprocesses, include_task_data_for_completed_tasks=include_task_data_for_completed_tasks, task_model_mapping=task_model_mapping, bpmn_subprocess_mapping=bpmn_subprocess_mapping, + spiff_serializer_version=process_instance_model.spiff_serializer_version, + bpmn_process_definition=process_instance_model.bpmn_process_definition, + bpmn_process=process_instance_model.bpmn_process, + bpmn_process_definition_id=process_instance_model.bpmn_process_definition_id, ) # FIXME: the from_dict entrypoint in spiff will one day do this copy instead process_copy = copy.deepcopy(full_bpmn_process_dict) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_service.py index 8c27a0074..a656caa6a 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_service.py @@ -1,10 +1,13 @@ import base64 +import copy import hashlib +import json import time from collections.abc import Generator from contextlib import suppress from datetime import datetime from datetime import timezone +from hashlib import sha256 from typing import Any from urllib.parse import unquote from uuid import UUID @@ -34,8 +37,10 @@ from spiffworkflow_backend.exceptions.error import HumanTaskAlreadyCompletedError from spiffworkflow_backend.exceptions.error import HumanTaskNotFoundError from spiffworkflow_backend.exceptions.error import ProcessInstanceMigrationNotSafeError +from spiffworkflow_backend.exceptions.error import ProcessInstanceMigrationUnnecessaryError from spiffworkflow_backend.exceptions.error import UserDoesNotHaveAccessToTaskError from spiffworkflow_backend.helpers.spiff_enum import ProcessInstanceExecutionMode +from spiffworkflow_backend.models.bpmn_process_definition import BpmnProcessDefinitionModel from spiffworkflow_backend.models.db import db from spiffworkflow_backend.models.group import GroupModel from spiffworkflow_backend.models.human_task import HumanTaskModel @@ -146,13 +151,42 @@ def create_process_instance( @classmethod def check_process_instance_can_be_migrated( - cls, process_instance: ProcessInstanceModel + cls, + process_instance: ProcessInstanceModel, + target_bpmn_process_hash: str | None = None, ) -> tuple[ ProcessInstanceProcessor, BpmnProcessSpec, IdToBpmnProcessSpecMapping, WorkflowDiff, SubprocessUuidToWorkflowDiffMapping ]: - (target_bpmn_process_spec, target_subprocess_specs) = ProcessInstanceProcessor.get_process_model_and_subprocesses( - process_instance.process_model_identifier, - ) + if target_bpmn_process_hash is None: + (target_bpmn_process_spec, target_subprocess_specs) = ProcessInstanceProcessor.get_process_model_and_subprocesses( + process_instance.process_model_identifier, + ) + full_bpmn_spec_dict = { + "spec": ProcessInstanceProcessor._serializer.to_dict(target_bpmn_process_spec), + "subprocess_specs": ProcessInstanceProcessor._serializer.to_dict(target_subprocess_specs), + } + target_bpmn_process_hash = sha256(json.dumps(full_bpmn_spec_dict, sort_keys=True).encode("utf8")).hexdigest() + else: + bpmn_process_definition = BpmnProcessDefinitionModel.query.filter_by( + full_process_model_hash=target_bpmn_process_hash + ).first() + full_bpmn_process_dict = ProcessInstanceProcessor._get_full_bpmn_process_dict( + bpmn_definition_to_task_definitions_mappings={}, + spiff_serializer_version=process_instance.spiff_serializer_version, + bpmn_process_definition=bpmn_process_definition, + bpmn_process_definition_id=bpmn_process_definition.id, + task_model_mapping={}, + bpmn_subprocess_mapping={}, + ) + process_copy = copy.deepcopy(full_bpmn_process_dict) + target_bpmn_process_spec = ProcessInstanceProcessor._serializer.from_dict(process_copy["spec"]) + target_subprocess_specs = ProcessInstanceProcessor._serializer.from_dict(process_copy["subprocess_specs"]) + + initial_bpmn_process_hash = process_instance.bpmn_process_definition.full_process_model_hash + if target_bpmn_process_hash == initial_bpmn_process_hash: + raise ProcessInstanceMigrationUnnecessaryError( + "Both target and current process model versions are the same. There is no need to migrate." + ) processor = ProcessInstanceProcessor( process_instance, include_task_data_for_completed_tasks=True, include_completed_subprocesses=True ) @@ -175,19 +209,23 @@ def check_process_instance_can_be_migrated( ) @classmethod - def migrate_process_instance_to_newest_model_version( - cls, process_instance: ProcessInstanceModel, user: UserModel, preserve_old_process_instance: bool = False + def migrate_process_instance( + cls, + process_instance: ProcessInstanceModel, + user: UserModel, + preserve_old_process_instance: bool = False, + target_bpmn_process_hash: str | None = None, ) -> None: + initial_git_revision = process_instance.bpmn_version_control_identifier + initial_bpmn_process_hash = process_instance.bpmn_process_definition.full_process_model_hash ( processor, target_bpmn_process_spec, target_subprocess_specs, top_level_bpmn_process_diff, subprocesses_diffs, - ) = cls.check_process_instance_can_be_migrated(process_instance) - ProcessInstanceTmpService.add_event_to_process_instance( - process_instance, ProcessInstanceEventType.process_instance_rewound_to_task.value - ) + ) = cls.check_process_instance_can_be_migrated(process_instance, target_bpmn_process_hash=target_bpmn_process_hash) + migrate_workflow(top_level_bpmn_process_diff, processor.bpmn_process_instance, target_bpmn_process_spec) for sp_id, sp in processor.bpmn_process_instance.subprocesses.items(): migrate_workflow(subprocesses_diffs[sp_id], sp, target_subprocess_specs.get(sp.spec.name)) @@ -219,6 +257,27 @@ def migrate_process_instance_to_newest_model_version( process_instance_model=process_instance, bpmn_process_instance=processor.bpmn_process_instance, ) + try: + current_git_revision = GitService.get_current_revision() + except GitCommandError: + current_git_revision = None + process_instance.bpmn_version_control_identifier = current_git_revision + db.session.add(process_instance) + + target_git_revision = process_instance.bpmn_version_control_identifier + if target_bpmn_process_hash is None: + target_bpmn_process_hash = process_instance.bpmn_process_definition.full_process_model_hash + ProcessInstanceTmpService.add_event_to_process_instance( + process_instance, + ProcessInstanceEventType.process_instance_migrated.value, + migration_details={ + "initial_git_revision": initial_git_revision, + "initial_bpmn_process_hash": initial_bpmn_process_hash or "", + "target_git_revision": target_git_revision, + "target_bpmn_process_hash": target_bpmn_process_hash or "", + }, + ) + db.session.commit() @classmethod def create_process_instance_from_process_model_identifier( diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_tmp_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_tmp_service.py index b9dab35da..b99c3a356 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_tmp_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_tmp_service.py @@ -8,6 +8,8 @@ from spiffworkflow_backend.models.process_instance import ProcessInstanceModel from spiffworkflow_backend.models.process_instance_error_detail import ProcessInstanceErrorDetailModel from spiffworkflow_backend.models.process_instance_event import ProcessInstanceEventModel +from spiffworkflow_backend.models.process_instance_migration_detail import ProcessInstanceMigrationDetailDict +from spiffworkflow_backend.models.process_instance_migration_detail import ProcessInstanceMigrationDetailModel from spiffworkflow_backend.models.process_instance_queue import ProcessInstanceQueueModel @@ -28,6 +30,7 @@ def add_event_to_process_instance( exception: Exception | None = None, timestamp: float | None = None, add_to_db_session: bool | None = True, + migration_details: ProcessInstanceMigrationDetailDict | None = None, ) -> tuple[ProcessInstanceEventModel, ProcessInstanceErrorDetailModel | None]: if user_id is None and hasattr(g, "user") and g.user: user_id = g.user.id @@ -45,6 +48,10 @@ def add_event_to_process_instance( process_instance_error_detail = None if exception is not None: + # NOTE: I tried to move this to its own method but + # est_unlocks_if_an_exception_is_thrown_with_a__dequeued_process_instance + # gave sqlalchemy rollback errors. I could not figure out why so went back to this. + # # truncate to avoid database errors on large values. We observed that text in mysql is 65K. stacktrace = traceback.format_exc().split("\n") message = str(exception)[0:1023] @@ -76,8 +83,26 @@ def add_event_to_process_instance( if add_to_db_session: db.session.add(process_instance_error_detail) + + if migration_details is not None: + pi_detail = cls.add_process_instance_migration_detail(process_instance_event, migration_details) + if add_to_db_session: + db.session.add(pi_detail) return (process_instance_event, process_instance_error_detail) + @classmethod + def add_process_instance_migration_detail( + cls, process_instance_event: ProcessInstanceEventModel, migration_details: ProcessInstanceMigrationDetailDict + ) -> ProcessInstanceMigrationDetailModel: + pi_detail = ProcessInstanceMigrationDetailModel( + process_instance_event=process_instance_event, + initial_git_revision=migration_details["initial_git_revision"], + target_git_revision=migration_details["target_git_revision"], + initial_bpmn_process_hash=migration_details["initial_bpmn_process_hash"], + target_bpmn_process_hash=migration_details["target_bpmn_process_hash"], + ) + return pi_detail + @staticmethod def is_enqueued_to_run_in_the_future(process_instance: ProcessInstanceModel) -> bool: queue_entry = ( diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/helpers/base_test.py b/spiffworkflow-backend/tests/spiffworkflow_backend/helpers/base_test.py index fe8c0fabe..927de1e35 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/helpers/base_test.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/helpers/base_test.py @@ -339,6 +339,7 @@ def create_process_instance_from_process_model( status: str | None = "not_started", user: UserModel | None = None, save_start_and_end_times: bool = True, + bpmn_version_control_identifier: str | None = None, ) -> ProcessInstanceModel: if user is None: user = self.find_or_create_user() @@ -357,6 +358,7 @@ def create_process_instance_from_process_model( updated_at_in_seconds=round(time.time()), start_in_seconds=start_in_seconds, end_in_seconds=end_in_seconds, + bpmn_version_control_identifier=bpmn_version_control_identifier, ) db.session.add(process_instance) db.session.commit() diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_instance_events_controller.py b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_instance_events_controller.py new file mode 100644 index 000000000..a9b5a6c72 --- /dev/null +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_instance_events_controller.py @@ -0,0 +1,62 @@ +from flask.app import Flask +from flask.testing import FlaskClient +from spiffworkflow_backend.models.process_instance import ProcessInstanceModel +from spiffworkflow_backend.models.process_instance_event import ProcessInstanceEventType +from spiffworkflow_backend.models.user import UserModel +from spiffworkflow_backend.services.process_instance_tmp_service import ProcessInstanceTmpService + +from tests.spiffworkflow_backend.helpers.base_test import BaseTest + + +class TestProcessInstanceEventsController(BaseTest): + def test_process_instance_migration_event_list( + self, + app: Flask, + client: FlaskClient, + with_db_and_bpmn_file_cleanup: None, + with_super_admin_user: UserModel, + ) -> None: + process_model, process_instance_id = self.create_and_run_process_instance( + client=client, + user=with_super_admin_user, + process_model_id="migration-test-with-subprocess", + bpmn_file_name="migration-initial.bpmn", + ) + process_instance = ProcessInstanceModel.query.filter_by(id=process_instance_id).first() + assert process_instance is not None + + number_of_events = 3 + for ii in range(number_of_events): + ProcessInstanceTmpService.add_event_to_process_instance( + process_instance, + ProcessInstanceEventType.process_instance_migrated.value, + migration_details={ + "initial_git_revision": f"rev{ii}", + "initial_bpmn_process_hash": f"hash{ii}", + "target_git_revision": f"rev{ii+1}", + "target_bpmn_process_hash": f"hash{ii+1}", + }, + ) + # add random event to ensure it does not come back from api + ProcessInstanceTmpService.add_event_to_process_instance( + process_instance, + ProcessInstanceEventType.process_instance_resumed.value, + ) + + response = client.get( + f"/v1.0/process-instance-events/{process_model.modified_process_model_identifier()}/{process_instance.id}/migration", + headers=self.logged_in_headers(with_super_admin_user), + ) + assert response.status_code == 200 + assert response.json + events = response.json["results"] + assert len(events) == number_of_events + + # events are returned newest first so reverse order to make checking easier + events.reverse() + for ii in range(number_of_events): + assert events[ii]["initial_git_revision"] == f"rev{ii}" + assert events[ii]["initial_bpmn_process_hash"] == f"hash{ii}" + assert events[ii]["target_git_revision"] == f"rev{ii+1}" + assert events[ii]["target_bpmn_process_hash"] == f"hash{ii+1}" + assert events[ii]["username"] == with_super_admin_user.username diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_instances_controller.py b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_instances_controller.py index e33ae3585..4e0756ede 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_instances_controller.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_instances_controller.py @@ -161,4 +161,10 @@ def test_process_instance_check_can_migrate( ) assert response.status_code == 200 assert response.json is not None - assert response.json == {"can_migrate": True, "process_instance_id": process_instance.id} + assert response.json["can_migrate"] is True + assert response.json["process_instance_id"] == process_instance.id + assert response.json["current_bpmn_process_hash"] is not None + + # this can actually be None if the process model repo is not git at all + # such as when running the docker container ci tests. + assert "current_git_revision" in response.json diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/scripts/test_get_all_permissions.py b/spiffworkflow-backend/tests/spiffworkflow_backend/scripts/test_get_all_permissions.py index 8f53b4dfc..a5bf66773 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/scripts/test_get_all_permissions.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/scripts/test_get_all_permissions.py @@ -37,6 +37,11 @@ def test_can_get_all_permissions( "uri": "/logs/typeahead-filter-values/hey:group:*", "permissions": ["read"], }, + { + "group_identifier": "my_test_group", + "uri": "/process-instance-events/hey:group:*", + "permissions": ["read"], + }, { "group_identifier": "my_test_group", "uri": "/process-instances/hey:group:*", diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_authorization_service.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_authorization_service.py index 53d8d1835..f0a667038 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_authorization_service.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_authorization_service.py @@ -128,6 +128,7 @@ def test_explode_permissions_all_on_process_group( "/process-instances/some-process-group:some-process-model:*", "delete", ), + ("/process-instance-events/some-process-group:some-process-model:*", "read"), ("/process-instances/for-me/some-process-group:some-process-model:*", "read"), ("/process-instances/some-process-group:some-process-model:*", "read"), ("/process-model-natural-language/some-process-group:some-process-model:*", "create"), @@ -168,6 +169,7 @@ def test_explode_permissions_start_on_process_group( "/process-data-file-download/some-process-group:some-process-model:*", "read", ), + ("/process-instance-events/some-process-group:some-process-model:*", "read"), ( "/process-instances/for-me/some-process-group:some-process-model:*", "read", @@ -216,6 +218,7 @@ def test_explode_permissions_all_on_process_model( "/process-instances/some-process-group:some-process-model/*", "delete", ), + ("/process-instance-events/some-process-group:some-process-model/*", "read"), ("/process-instances/for-me/some-process-group:some-process-model/*", "read"), ("/process-instances/some-process-group:some-process-model/*", "read"), ("/process-model-natural-language/some-process-group:some-process-model/*", "create"), @@ -256,6 +259,7 @@ def test_explode_permissions_start_on_process_model( "/process-data-file-download/some-process-group:some-process-model/*", "read", ), + ("/process-instance-events/some-process-group:some-process-model/*", "read"), ( "/process-instances/for-me/some-process-group:some-process-model/*", "read", @@ -538,6 +542,7 @@ def _expected_support_permissions(self) -> list[tuple[str, str]]: ("/messages/*", "create"), ("/process-data-file-download/*", "read"), ("/process-data/*", "read"), + ("/process-instance-events/*", "read"), ("/process-instance-migrate/*", "create"), ("/process-instance-reset/*", "create"), ("/process-instance-resume/*", "create"), diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_service.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_service.py index c68137d08..0cbb0c096 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_service.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_service.py @@ -7,10 +7,14 @@ import pytest from flask.app import Flask +from pytest_mock.plugin import MockerFixture from SpiffWorkflow.bpmn.util import PendingBpmnEvent # type: ignore from spiffworkflow_backend.exceptions.error import ProcessInstanceMigrationNotSafeError from spiffworkflow_backend.models.process_instance import ProcessInstanceModel from spiffworkflow_backend.models.process_instance import ProcessInstanceStatus +from spiffworkflow_backend.models.process_instance_event import ProcessInstanceEventModel +from spiffworkflow_backend.models.process_instance_event import ProcessInstanceEventType +from spiffworkflow_backend.services.git_service import GitService from spiffworkflow_backend.services.process_instance_processor import ProcessInstanceProcessor from spiffworkflow_backend.services.process_instance_service import ProcessInstanceService from spiffworkflow_backend.services.spec_file_service import SpecFileService @@ -157,6 +161,7 @@ def test_does_not_skip_duration_timer_events_for_now(self) -> None: def test_it_can_migrate_a_process_instance( self, app: Flask, + mocker: MockerFixture, with_db_and_bpmn_file_cleanup: None, ) -> None: initiator_user = self.find_or_create_user("initiator_user") @@ -165,12 +170,20 @@ def test_it_can_migrate_a_process_instance( process_model_source_directory="migration-test-with-subprocess", bpmn_file_name="migration-initial.bpmn", ) - process_instance = self.create_process_instance_from_process_model(process_model=process_model, user=initiator_user) + mock_get_current_revision = mocker.patch.object(GitService, "get_current_revision") + + # Set the return value for the first call + process_instance = self.create_process_instance_from_process_model( + process_model=process_model, user=initiator_user, bpmn_version_control_identifier="rev1" + ) processor = ProcessInstanceProcessor(process_instance) processor.do_engine_steps(save=True, execution_strategy_name="greedy") + initial_bpmn_process_hash = process_instance.bpmn_process_definition.full_process_model_hash + assert initial_bpmn_process_hash is not None initial_tasks = processor.bpmn_process_instance.get_tasks() - assert "manual_task_two" not in processor.bpmn_process_instance.spec.task_specs + spiff_task = processor.__class__.get_task_by_bpmn_identifier("manual_task_two", processor.bpmn_process_instance) + assert spiff_task is None new_file_path = os.path.join( app.instance_path, @@ -192,7 +205,8 @@ def test_it_can_migrate_a_process_instance( ) process_instance = ProcessInstanceModel.query.filter_by(id=process_instance.id).first() - ProcessInstanceService.migrate_process_instance_to_newest_model_version(process_instance, user=initiator_user) + mock_get_current_revision.return_value = "rev2" + ProcessInstanceService.migrate_process_instance(process_instance, user=initiator_user) for initial_task in initial_tasks: new_task = processor.bpmn_process_instance.get_task_from_id(initial_task.id) @@ -212,6 +226,93 @@ def test_it_can_migrate_a_process_instance( assert process_instance.status == ProcessInstanceStatus.complete.value + target_bpmn_process_hash = process_instance.bpmn_process_definition.full_process_model_hash + assert target_bpmn_process_hash is not None + assert initial_bpmn_process_hash != target_bpmn_process_hash + + pi_events = ProcessInstanceEventModel.query.filter_by( + process_instance_id=process_instance.id, event_type=ProcessInstanceEventType.process_instance_migrated.value + ).all() + assert len(pi_events) == 1 + process_instance_event = pi_events[0] + assert len(process_instance_event.migration_details) == 1 + pi_migration_details = process_instance_event.migration_details[0] + + assert pi_migration_details.initial_bpmn_process_hash == initial_bpmn_process_hash + assert pi_migration_details.target_bpmn_process_hash == target_bpmn_process_hash + assert pi_migration_details.initial_git_revision == "rev1" + assert pi_migration_details.target_git_revision == "rev2" + + def test_it_can_migrate_a_process_instance_and_revert( + self, + app: Flask, + mocker: MockerFixture, + with_db_and_bpmn_file_cleanup: None, + ) -> None: + initiator_user = self.find_or_create_user("initiator_user") + process_model = load_test_spec( + process_model_id="test_group/migration-test-with-subprocess", + process_model_source_directory="migration-test-with-subprocess", + bpmn_file_name="migration-initial.bpmn", + ) + mock_get_current_revision = mocker.patch.object(GitService, "get_current_revision") + + # Set the return value for the first call + process_instance = self.create_process_instance_from_process_model( + process_model=process_model, user=initiator_user, bpmn_version_control_identifier="rev1" + ) + processor = ProcessInstanceProcessor(process_instance) + processor.do_engine_steps(save=True, execution_strategy_name="greedy") + initial_bpmn_process_hash = process_instance.bpmn_process_definition.full_process_model_hash + assert initial_bpmn_process_hash is not None + + spiff_task = processor.__class__.get_task_by_bpmn_identifier("manual_task_two", processor.bpmn_process_instance) + assert spiff_task is None + + new_file_path = os.path.join( + app.instance_path, + "..", + "..", + "tests", + "data", + "migration-test-with-subprocess", + "migration-new.bpmn", + ) + with open(new_file_path) as f: + new_contents = f.read().encode() + + SpecFileService.update_file( + process_model_info=process_model, + file_name="migration-initial.bpmn", + binary_data=new_contents, + update_process_cache_only=True, + ) + + process_instance = ProcessInstanceModel.query.filter_by(id=process_instance.id).first() + mock_get_current_revision.return_value = "rev2" + ProcessInstanceService.migrate_process_instance(process_instance, user=initiator_user) + process_instance = ProcessInstanceModel.query.filter_by(id=process_instance.id).first() + processor = ProcessInstanceProcessor(process_instance) + processor.do_engine_steps(save=True, execution_strategy_name="greedy") + spiff_task = processor.__class__.get_task_by_bpmn_identifier("manual_task_two", processor.bpmn_process_instance) + assert spiff_task is not None + + ProcessInstanceService.migrate_process_instance( + process_instance, user=initiator_user, target_bpmn_process_hash=initial_bpmn_process_hash + ) + processor = ProcessInstanceProcessor(process_instance) + spiff_task = processor.__class__.get_task_by_bpmn_identifier("manual_task_two", processor.bpmn_process_instance) + assert spiff_task is None + human_task_one = process_instance.active_human_tasks[0] + assert human_task_one.task_model.task_definition.bpmn_identifier == "manual_task_one" + self.complete_next_manual_task(processor) + assert process_instance.status == ProcessInstanceStatus.complete.value + + pi_events = ProcessInstanceEventModel.query.filter_by( + process_instance_id=process_instance.id, event_type=ProcessInstanceEventType.process_instance_migrated.value + ).all() + assert len(pi_events) == 2 + def test_it_can_check_if_a_process_instance_can_be_migrated( self, app: Flask, diff --git a/spiffworkflow-frontend/src/a-spiffui-v2/views/Dashboards/myProcesses/CellRenderer.tsx b/spiffworkflow-frontend/src/a-spiffui-v2/views/Dashboards/myProcesses/CellRenderer.tsx index 1aa5b26a7..6cbc1e0e0 100644 --- a/spiffworkflow-frontend/src/a-spiffui-v2/views/Dashboards/myProcesses/CellRenderer.tsx +++ b/spiffworkflow-frontend/src/a-spiffui-v2/views/Dashboards/myProcesses/CellRenderer.tsx @@ -1,15 +1,18 @@ // react doesn't like the name "useTheme" but we don't control that /* eslint-disable react-hooks/rules-of-hooks */ import { Chip, Stack, Typography, useTheme } from '@mui/material'; +import DateAndTimeService from '../../../../services/DateAndTimeService'; import { formatSecondsForDisplay } from '../../../utils/Utils'; /** Used by the Processes datagrid in Dashboards to render things like chips on cells, etc. */ export default function CellRenderer({ header, data, + title, }: { header: string; data: Record; + title?: string; }) { /** These values map to theme tokens, which enable the light/dark modes etc. */ const chipBackground = (params: any) => { @@ -57,15 +60,32 @@ export default function CellRenderer({ alignItems: 'center', }} > - + {formatSecondsForDisplay(data.value)} ); } + if (header === 'timestamp') { + return ( + + + {DateAndTimeService.convertSecondsToFormattedDateTime(data.value)} + + + ); + } return ( - {data.value} + + {data.value} + ); } diff --git a/spiffworkflow-frontend/src/a-spiffui-v2/views/Dashboards/myProcesses/MyProcesses.tsx b/spiffworkflow-frontend/src/a-spiffui-v2/views/Dashboards/myProcesses/MyProcesses.tsx index bb8684bcd..c4a55bcfb 100644 --- a/spiffworkflow-frontend/src/a-spiffui-v2/views/Dashboards/myProcesses/MyProcesses.tsx +++ b/spiffworkflow-frontend/src/a-spiffui-v2/views/Dashboards/myProcesses/MyProcesses.tsx @@ -90,9 +90,9 @@ export default function MyProcesses({ headerName: column.Header, colSpan: 0, flex: 1, - renderCell: (params: Record) => ( - - ), + renderCell: (params: Record) => { + return ; + }, }), ); diff --git a/spiffworkflow-frontend/src/hooks/UriListForPermissions.tsx b/spiffworkflow-frontend/src/hooks/UriListForPermissions.tsx index 0ef121696..84f81c738 100644 --- a/spiffworkflow-frontend/src/hooks/UriListForPermissions.tsx +++ b/spiffworkflow-frontend/src/hooks/UriListForPermissions.tsx @@ -22,6 +22,7 @@ export const useUriListForPermissions = () => { processInstanceListForMePath: `/v1.0/process-instances/for-me`, processInstanceLogListPath: `/v1.0/logs/${params.process_model_id}/${params.process_instance_id}`, processInstanceReportListPath: '/v1.0/process-instances/reports', + processInstanceMigratePath: `/v1.0/process-instance-migrate/${params.process_model_id}/${params.process_instance_id}`, processInstanceResetPath: `/v1.0/process-instance-reset/${params.process_model_id}/${params.process_instance_id}`, processInstanceResumePath: `/v1.0/process-instance-resume/${params.process_model_id}/${params.process_instance_id}`, processInstanceSendEventPath: `/v1.0/send-event/${params.process_model_id}/${params.process_instance_id}`, diff --git a/spiffworkflow-frontend/src/interfaces.ts b/spiffworkflow-frontend/src/interfaces.ts index af29366e0..599d253aa 100644 --- a/spiffworkflow-frontend/src/interfaces.ts +++ b/spiffworkflow-frontend/src/interfaces.ts @@ -548,3 +548,13 @@ export interface PublicTask { export interface RJSFFormObject { formData: any; } + +export interface MigrationEvent { + id: number; + initial_bpmn_process_hash: string; + initial_git_revision: string; + target_bpmn_process_hash: string; + target_git_revision: string; + timestamp: string; + username: string; +} diff --git a/spiffworkflow-frontend/src/routes/ProcessInstanceMigratePage.tsx b/spiffworkflow-frontend/src/routes/ProcessInstanceMigratePage.tsx new file mode 100644 index 000000000..08eedcc46 --- /dev/null +++ b/spiffworkflow-frontend/src/routes/ProcessInstanceMigratePage.tsx @@ -0,0 +1,364 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import { useParams } from 'react-router-dom'; +import { + Alert, + AlertTitle, + CircularProgress, + Typography, + Box, + Button, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, +} from '@mui/material'; +import CheckCircleIcon from '@mui/icons-material/CheckCircle'; + +import { green } from '@mui/material/colors'; +import { DataGrid } from '@mui/x-data-grid'; +import HttpService from '../services/HttpService'; + +import ProcessBreadcrumb from '../components/ProcessBreadcrumb'; +import { useUriListForPermissions } from '../hooks/UriListForPermissions'; +import { MigrationEvent } from '../interfaces'; +import CellRenderer from '../a-spiffui-v2/views/Dashboards/myProcesses/CellRenderer'; + +function DangerousMigrationButton({ + successCallback, + failureCallback, + migrationEvent, + title, + buttonText = 'Migrate to Newest', +}: { + successCallback: (result: any) => void; + failureCallback: (error: any) => void; + title?: string; + migrationEvent?: MigrationEvent; + buttonText?: string; +}) { + const [openDialog, setOpenDialog] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const { targetUris } = useUriListForPermissions(); + + const handleRunMigration = () => { + setIsLoading(true); + let queryParams = ''; + if (migrationEvent) { + queryParams = `?target_bpmn_process_hash=${migrationEvent.initial_bpmn_process_hash}`; + } + HttpService.makeCallToBackend({ + httpMethod: 'POST', + path: `${targetUris.processInstanceMigratePath}${queryParams}`, + successCallback: (result: any) => { + setIsLoading(false); + setOpenDialog(false); + successCallback(result); + }, + failureCallback: (error: any) => { + setIsLoading(false); + setOpenDialog(false); + failureCallback(error); + }, + }); + }; + + const handleOpenDialog = () => { + setOpenDialog(true); + }; + + const handleCloseDialog = () => { + setOpenDialog(false); + }; + + return ( + <> + + + + Confirm Migrate Instance + + + Are you sure you want to proceed with this potentially-dangerous + process instance migration? + + + + + + + + + ); +} +function MigrationStatus({ + migrationCheckResult, +}: { + migrationCheckResult: any | null; +}) { + let returnComponent = null; + if (migrationCheckResult === null) { + returnComponent = ( + <> + + Checking if ready to migrate + + ); + } else if (migrationCheckResult.can_migrate) { + returnComponent = ( + <> + + + Ready to migrate to newest process model version + + + ); + } else if ( + migrationCheckResult.exception_class === + 'ProcessInstanceMigrationUnnecessaryError' + ) { + returnComponent = ( + + Nothing to migrateThe process instance is + already associated with the most recent process model version. + + ); + } else { + returnComponent = ( + + Process instance not migratable + Process Instances can be migrated if the target process model version + has not changed any of the tasks that have already been completed and if + the process instance is not already at the newest version. + + ); + } + return ( + + {returnComponent} + + ); +} + +export default function ProcessInstanceMigratePage() { + const params = useParams(); + const [migrationCheckResult, setMigrationCheckResult] = useState(null); + const [migrationResult, setMigrationResult] = useState(null); + const [migrationEvents, setMigrationEvents] = useState< + MigrationEvent[] | null + >(null); + + const fetchProcessInstanceMigrationDetails = useCallback(() => { + HttpService.makeCallToBackend({ + path: `/process-instances/${params.process_model_id}/${params.process_instance_id}/check-can-migrate`, + successCallback: (result: any) => setMigrationCheckResult(result), + }); + HttpService.makeCallToBackend({ + path: `/process-instance-events/${params.process_model_id}/${params.process_instance_id}/migration`, + successCallback: (result: any) => setMigrationEvents(result.results), + }); + }, [params.process_instance_id, params.process_model_id]); + + useEffect(fetchProcessInstanceMigrationDetails, [ + fetchProcessInstanceMigrationDetails, + ]); + + const onMigrationComplete = (result: any) => { + fetchProcessInstanceMigrationDetails(); + setMigrationResult(result); + }; + const onMigrationFailure = (error: any) => { + setMigrationResult(error); + }; + + const migrationResultComponent = () => { + if (!migrationResult) { + return null; + } + + if (migrationResult.ok) { + return Process instance migrated; + } + + let alertBody = null; + if (typeof migrationResult === 'string') { + alertBody = migrationResult; + } else if ( + typeof migrationResult === 'object' && + migrationResult.error_code + ) { + alertBody = migrationResult.message; + } else { + alertBody = ( +
    + {Object.keys(migrationResult).map((key) => ( +
  • + {key}: {migrationResult[key]} +
  • + ))} +
+ ); + } + return ( + + Error + {alertBody} + + ); + }; + + const processInstanceMigrationEventTable = () => { + if (!migrationEvents || migrationEvents.length === 0) { + return null; + } + + const columns = [ + { + field: 'username', + headerName: 'Username', + flex: 2, + renderCell: (data: Record) => { + return ; + }, + }, + { + field: 'timestamp', + headerName: 'Timestamp', + flex: 2, + renderCell: (data: Record) => { + return ; + }, + }, + { + field: 'initial_git_revision', + headerName: 'Initial Git Revision', + flex: 2, + renderCell: (data: Record) => { + return ( + + ); + }, + }, + { + field: 'target_git_revision', + headerName: 'Target Git Revision', + flex: 2, + renderCell: (data: Record) => { + return ( + + ); + }, + }, + { + field: 'actions', + headerName: 'Actions', + flex: 2, + renderCell: (data: Record) => { + if ( + migrationCheckResult && + data.row.initial_bpmn_process_hash === + migrationCheckResult.current_bpmn_process_hash + ) { + return null; + } + return ( + + ); + }, + }, + ]; + const rows = migrationEvents.map((migrationEvent: MigrationEvent) => { + console.log('migrationEvent', migrationEvent); + return migrationEvent; + }); + return ( + <> +

Previous Migrations

+ 'auto'} + rows={rows} + columns={columns} + hideFooter + /> + + ); + }; + + return ( + <> + +
+ {migrationResultComponent()} +
+ +
+ {migrationCheckResult?.can_migrate ? ( + + ) : null} +
+
+ {processInstanceMigrationEventTable()} + + ); +} diff --git a/spiffworkflow-frontend/src/routes/ProcessInstanceRoutes.tsx b/spiffworkflow-frontend/src/routes/ProcessInstanceRoutes.tsx index f7294c10a..88f202299 100644 --- a/spiffworkflow-frontend/src/routes/ProcessInstanceRoutes.tsx +++ b/spiffworkflow-frontend/src/routes/ProcessInstanceRoutes.tsx @@ -7,6 +7,7 @@ import ProcessInstanceReportEdit from './ProcessInstanceReportEdit'; import ProcessInstanceFindById from './ProcessInstanceFindById'; import ProcessInterstitialPage from './ProcessInterstitialPage'; import ProcessInstanceProgressPage from './ProcessInstanceProgressPage'; +import ProcessInstanceMigratePage from './ProcessInstanceMigratePage'; export default function ProcessInstanceRoutes() { return ( @@ -38,6 +39,10 @@ export default function ProcessInstanceRoutes() { path=":process_model_id/:process_instance_id/progress" element={} /> + } + /> } diff --git a/spiffworkflow-frontend/src/routes/ProcessInstanceShow.tsx b/spiffworkflow-frontend/src/routes/ProcessInstanceShow.tsx index 52e6d06e1..a64323b32 100644 --- a/spiffworkflow-frontend/src/routes/ProcessInstanceShow.tsx +++ b/spiffworkflow-frontend/src/routes/ProcessInstanceShow.tsx @@ -23,6 +23,7 @@ import { Warning, Link as LinkIcon, View, + Migrate, } from '@carbon/icons-react'; import { Accordion, @@ -151,6 +152,7 @@ export default function ProcessInstanceShow({ variant }: OwnProps) { : targetUris.processInstanceTaskListForMePath; const permissionRequestData: PermissionsToCheck = { + [`${targetUris.processInstanceMigratePath}`]: ['POST'], [`${targetUris.processInstanceResumePath}`]: ['POST'], [`${targetUris.processInstanceSuspendPath}`]: ['POST'], [`${targetUris.processInstanceTerminatePath}`]: ['POST'], @@ -591,6 +593,12 @@ export default function ProcessInstanceShow({ variant }: OwnProps) { } }; + const navigateToProcessInstanceMigratePage = () => { + navigate( + `/process-instances/${params.process_model_id}/${params.process_instance_id}/migrate`, + ); + }; + const terminateButton = () => { if ( processInstance && @@ -633,6 +641,21 @@ export default function ProcessInstanceShow({ variant }: OwnProps) { } return
; }; + const migrateButton = () => { + if (processInstance && processInstance.status === 'suspended') { + return ( +