diff options
56 files changed, 3775 insertions, 2478 deletions
diff --git a/.zuul.yaml b/.zuul.yaml index 3efcff86b..72684c460 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -1,3 +1,6 @@ +- queue: + name: zuul + - nodeset: name: zuul-functional-temp-master nodes: @@ -286,6 +289,7 @@ nodeset: ubuntu-jammy - project: + queue: zuul vars: node_version: 16 release_python: python3 diff --git a/TESTING.rst b/TESTING.rst index 6c4c8c0d8..fe9af66f6 100644 --- a/TESTING.rst +++ b/TESTING.rst @@ -1,34 +1,30 @@ -=========================== -Testing Your OpenStack Code -=========================== +============ +Testing Zuul +============ ------------ A Quickstart ------------ -This is designed to be enough information for you to run your first tests. -Detailed information on testing can be found here: https://wiki.openstack.org/wiki/Testing +This is designed to be enough information for you to run your first tests on +an Ubuntu 20.04 (or later) host. *Install pip*:: - [apt-get | yum] install python-pip + sudo apt-get install python3-pip More information on pip here: http://www.pip-installer.org/en/latest/ -*Use pip to install tox and tox-docker*:: +*Use pip to install tox*:: - pip install tox tox-docker + pip install tox -As of zuul v3, a running zookeeper is required to execute tests. Using the -``-docker`` suffixed commands will ensure this is started automatically by tox, -but if you do not wish to use this: +A running zookeeper is required to execute tests, but it also needs to be +configured for TLS and a certificate authority set up to handle socket +authentication. Because of these complexities, it's recommended to use a +helper script to set up these dependencies, as well as a database servers:: -*Install zookeeper*:: - - [apt-get | yum] install zookeeperd - -*Start zookeeper*:: - - service zookeeper start + sudo apt-get install docker-compose # or podman-compose if preferred + ROOTCMD=sudo tools/test-setup-docker.sh .. note:: Installing and bulding javascript is not required, but tests that depend on the javascript assets having been built will be skipped diff --git a/doc/source/drivers/timer.rst b/doc/source/drivers/timer.rst index 1d7931c5e..57de1573d 100644 --- a/doc/source/drivers/timer.rst +++ b/doc/source/drivers/timer.rst @@ -18,6 +18,9 @@ will enqueue an event into its pipeline for every project and branch defined in the configuration. Any job associated with the pipeline will run in response to that event. +Zuul implements the timer using `apscheduler`_, Please check the +`apscheduler documentation`_ for more information about the syntax. + .. attr:: pipeline.trigger.timer The timer trigger supports the following attributes: @@ -27,9 +30,17 @@ will run in response to that event. The time specification in cron syntax. Only the 5 part syntax is supported, not the symbolic names. Example: ``0 0 * * *`` - runs at midnight. The first weekday is Monday. An optional 6th - part specifies seconds. The optional 7th part specifies a - jitter in seconds. This delays the trigger randomly, limited by + runs at midnight. + An optional 6th part specifies seconds. The optional 7th part specifies + a jitter in seconds. This delays the trigger randomly, limited by the specified value. Example ``0 0 * * * * 60`` runs at midnight or randomly up to 60 seconds later. The jitter is applied individually to each project-branch combination. + + .. warning:: + Be aware the day-of-week value differs from from cron. + The first weekday is Monday (0), and the last is Sunday (6). + + +.. _apscheduler: https://apscheduler.readthedocs.io/ +.. _apscheduler documentation: https://apscheduler.readthedocs.io/en/3.x/modules/triggers/cron.html#module-apscheduler.triggers.cron diff --git a/doc/source/examples/keycloak/docker-compose.yaml b/doc/source/examples/keycloak/docker-compose.yaml index 917c50231..9e70fec5f 100644 --- a/doc/source/examples/keycloak/docker-compose.yaml +++ b/doc/source/examples/keycloak/docker-compose.yaml @@ -7,21 +7,19 @@ version: '2.1' services: keycloak: - image: docker.io/jboss/keycloak + image: quay.io/keycloak/keycloak:18.0 environment: - - KEYCLOAK_USER=admin - - KEYCLOAK_PASSWORD=kcadmin + - KEYCLOAK_ADMIN=admin + - KEYCLOAK_ADMIN_PASSWORD=kcadmin - DB_VENDOR=h2 - - KEYCLOAK_IMPORT=/var/keycloak_import/zuul-demo-data.json - - JAVA_OPTS_APPEND="-Djboss.socket.binding.port-offset=2" + - KC_HTTP_PORT=8082 ports: - "8082:8082" volumes: - - "./keycloak/:/var/keycloak_import/:z" - entrypoint: | - /bin/sh -c '\ - /opt/jboss/tools/docker-entrypoint.sh -b 0.0.0.0' - command: [] + - "./keycloak/:/opt/keycloak/data/import/:z" + command: + - start-dev + - --import-realm networks: - zuul diff --git a/doc/source/examples/keycloak/etc_zuul/zuul.conf b/doc/source/examples/keycloak/etc_zuul/zuul.conf index 35cabe807..f2401cc90 100644 --- a/doc/source/examples/keycloak/etc_zuul/zuul.conf +++ b/doc/source/examples/keycloak/etc_zuul/zuul.conf @@ -14,7 +14,7 @@ tenant_config=/etc/zuul/main.yaml default=true driver=OpenIDConnect realm=zuul-demo -issuer_id=http://keycloak:8082/auth/realms/zuul-demo +issuer_id=http://keycloak:8082/realms/zuul-demo client_id=zuul [connection "gerrit"] diff --git a/doc/source/examples/keycloak/keycloak/zuul-demo-data.json b/doc/source/examples/keycloak/keycloak/zuul-demo-data.json index 448267eed..194814e1b 100644 --- a/doc/source/examples/keycloak/keycloak/zuul-demo-data.json +++ b/doc/source/examples/keycloak/keycloak/zuul-demo-data.json @@ -1,2224 +1,1927 @@ { - "id": "zuul-demo", - "realm": "zuul-demo", - "notBefore": 0, - "revokeRefreshToken": false, - "refreshTokenMaxReuse": 0, - "accessTokenLifespan": 300, - "accessTokenLifespanForImplicitFlow": 900, - "ssoSessionIdleTimeout": 1800, - "ssoSessionMaxLifespan": 36000, - "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, - "enabled": true, - "sslRequired": "external", - "registrationAllowed": true, - "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": "b295f2e1-c823-4f30-84b4-c534676c3ded", - "name": "uma_authorization", - "composite": false, - "clientRole": false, - "containerId": "zuul-demo", - "attributes": {} - }, - { - "id": "047a5001-b3fe-452a-8fd3-985d82f7df31", - "name": "offline_access", - "description": "${role_offline-access}", - "composite": false, - "clientRole": false, - "containerId": "zuul-demo", - "attributes": {} - } - ], - "client": { - "realm-management": [ - { - "id": "7e18128e-7a99-4403-8107-5a90ac0b952c", - "name": "view-identity-providers", - "description": "${role_view-identity-providers}", - "composite": false, - "clientRole": true, - "containerId": "63ae0908-5213-436d-b3d6-5a611eb24216", - "attributes": {} - }, - { - "id": "95f5f229-415e-48ba-8bfc-ff0a750f119e", - "name": "view-users", - "description": "${role_view-users}", - "composite": true, - "composites": { - "client": { - "realm-management": [ - "query-groups", - "query-users" - ] - } - }, - "clientRole": true, - "containerId": "63ae0908-5213-436d-b3d6-5a611eb24216", - "attributes": {} - }, - { - "id": "7cffdbb0-ced3-4fb0-98de-6b1d8506dbe4", - "name": "manage-identity-providers", - "description": "${role_manage-identity-providers}", - "composite": false, - "clientRole": true, - "containerId": "63ae0908-5213-436d-b3d6-5a611eb24216", - "attributes": {} - }, - { - "id": "08729d50-a31e-42fd-ad81-120cdee3b1b9", - "name": "query-clients", - "description": "${role_query-clients}", - "composite": false, - "clientRole": true, - "containerId": "63ae0908-5213-436d-b3d6-5a611eb24216", - "attributes": {} - }, - { - "id": "69ffc663-32cb-4f4e-a9af-669bd23b4edc", - "name": "query-groups", - "description": "${role_query-groups}", - "composite": false, - "clientRole": true, - "containerId": "63ae0908-5213-436d-b3d6-5a611eb24216", - "attributes": {} - }, - { - "id": "2a9f4d72-35bb-4072-b8aa-3fe7f1ab7f26", - "name": "view-clients", - "description": "${role_view-clients}", - "composite": true, - "composites": { - "client": { - "realm-management": [ - "query-clients" - ] - } - }, - "clientRole": true, - "containerId": "63ae0908-5213-436d-b3d6-5a611eb24216", - "attributes": {} - }, - { - "id": "16c74fae-7feb-446d-9d42-08b31583ddf5", - "name": "manage-events", - "description": "${role_manage-events}", - "composite": false, - "clientRole": true, - "containerId": "63ae0908-5213-436d-b3d6-5a611eb24216", - "attributes": {} - }, - { - "id": "30cc0071-f914-446a-9aa3-a3372d9f45c0", - "name": "manage-authorization", - "description": "${role_manage-authorization}", - "composite": false, - "clientRole": true, - "containerId": "63ae0908-5213-436d-b3d6-5a611eb24216", - "attributes": {} - }, - { - "id": "d3d32a6d-f1da-410f-8c52-62d1b2ac7abc", - "name": "query-realms", - "description": "${role_query-realms}", - "composite": false, - "clientRole": true, - "containerId": "63ae0908-5213-436d-b3d6-5a611eb24216", - "attributes": {} - }, - { - "id": "1f7afd46-36a8-46e8-abc1-8949836ec532", - "name": "view-authorization", - "description": "${role_view-authorization}", - "composite": false, - "clientRole": true, - "containerId": "63ae0908-5213-436d-b3d6-5a611eb24216", - "attributes": {} - }, - { - "id": "6384baeb-1ec0-4e69-a5c2-7b632c3bfda5", - "name": "impersonation", - "description": "${role_impersonation}", - "composite": false, - "clientRole": true, - "containerId": "63ae0908-5213-436d-b3d6-5a611eb24216", - "attributes": {} - }, - { - "id": "eefc7ff0-e2f2-4381-b034-c96d0bae3769", - "name": "realm-admin", - "description": "${role_realm-admin}", - "composite": true, - "composites": { - "client": { - "realm-management": [ - "view-identity-providers", - "view-users", - "manage-identity-providers", - "query-clients", - "query-groups", - "view-clients", - "manage-events", - "view-authorization", - "manage-authorization", - "query-realms", - "impersonation", - "create-client", - "manage-clients", - "manage-users", - "manage-realm", - "view-events", - "view-realm", - "query-users" - ] - } - }, - "clientRole": true, - "containerId": "63ae0908-5213-436d-b3d6-5a611eb24216", - "attributes": {} - }, - { - "id": "1c104476-dc65-4df5-9f0e-3f9086a75813", - "name": "create-client", - "description": "${role_create-client}", - "composite": false, - "clientRole": true, - "containerId": "63ae0908-5213-436d-b3d6-5a611eb24216", - "attributes": {} - }, - { - "id": "f0893b2a-770e-4b21-a430-1a9974ecc01a", - "name": "manage-clients", - "description": "${role_manage-clients}", - "composite": false, - "clientRole": true, - "containerId": "63ae0908-5213-436d-b3d6-5a611eb24216", - "attributes": {} - }, - { - "id": "dfddee2e-682a-47ea-924c-59699f338e0c", - "name": "manage-users", - "description": "${role_manage-users}", - "composite": false, - "clientRole": true, - "containerId": "63ae0908-5213-436d-b3d6-5a611eb24216", - "attributes": {} - }, - { - "id": "a8198df9-d3cd-460f-a48f-ccede56e08a9", - "name": "manage-realm", - "description": "${role_manage-realm}", - "composite": false, - "clientRole": true, - "containerId": "63ae0908-5213-436d-b3d6-5a611eb24216", - "attributes": {} - }, - { - "id": "657b4b26-98c5-4963-a353-db26453f5dd2", - "name": "view-events", - "description": "${role_view-events}", - "composite": false, - "clientRole": true, - "containerId": "63ae0908-5213-436d-b3d6-5a611eb24216", - "attributes": {} - }, - { - "id": "693093ae-6004-48be-bb22-8804f61af1c0", - "name": "view-realm", - "description": "${role_view-realm}", - "composite": false, - "clientRole": true, - "containerId": "63ae0908-5213-436d-b3d6-5a611eb24216", - "attributes": {} - }, - { - "id": "418abb16-fdc3-402e-8125-52800990445b", - "name": "query-users", - "description": "${role_query-users}", - "composite": false, - "clientRole": true, - "containerId": "63ae0908-5213-436d-b3d6-5a611eb24216", - "attributes": {} - } - ], - "security-admin-console": [], - "admin-cli": [], - "account-console": [], - "zuul": [], - "broker": [], - "account": [ - { - "id": "b839cd34-f23d-4c14-8060-ed635e708b87", - "name": "view-consent", - "description": "${role_view-consent}", - "composite": false, - "clientRole": true, - "containerId": "6fd2abe1-4aeb-4834-8a9e-e3f499d64a03", - "attributes": {} - }, - { - "id": "a98bcb3b-1584-45ab-afa8-e431ddfed5e7", - "name": "view-applications", - "description": "${role_view-applications}", - "composite": false, - "clientRole": true, - "containerId": "6fd2abe1-4aeb-4834-8a9e-e3f499d64a03", - "attributes": {} - }, - { - "id": "471d721c-872c-4004-bb76-e6399f5e1fd0", - "name": "delete-account", - "description": "${role_delete-account}", - "composite": false, - "clientRole": true, - "containerId": "6fd2abe1-4aeb-4834-8a9e-e3f499d64a03", - "attributes": {} - }, - { - "id": "e5a0f953-bdcb-4136-bfbf-0c311797e57f", - "name": "manage-account", - "composite": false, - "clientRole": true, - "containerId": "6fd2abe1-4aeb-4834-8a9e-e3f499d64a03", - "attributes": {} - }, - { - "id": "c8726cd0-6447-4131-9731-717897ff167a", - "name": "manage-consent", - "description": "${role_manage-consent}", - "composite": true, - "composites": { - "client": { - "account": [ - "view-consent" - ] - } - }, - "clientRole": true, - "containerId": "6fd2abe1-4aeb-4834-8a9e-e3f499d64a03", - "attributes": {} - }, - { - "id": "eb6c2340-cecf-4095-bdc7-d14f38a9aee6", - "name": "view-profile", - "composite": false, - "clientRole": true, - "containerId": "6fd2abe1-4aeb-4834-8a9e-e3f499d64a03", - "attributes": {} - } - ], - } - }, - "groups": [ - { - "id": "d372cb0e-f3c8-40be-8527-73775d1f7e47", - "name": "example-tenant-admin", - "path": "/example-tenant-admin", - "attributes": {}, - "realmRoles": [], - "clientRoles": {}, - "subGroups": [] - } - ], - "defaultRoles": [ - "uma_authorization", - "offline_access" - ], - "requiredCredentials": [ - "password" - ], - "otpPolicyType": "totp", - "otpPolicyAlgorithm": "HmacSHA1", - "otpPolicyInitialCounter": 0, - "otpPolicyDigits": 6, - "otpPolicyLookAheadWindow": 1, - "otpPolicyPeriod": 30, - "otpSupportedApplications": [ - "FreeOTP", - "Google Authenticator" - ], - "webAuthnPolicyRpEntityName": "keycloak", - "webAuthnPolicySignatureAlgorithms": [ - "ES256" - ], - "webAuthnPolicyRpId": "", - "webAuthnPolicyAttestationConveyancePreference": "not specified", - "webAuthnPolicyAuthenticatorAttachment": "not specified", - "webAuthnPolicyRequireResidentKey": "not specified", - "webAuthnPolicyUserVerificationRequirement": "not specified", - "webAuthnPolicyCreateTimeout": 0, - "webAuthnPolicyAvoidSameAuthenticatorRegister": false, - "webAuthnPolicyAcceptableAaguids": [], - "webAuthnPolicyPasswordlessRpEntityName": "keycloak", - "webAuthnPolicyPasswordlessSignatureAlgorithms": [ - "ES256" - ], - "webAuthnPolicyPasswordlessRpId": "", - "webAuthnPolicyPasswordlessAttestationConveyancePreference": "not specified", - "webAuthnPolicyPasswordlessAuthenticatorAttachment": "not specified", - "webAuthnPolicyPasswordlessRequireResidentKey": "not specified", - "webAuthnPolicyPasswordlessUserVerificationRequirement": "not specified", - "webAuthnPolicyPasswordlessCreateTimeout": 0, - "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister": false, - "webAuthnPolicyPasswordlessAcceptableAaguids": [], - "users": [ - { - "id": "3defcf9c-a89d-496e-9769-526bc65b6db6", - "createdTimestamp": 1609929909934, - "username": "admin", - "enabled": true, - "totp": false, - "emailVerified": false, - "firstName": "Zuul", - "lastName": "Administrator", - "email": "admin@example.com", - "credentials": [ - { - "id": "1a05a791-be4d-4060-ae6b-e117c6c8de70", - "type": "password", - "createdDate": 1609929922393, - "secretData": "{\"value\":\"BtE429Fzy8ygtLLD+eEHHY5DzJr3pzDf5wBwR8ZD4HFRPPn6NldxGTy+0AEJBGAQt+dZ0eEqVV8edytNR8PwMw==\",\"salt\":\"jL8euyHG4ZQy2BUMY/LpIg==\",\"additionalParameters\":{}}", - "credentialData": "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + "id" : "zuul-demo", + "realm" : "zuul-demo", + "notBefore" : 0, + "defaultSignatureAlgorithm" : "RS256", + "revokeRefreshToken" : false, + "refreshTokenMaxReuse" : 0, + "accessTokenLifespan" : 300, + "accessTokenLifespanForImplicitFlow" : 900, + "ssoSessionIdleTimeout" : 1800, + "ssoSessionMaxLifespan" : 36000, + "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" : "56408c69-b6fb-4845-be59-db262fe21dea", + "name" : "offline_access", + "description" : "${role_offline-access}", + "composite" : false, + "clientRole" : false, + "containerId" : "zuul-demo", + "attributes" : { } + }, { + "id" : "156d7b5b-65fb-4c89-b3d8-c2f82e9ecdd4", + "name" : "default-roles-zuul-demo", + "description" : "${role_default-roles}", + "composite" : true, + "composites" : { + "realm" : [ "offline_access", "uma_authorization" ], + "client" : { + "account" : [ "view-profile", "manage-account" ] } - ], - "disableableCredentialTypes": [], - "requiredActions": [], - "realmRoles": [ - "uma_authorization", - "offline_access" - ], - "clientRoles": { - "account": [ - "manage-account", - "view-profile" - ] - }, - "notBefore": 0, - "groups": [] - }, - { - "id": "091fbeb0-ac10-47eb-a113-88fbb0a7988f", - "createdTimestamp": 1609929807691, - "username": "user1", - "enabled": true, - "totp": false, - "emailVerified": false, - "firstName": "User", - "lastName": "One", - "email": "user1@example.com", - "credentials": [ - { - "id": "caec7277-9620-4232-b07f-10076ae32e17", - "type": "password", - "createdDate": 1609929824753, - "secretData": "{\"value\":\"XbD+O1HUVhvqMg2xP36il/haxbzS65GjV2YYwqJWGfJuztQCn2G5ArmeWcqMxEd6BEBnPjZCjM3tpaBSFluLvg==\",\"salt\":\"8dXWJvuUtbqZuBw3ZUL4zA==\",\"additionalParameters\":{}}", - "credentialData": "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" - } - ], - "disableableCredentialTypes": [], - "requiredActions": [], - "realmRoles": [ - "uma_authorization", - "offline_access" - ], - "clientRoles": { - "account": [ - "manage-account", - "view-profile" - ] - }, - "notBefore": 0, - "groups": [ - "/example-tenant-admin" - ] - }, - { - "id": "633cbfcd-43d9-4588-9c74-7af4ea007dda", - "createdTimestamp": 1609929870956, - "username": "user2", - "enabled": true, - "totp": false, - "emailVerified": false, - "firstName": "User", - "lastName": "Two", - "email": "user2@example.com", - "credentials": [ - { - "id": "1975173e-f07c-4223-9bf6-fa44dcd27cf0", - "type": "password", - "createdDate": 1609929881586, - "secretData": "{\"value\":\"SZ0ESXObiHfCOo4m9afbnpNaZ52H0k0VVuHe2PecmUZ4FxpAXbCsUimNNsz5VVRdqhAqWJi2AcExCoKFSJzeug==\",\"salt\":\"+pc2TGNg/CjypsBPjH0YJg==\",\"additionalParameters\":{}}", - "credentialData": "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" - } - ], - "disableableCredentialTypes": [], - "requiredActions": [], - "realmRoles": [ - "uma_authorization", - "offline_access" - ], - "clientRoles": { - "account": [ - "manage-account", - "view-profile" - ] - }, - "notBefore": 0, - "groups": [] - } - ], - "scopeMappings": [ - { - "clientScope": "offline_access", - "roles": [ - "offline_access" - ] - } - ], - "clientScopeMappings": { - "account": [ - { - "client": "account-console", - "roles": [ - "manage-account" - ] - } - ] - }, - "clients": [ - { - "id": "6fd2abe1-4aeb-4834-8a9e-e3f499d64a03", - "clientId": "account", - "name": "${client_account}", - "rootUrl": "${authBaseUrl}", - "baseUrl": "/realms/zuul-demo/account/", - "surrogateAuthRequired": false, - "enabled": true, - "alwaysDisplayInConsole": false, - "clientAuthenticatorType": "client-secret", - "secret": "**********", - "defaultRoles": [ - "manage-account", - "view-profile" - ], - "redirectUris": [ - "/realms/zuul-demo/account/*" - ], - "webOrigins": [], - "notBefore": 0, - "bearerOnly": false, - "consentRequired": false, - "standardFlowEnabled": true, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": false, - "serviceAccountsEnabled": false, - "publicClient": false, - "frontchannelLogout": false, - "protocol": "openid-connect", - "attributes": {}, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": false, - "nodeReRegistrationTimeout": 0, - "defaultClientScopes": [ - "web-origins", - "role_list", - "profile", - "roles", - "email" - ], - "optionalClientScopes": [ - "address", - "phone", - "offline_access", - "microprofile-jwt" - ] - }, - { - "id": "1958e9b1-c3ce-4bec-89bd-3b0050a4dacf", - "clientId": "account-console", - "name": "${client_account-console}", - "rootUrl": "${authBaseUrl}", - "baseUrl": "/realms/zuul-demo/account/", - "surrogateAuthRequired": false, - "enabled": true, - "alwaysDisplayInConsole": false, - "clientAuthenticatorType": "client-secret", - "secret": "e555a236-3a76-4ac9-a55a-48f81a00535f", - "redirectUris": [ - "/realms/zuul-demo/account/*" - ], - "webOrigins": [], - "notBefore": 0, - "bearerOnly": false, - "consentRequired": false, - "standardFlowEnabled": true, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": false, - "serviceAccountsEnabled": false, - "publicClient": true, - "frontchannelLogout": false, - "protocol": "openid-connect", - "attributes": { - "pkce.code.challenge.method": "S256" - }, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": false, - "nodeReRegistrationTimeout": 0, - "protocolMappers": [ - { - "id": "5a075254-4849-43dc-b036-53c606571988", - "name": "audience resolve", - "protocol": "openid-connect", - "protocolMapper": "oidc-audience-resolve-mapper", - "consentRequired": false, - "config": {} - } - ], - "defaultClientScopes": [ - "web-origins", - "role_list", - "profile", - "roles", - "email" - ], - "optionalClientScopes": [ - "address", - "phone", - "offline_access", - "microprofile-jwt" - ] - }, - { - "id": "3e59f406-d6b9-45bd-b072-ef1650233625", - "clientId": "admin-cli", - "name": "${client_admin-cli}", - "surrogateAuthRequired": false, - "enabled": true, - "alwaysDisplayInConsole": false, - "clientAuthenticatorType": "client-secret", - "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": {}, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": false, - "nodeReRegistrationTimeout": 0, - "defaultClientScopes": [ - "web-origins", - "role_list", - "profile", - "roles", - "email" - ], - "optionalClientScopes": [ - "address", - "phone", - "offline_access", - "microprofile-jwt" - ] - }, - { - "id": "b607307d-dde7-4563-9e70-a7fa9223c229", - "clientId": "broker", - "name": "${client_broker}", - "surrogateAuthRequired": false, - "enabled": true, - "alwaysDisplayInConsole": false, - "clientAuthenticatorType": "client-secret", - "secret": "**********", - "redirectUris": [], - "webOrigins": [], - "notBefore": 0, - "bearerOnly": false, - "consentRequired": false, - "standardFlowEnabled": true, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": false, - "serviceAccountsEnabled": false, - "publicClient": false, - "frontchannelLogout": false, - "protocol": "openid-connect", - "attributes": {}, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": false, - "nodeReRegistrationTimeout": 0, - "defaultClientScopes": [ - "web-origins", - "role_list", - "profile", - "roles", - "email" - ], - "optionalClientScopes": [ - "address", - "phone", - "offline_access", - "microprofile-jwt" - ] - }, - { - "id": "63ae0908-5213-436d-b3d6-5a611eb24216", - "clientId": "realm-management", - "name": "${client_realm-management}", - "surrogateAuthRequired": false, - "enabled": true, - "alwaysDisplayInConsole": false, - "clientAuthenticatorType": "client-secret", - "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": {}, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": false, - "nodeReRegistrationTimeout": 0, - "defaultClientScopes": [ - "web-origins", - "role_list", - "profile", - "roles", - "email" - ], - "optionalClientScopes": [ - "address", - "phone", - "offline_access", - "microprofile-jwt" - ] - }, - { - "id": "26cb8208-4182-478b-903e-6abe9b555680", - "clientId": "security-admin-console", - "name": "${client_security-admin-console}", - "rootUrl": "${authAdminUrl}", - "baseUrl": "/admin/zuul-demo/console/", - "surrogateAuthRequired": false, - "enabled": true, - "alwaysDisplayInConsole": false, - "clientAuthenticatorType": "client-secret", - "secret": "**********", - "redirectUris": [ - "/admin/zuul-demo/console/*" - ], - "webOrigins": [ - "+" - ], - "notBefore": 0, - "bearerOnly": false, - "consentRequired": false, - "standardFlowEnabled": true, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": false, - "serviceAccountsEnabled": false, - "publicClient": true, - "frontchannelLogout": false, - "protocol": "openid-connect", - "attributes": { - "pkce.code.challenge.method": "S256" - }, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": false, - "nodeReRegistrationTimeout": 0, - "protocolMappers": [ - { - "id": "9077bfa3-32f2-4690-a1f4-fdb2726a6ef2", - "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", - "role_list", - "profile", - "roles", - "email" - ], - "optionalClientScopes": [ - "address", - "phone", - "offline_access", - "microprofile-jwt" - ] - }, - { - "id": "118b61cd-e410-414a-a4ee-64e5722725b7", - "clientId": "zuul", - "description": "zuul oauth client", - "rootUrl": "http://localhost:9000", - "adminUrl": "http://localhost:9000", - "surrogateAuthRequired": false, - "enabled": true, - "alwaysDisplayInConsole": false, - "clientAuthenticatorType": "client-secret", - "secret": "**********", - "redirectUris": [ - "http://127.0.0.1:9000/*", - "http://localhost:9000/*", - "http://127.0.0.1:3000/*", - "http://localhost:3000/*" - ], - "webOrigins": [ - "http://localhost:9000", - "http://localhost:3000" - ], - "notBefore": 0, - "bearerOnly": false, - "consentRequired": false, - "standardFlowEnabled": true, - "implicitFlowEnabled": true, - "directAccessGrantsEnabled": false, - "serviceAccountsEnabled": false, - "publicClient": true, - "frontchannelLogout": false, - "protocol": "openid-connect", - "attributes": { - "saml.assertion.signature": "false", - "saml.force.post.binding": "false", - "saml.multivalued.roles": "false", - "saml.encrypt": "false", - "saml.server.signature": "false", - "saml.server.signature.keyinfo.ext": "false", - "exclude.session.state.from.auth.response": "true", - "saml_force_name_id_format": "false", - "saml.client.signature": "false", - "tls.client.certificate.bound.access.tokens": "false", - "saml.authnstatement": "false", - "display.on.consent.screen": "false", - "saml.onetimeuse.condition": "false" - }, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": true, - "nodeReRegistrationTimeout": -1, - "protocolMappers": [ - { - "id": "2389430c-5b5e-4185-a116-a89fe9b2cbe0", - "name": "groups", - "protocol": "openid-connect", - "protocolMapper": "oidc-group-membership-mapper", - "consentRequired": false, - "config": { - "full.path": "false", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "groups", - "userinfo.token.claim": "true" - } - } - ], - "defaultClientScopes": [ - "web-origins", - "role_list", - "profile", - "roles", - "email", - "zuul_audience" - ], - "optionalClientScopes": [ - "address", - "phone", - "offline_access", - "microprofile-jwt" - ] - } - ], - "clientScopes": [ - { - "id": "a8ff8d15-7e18-46a7-afe9-cb4b51317f21", - "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": "2fb91b25-bf0f-4d30-8c32-8da4d6e2e14a", - "name": "address", - "protocol": "openid-connect", - "protocolMapper": "oidc-address-mapper", - "consentRequired": false, - "config": { - "user.attribute.formatted": "formatted", - "user.attribute.country": "country", - "user.attribute.postal_code": "postal_code", - "userinfo.token.claim": "true", - "user.attribute.street": "street", - "id.token.claim": "true", - "user.attribute.region": "region", - "access.token.claim": "true", - "user.attribute.locality": "locality" - } - } - ] - }, - { - "id": "2491f163-0678-4222-974b-42851cb8bbea", - "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": "247ada79-fd01-4e62-9231-46e09f2de990", - "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" + "clientRole" : false, + "containerId" : "zuul-demo", + "attributes" : { } + }, { + "id" : "a52bd4e0-3f93-4eaf-9792-b5a2b4f564b1", + "name" : "uma_authorization", + "description" : "${role_uma_authorization}", + "composite" : false, + "clientRole" : false, + "containerId" : "zuul-demo", + "attributes" : { } + } ], + "client" : { + "realm-management" : [ { + "id" : "76b84ff6-843c-4fbc-903e-56f82324726a", + "name" : "query-groups", + "description" : "${role_query-groups}", + "composite" : false, + "clientRole" : true, + "containerId" : "a9986aaf-a807-43db-b27e-bfcb4a6ed680", + "attributes" : { } + }, { + "id" : "e95f28d5-0a2c-42e5-ac27-ba8c1e49ca9e", + "name" : "manage-users", + "description" : "${role_manage-users}", + "composite" : false, + "clientRole" : true, + "containerId" : "a9986aaf-a807-43db-b27e-bfcb4a6ed680", + "attributes" : { } + }, { + "id" : "c57c009d-8cda-40a2-9f94-9faec21684d1", + "name" : "view-clients", + "description" : "${role_view-clients}", + "composite" : true, + "composites" : { + "client" : { + "realm-management" : [ "query-clients" ] } }, - { - "id": "6a89efdc-4a19-4059-943f-fbf4b0c80fbc", - "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": "77dc1544-6890-4714-acb5-bce0d34c15d5", - "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": "f1cc4b3b-6f0d-4564-b157-8e7790764643", - "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": "e9382111-9f81-455b-805a-9252c4c3db24", - "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": "7652d555-2c47-4312-85bb-b33e3f6d53ac", - "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": "e0d29ae8-b246-4d7c-bb0e-9b1f3b2fb95c", - "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": "b68bd653-6780-4cd7-a588-31073e2cc88b", - "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": "08a83a89-6029-4c5d-9fe1-3aaaed3a1579", - "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": "2890097c-f60f-4690-a964-005272790b26", - "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": "c7fa5831-9380-42d8-929a-a31e89b0dca5", - "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": "fcb6f43b-0dfd-42c4-8f96-2b25078be8cf", - "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": "15cc1330-fc07-4852-bd72-746ea7c70fc1", - "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": "59e8530c-0630-46e3-b1db-629bd8bafa78", - "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": "42a077a7-5a55-4838-b952-33f049fb5fc3", - "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": "bdc4888d-48ae-4d56-b1cd-d256c50d9b64", - "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": "97fb10c8-ff6e-4ccc-a25a-cfae87e783ef", - "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" + "clientRole" : true, + "containerId" : "a9986aaf-a807-43db-b27e-bfcb4a6ed680", + "attributes" : { } + }, { + "id" : "8eee1534-e312-4469-803e-ef89a2c4e02b", + "name" : "manage-identity-providers", + "description" : "${role_manage-identity-providers}", + "composite" : false, + "clientRole" : true, + "containerId" : "a9986aaf-a807-43db-b27e-bfcb4a6ed680", + "attributes" : { } + }, { + "id" : "f9b1241a-c8ba-4842-841c-7c9f6126d13e", + "name" : "realm-admin", + "description" : "${role_realm-admin}", + "composite" : true, + "composites" : { + "client" : { + "realm-management" : [ "query-groups", "view-clients", "manage-users", "view-identity-providers", "manage-identity-providers", "manage-clients", "query-realms", "manage-realm", "view-events", "create-client", "query-clients", "view-users", "view-realm", "manage-authorization", "impersonation", "query-users", "manage-events", "view-authorization" ] } }, - { - "id": "adb8eb09-943b-4e6f-a7ee-bc531a55e359", - "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" + "clientRole" : true, + "containerId" : "a9986aaf-a807-43db-b27e-bfcb4a6ed680", + "attributes" : { } + }, { + "id" : "12782ab5-4041-4c49-962a-08d0560fd5ed", + "name" : "view-identity-providers", + "description" : "${role_view-identity-providers}", + "composite" : false, + "clientRole" : true, + "containerId" : "a9986aaf-a807-43db-b27e-bfcb4a6ed680", + "attributes" : { } + }, { + "id" : "1d33026a-2968-4b14-931a-4d855bb82bbc", + "name" : "manage-clients", + "description" : "${role_manage-clients}", + "composite" : false, + "clientRole" : true, + "containerId" : "a9986aaf-a807-43db-b27e-bfcb4a6ed680", + "attributes" : { } + }, { + "id" : "74152341-86df-4741-ad3d-2a4cc4037137", + "name" : "manage-realm", + "description" : "${role_manage-realm}", + "composite" : false, + "clientRole" : true, + "containerId" : "a9986aaf-a807-43db-b27e-bfcb4a6ed680", + "attributes" : { } + }, { + "id" : "56ac5037-efe9-4b66-a8c9-41d6c573227a", + "name" : "query-realms", + "description" : "${role_query-realms}", + "composite" : false, + "clientRole" : true, + "containerId" : "a9986aaf-a807-43db-b27e-bfcb4a6ed680", + "attributes" : { } + }, { + "id" : "9a648218-df86-4203-bc91-29825d91f802", + "name" : "view-events", + "description" : "${role_view-events}", + "composite" : false, + "clientRole" : true, + "containerId" : "a9986aaf-a807-43db-b27e-bfcb4a6ed680", + "attributes" : { } + }, { + "id" : "e6b054ff-8f2b-43e2-8d4a-f7003b893203", + "name" : "create-client", + "description" : "${role_create-client}", + "composite" : false, + "clientRole" : true, + "containerId" : "a9986aaf-a807-43db-b27e-bfcb4a6ed680", + "attributes" : { } + }, { + "id" : "3201e1fc-57b4-4531-a142-adda7c9a132a", + "name" : "query-clients", + "description" : "${role_query-clients}", + "composite" : false, + "clientRole" : true, + "containerId" : "a9986aaf-a807-43db-b27e-bfcb4a6ed680", + "attributes" : { } + }, { + "id" : "e5e6855e-215a-4741-970c-269d201a7e9c", + "name" : "view-realm", + "description" : "${role_view-realm}", + "composite" : false, + "clientRole" : true, + "containerId" : "a9986aaf-a807-43db-b27e-bfcb4a6ed680", + "attributes" : { } + }, { + "id" : "c7599b5a-5374-4867-a6f1-5f4608da8196", + "name" : "view-users", + "description" : "${role_view-users}", + "composite" : true, + "composites" : { + "client" : { + "realm-management" : [ "query-groups", "query-users" ] } }, - { - "id": "c2260f1f-eb68-4186-b86c-74c23a450151", - "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" + "clientRole" : true, + "containerId" : "a9986aaf-a807-43db-b27e-bfcb4a6ed680", + "attributes" : { } + }, { + "id" : "36aecf8f-3265-41c1-b585-307005ee02af", + "name" : "manage-authorization", + "description" : "${role_manage-authorization}", + "composite" : false, + "clientRole" : true, + "containerId" : "a9986aaf-a807-43db-b27e-bfcb4a6ed680", + "attributes" : { } + }, { + "id" : "775ac110-9dc8-4dd5-93e7-45b61e9dd29a", + "name" : "impersonation", + "description" : "${role_impersonation}", + "composite" : false, + "clientRole" : true, + "containerId" : "a9986aaf-a807-43db-b27e-bfcb4a6ed680", + "attributes" : { } + }, { + "id" : "f00e65f4-2e53-4684-9eb4-b55c1c0ef4a3", + "name" : "query-users", + "description" : "${role_query-users}", + "composite" : false, + "clientRole" : true, + "containerId" : "a9986aaf-a807-43db-b27e-bfcb4a6ed680", + "attributes" : { } + }, { + "id" : "888e5d3f-1cb5-48f6-b3de-90acad8472f1", + "name" : "manage-events", + "description" : "${role_manage-events}", + "composite" : false, + "clientRole" : true, + "containerId" : "a9986aaf-a807-43db-b27e-bfcb4a6ed680", + "attributes" : { } + }, { + "id" : "125b0c77-45a3-4772-ab9d-cf45803cb3ef", + "name" : "view-authorization", + "description" : "${role_view-authorization}", + "composite" : false, + "clientRole" : true, + "containerId" : "a9986aaf-a807-43db-b27e-bfcb4a6ed680", + "attributes" : { } + } ], + "security-admin-console" : [ ], + "admin-cli" : [ ], + "account-console" : [ ], + "zuul" : [ ], + "broker" : [ { + "id" : "ec4aeaa2-8d80-47b3-a863-928f93b0157f", + "name" : "read-token", + "description" : "${role_read-token}", + "composite" : false, + "clientRole" : true, + "containerId" : "1f0c06e8-299d-4818-aada-4c59e326cbf8", + "attributes" : { } + } ], + "account" : [ { + "id" : "2c559af2-6553-42bd-a98b-de61ba35901a", + "name" : "view-applications", + "description" : "${role_view-applications}", + "composite" : false, + "clientRole" : true, + "containerId" : "38f83427-736e-4570-9f68-25a8b53f0b51", + "attributes" : { } + }, { + "id" : "976602d1-7350-4204-b8a1-00fd5349c7a6", + "name" : "view-consent", + "description" : "${role_view-consent}", + "composite" : false, + "clientRole" : true, + "containerId" : "38f83427-736e-4570-9f68-25a8b53f0b51", + "attributes" : { } + }, { + "id" : "edfc8de7-a5b8-4e33-8ca8-c577c40bb3e2", + "name" : "view-profile", + "description" : "${role_view-profile}", + "composite" : false, + "clientRole" : true, + "containerId" : "38f83427-736e-4570-9f68-25a8b53f0b51", + "attributes" : { } + }, { + "id" : "307ea046-f4ae-4cd4-b170-7f583682537b", + "name" : "delete-account", + "description" : "${role_delete-account}", + "composite" : false, + "clientRole" : true, + "containerId" : "38f83427-736e-4570-9f68-25a8b53f0b51", + "attributes" : { } + }, { + "id" : "0b943331-83af-4fac-b7c9-0ac6f4b73384", + "name" : "manage-account", + "description" : "${role_manage-account}", + "composite" : true, + "composites" : { + "client" : { + "account" : [ "manage-account-links" ] } }, - { - "id": "e618b191-1c20-466d-a412-9b59a221d587", - "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": "String" + "clientRole" : true, + "containerId" : "38f83427-736e-4570-9f68-25a8b53f0b51", + "attributes" : { } + }, { + "id" : "4b64f538-1336-44eb-ba18-c26859d7291a", + "name" : "manage-account-links", + "description" : "${role_manage-account-links}", + "composite" : false, + "clientRole" : true, + "containerId" : "38f83427-736e-4570-9f68-25a8b53f0b51", + "attributes" : { } + }, { + "id" : "12a120c6-1df6-47f6-b0ff-a3196cef8885", + "name" : "manage-consent", + "description" : "${role_manage-consent}", + "composite" : true, + "composites" : { + "client" : { + "account" : [ "view-consent" ] } }, - { - "id": "bf72e820-32b2-41ff-a812-7349dbc97dc1", - "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": "00ec085d-5cdc-4009-968a-bc03843e0418", - "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": "e7f8706d-04b2-4ceb-97e0-d6c50f268653", - "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": "dc881508-6029-4af0-9ab4-bc84613d0bfe", - "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": "a45a99e9-3413-4dbe-a1bf-da76d82c6887", - "name": "role_list", - "description": "SAML role list", - "protocol": "saml", - "attributes": { - "consent.screen.text": "${samlRoleListScopeConsentText}", - "display.on.consent.screen": "true" - }, - "protocolMappers": [ - { - "id": "8fdf3fb9-4eeb-4e14-986e-7c9711838ca5", - "name": "role list", - "protocol": "saml", - "protocolMapper": "saml-role-list-mapper", - "consentRequired": false, - "config": { - "single": "false", - "attribute.nameformat": "Basic", - "attribute.name": "Role" - } - } - ] - }, - { - "id": "cf8edb74-1db4-4cc0-89fa-ec2a9ef19565", - "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": "67629d5f-39aa-4521-b936-91964357e630", - "name": "audience resolve", - "protocol": "openid-connect", - "protocolMapper": "oidc-audience-resolve-mapper", - "consentRequired": false, - "config": {} - }, - { - "id": "75458c67-6f8c-4646-a851-c3a5f6c9c6e1", - "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": "059aa66a-7d91-4116-8b4e-51f5ab57424c", - "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": "2637f0c2-5d96-4057-a032-ce8f11477048", - "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": "33cbf353-644c-4801-91c1-b6ca677f65d2", - "name": "allowed web origins", - "protocol": "openid-connect", - "protocolMapper": "oidc-allowed-origins-mapper", - "consentRequired": false, - "config": {} - } - ] - }, - { - "id": "ace09ede-6cf1-4e36-9557-91beb58ce557", - "name": "zuul_audience", - "description": "fix audience claim for Zuul", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "true", - "display.on.consent.screen": "true" - }, - "protocolMappers": [ - { - "id": "9a1029e7-f142-4ff1-ba47-50ca6bb24073", - "name": "zuul_audience_mapper", - "protocol": "openid-connect", - "protocolMapper": "oidc-audience-mapper", - "consentRequired": false, - "config": { - "included.client.audience": "zuul", - "id.token.claim": "true", - "access.token.claim": "true", - "userinfo.token.claim": "true" - } - } - ] + "clientRole" : true, + "containerId" : "38f83427-736e-4570-9f68-25a8b53f0b51", + "attributes" : { } + } ] } - ], - "defaultDefaultClientScopes": [ - "email", - "web-origins", - "profile", - "role_list", - "roles" - ], - "defaultOptionalClientScopes": [ - "offline_access", - "microprofile-jwt", - "address", - "phone" - ], - "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": "909674c7-fb40-458b-af37-8372300725b1", - "name": "Max Clients Limit", - "providerId": "max-clients", - "subType": "anonymous", - "subComponents": {}, - "config": { - "max-clients": [ - "200" - ] - } - }, - { - "id": "b350d08d-4298-4d76-bd94-e120b1aadd28", - "name": "Allowed Client Scopes", - "providerId": "allowed-client-templates", - "subType": "anonymous", - "subComponents": {}, - "config": { - "allow-default-scopes": [ - "true" - ] - } - }, - { - "id": "4c1d186e-250d-497b-ac28-b7dffd021c19", - "name": "Allowed Protocol Mapper Types", - "providerId": "allowed-protocol-mappers", - "subType": "anonymous", - "subComponents": {}, - "config": { - "allowed-protocol-mapper-types": [ - "oidc-address-mapper", - "oidc-sha256-pairwise-sub-mapper", - "saml-user-attribute-mapper", - "oidc-full-name-mapper", - "oidc-usermodel-attribute-mapper", - "saml-user-property-mapper", - "oidc-usermodel-property-mapper", - "saml-role-list-mapper" - ] - } - }, - { - "id": "74a38dd7-41bf-414e-9858-f4f287c8caf3", - "name": "Full Scope Disabled", - "providerId": "scope", - "subType": "anonymous", - "subComponents": {}, - "config": {} - }, - { - "id": "19082549-290b-4e93-bd77-6ce5cc386b63", - "name": "Allowed Protocol Mapper Types", - "providerId": "allowed-protocol-mappers", - "subType": "authenticated", - "subComponents": {}, - "config": { - "allowed-protocol-mapper-types": [ - "saml-user-property-mapper", - "oidc-usermodel-attribute-mapper", - "oidc-address-mapper", - "oidc-usermodel-property-mapper", - "saml-role-list-mapper", - "saml-user-attribute-mapper", - "oidc-full-name-mapper", - "oidc-sha256-pairwise-sub-mapper" - ] - } - }, - { - "id": "62c50eea-fc15-40f1-a3c0-9923dcb8a8c7", - "name": "Trusted Hosts", - "providerId": "trusted-hosts", - "subType": "anonymous", - "subComponents": {}, - "config": { - "host-sending-registration-request-must-match": [ - "true" - ], - "client-uris-must-match": [ - "true" - ] - } - }, - { - "id": "21095085-2c9b-45ea-99d3-9ce962ab2020", - "name": "Allowed Client Scopes", - "providerId": "allowed-client-templates", - "subType": "authenticated", - "subComponents": {}, - "config": { - "allow-default-scopes": [ - "true" - ] - } - }, - { - "id": "36577457-a002-4b44-8029-9a599f2395a5", - "name": "Consent Required", - "providerId": "consent-required", - "subType": "anonymous", - "subComponents": {}, - "config": {} - } - ], - "org.keycloak.keys.KeyProvider": [ - { - "id": "0a6a397a-6eb1-46ab-8c6d-a3d4ea47f085", - "name": "rsa-generated", - "providerId": "rsa-generated", - "subComponents": {}, - "config": { - "privateKey": [ - "MIIEpQIBAAKCAQEA3q7PYMrX38XUpUScjbEn/4oyv5AI5TsyoFEPhgCFHzGEkeXWop3PNhh3OgFpz2Tn6qEqs9vRWi/8VDVC73lOZJWn70qfCDlr9Acmm1eKhrWkyHH6ONyg7qPj/PSsjapSsto5rVp1tdaOfObgWaLvg9dsOmbF+P6vUT6ggUD1p73KHySdDKiKHUTbaxaOVgDVTzeptDRg2iFaz+E0ETBsFKcJgXuPVd4IlxaXogDFLTKo6j2ITtnU8hw4wlqY3pkLI5OHP9EKmkV94fzmfYIoMxv4vLkwWRdz24hB459OCeDH0su25+JNnA0WA5SPRTUepeAsuZIFletYw2Mq/BFN5QIDAQABAoIBAQCX9e043VOxtjwlyAuZueJUxUdaaH1ZiStEMe0JAgPWRKF2OsVc1ZpZDRsXr204hWCqQe91K7XS+NoV0P7rkvmHNIWUi3S5VQ4xSkvzSCsVQJHB493gvdbo41iq/4Fdb3Td7oPbo9aeD1vPKnLBWKpgazrFI+tHvu1+4OEBM4YwP9KPXK4QOlxKFjzwGIeSgyMd3KnHbC6b9C8rlrf7b9Uj4UPVpGtNuPqTJyOeBTHh8CNJzYWYN0BPHk8DCMPaFkMXx42gSLsAAVOO1Kczby31Ac2IUIVDn/Z5kBmgxd6BWoVDSh1NpAlAYhK2A+LNUzveetE60CC80k387/N0JdlFAoGBAPr1v/e3rlD2pS3YA8UNQTcJjTJAvAuUw9kPXxYQd6G3UtuGwYNRw9DgZSA2E3eojzs6hLuS4W0mIVkmGwa/6SXD5TVbdaWbGO8C+x/Kxz0doRoM2AhA64uMy6zUNMwqIjroNuVn9vj8n5lMND4qqhMifYYuv9CQvyYOlXOSaFZTAoGBAOMnriWZQJqnLcd9zjr3DLxbrp/XCSLVvYRRVEleLkB+tahIFMNez50JHdTJb/TctDRfAFlZT2o0XI+FrHSqFI81UZes/cH2uqaIo+CgwRS4WY7f+Sp/ao6y+WZl2+Ip+Ao/QwjOvQ6u2jL+rCmZrPzoKoQHJaT+7WlHV2zZh9PnAoGBAOfquyCdakX/6N3YxoMPoLjP5uAN2rmJHQQ2pFSrmyKjW7rraWGF6kPZWxrNXmgyKUs+5PLC5fgMSL0t7cPrDfaMdgE9KBeGoSAfzRqwNjdQblS4kdvuwr4PuxlYcgJK3Z86gaC21xF5w7PTLGRW4R0VvpKGGVUQrtTonUxUfH9pAoGBALezd1pgvaZUXOlgDFATSvFpE6egN3s8b696Nje/Ophd4HrfECuPmUbeAIn8/dWARxuzWIzpdRfmkJRg/j667TWDYSDQfcdGyVu0VRNr7bnb/FFXQCHmOT597sOozFHyru1ai9OrnakqLrveyUw7Q3KkLv3m6cKth0IEt+cHZJkxAoGAHIGs8zI/Bp2U4Z4yHedyQ/tGF8Vk+ZehIsaHhOGEp3Cps4af5LBA5bstkArFsz+NJ7oDrxVBMUUNNypyUX4UF6EDcyZ+2vwBeill9fZqKR7BBBdC2AS6mT52gcCbFqBiGMHd+tYQj0tYnsann5e2a0ssqdXTYUn9YJO6iWh8EGY=" - ], - "certificate": [ - "MIICoTCCAYkCBgF210UjOTANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDDAl6dXVsLWRlbW8wHhcNMjEwMTA2MTAzNTAxWhcNMzEwMTA2MTAzNjQxWjAUMRIwEAYDVQQDDAl6dXVsLWRlbW8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDers9gytffxdSlRJyNsSf/ijK/kAjlOzKgUQ+GAIUfMYSR5dainc82GHc6AWnPZOfqoSqz29FaL/xUNULveU5klafvSp8IOWv0ByabV4qGtaTIcfo43KDuo+P89KyNqlKy2jmtWnW11o585uBZou+D12w6ZsX4/q9RPqCBQPWnvcofJJ0MqIodRNtrFo5WANVPN6m0NGDaIVrP4TQRMGwUpwmBe49V3giXFpeiAMUtMqjqPYhO2dTyHDjCWpjemQsjk4c/0QqaRX3h/OZ9gigzG/i8uTBZF3PbiEHjn04J4MfSy7bn4k2cDRYDlI9FNR6l4Cy5kgWV61jDYyr8EU3lAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAEFDFCBBNEtzOuAs46BRNBzGO2QCvLXsBASKSDFTsv2AMaFJIlc9l5U16wMcrZMTvsPY5MQxuaZvIDyAQH7NaxuRW7siZFBMRtwPmSrPHb/XqGh5OO4lXLX+Vpnx/JuMt9dOgyr9hTIYZ2UCFYxdC7ht21XSlRU6dZfEO8lTu+XEPQil42sZ/aB2efKfgWHieNvzTx3RLfG9lbsP4Dhg7FX+rGDz3Ql8PCl0Puai4pDwMjvvpZcZBjEzonw9pQ1rlSyGx0DLSJxMPMlQ33auVwLYD3MYIrgJplnScA0aKelOYJrnbSFMOlb/DwKxQNvM77banJQGq5Lpn617r2FmHNk=" - ], - "priority": [ - "100" - ] - } - }, - { - "id": "78880d4c-8ff2-4619-82b4-02a8fbd4a624", - "name": "hmac-generated", - "providerId": "hmac-generated", - "subComponents": {}, - "config": { - "kid": [ - "a23a2cdc-8d85-4754-b885-fc14a9e1a7bc" - ], - "secret": [ - "k5rhshjW9QaQvYnfC4nZUzQRkLZHKJlW524t125u63s9nza8ptFhPkLl7C3AGYSS6vD3tKSfOvkGuxubsRIJKg" - ], - "priority": [ - "100" - ], - "algorithm": [ - "HS256" - ] - } - }, - { - "id": "96281deb-af4d-49f8-a35c-10993007c7df", - "name": "aes-generated", - "providerId": "aes-generated", - "subComponents": {}, - "config": { - "kid": [ - "074f1951-9f97-40ff-8f0a-4219353d6f3d" - ], - "secret": [ - "Csz6nXZD0tf117pBKkJ7qw" - ], - "priority": [ - "100" - ] - } - } - ] + "groups" : [ { + "id" : "33a7bf20-adbe-47d9-b18c-77aa9ae9f045", + "name" : "example-tenant-admin", + "path" : "/example-tenant-admin", + "attributes" : { }, + "realmRoles" : [ ], + "clientRoles" : { }, + "subGroups" : [ ] + } ], + "defaultRole" : { + "id" : "156d7b5b-65fb-4c89-b3d8-c2f82e9ecdd4", + "name" : "default-roles-zuul-demo", + "description" : "${role_default-roles}", + "composite" : true, + "clientRole" : false, + "containerId" : "zuul-demo" }, - "internationalizationEnabled": false, - "supportedLocales": [], - "authenticationFlows": [ - { - "id": "3e95c1d5-2e16-40ad-89d3-cc13bd514fee", - "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", - "requirement": "ALTERNATIVE", - "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "requirement": "ALTERNATIVE", - "priority": 20, - "flowAlias": "Verify Existing Account by Re-authentication", - "userSetupAllowed": false, - "autheticatorFlow": true - } - ] - }, - { - "id": "7143f715-1076-4c59-a039-34c5ec30d7e8", - "alias": "Authentication Options", - "description": "Authentication options.", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "basic-auth", - "requirement": "REQUIRED", - "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticator": "basic-auth-otp", - "requirement": "DISABLED", - "priority": 20, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticator": "auth-spnego", - "requirement": "DISABLED", - "priority": 30, - "userSetupAllowed": false, - "autheticatorFlow": false - } - ] - }, - { - "id": "e3804922-4ffa-4e53-aa28-e982730f96a5", - "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", - "requirement": "REQUIRED", - "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticator": "auth-otp-form", - "requirement": "REQUIRED", - "priority": 20, - "userSetupAllowed": false, - "autheticatorFlow": false - } - ] - }, - { - "id": "e5ba2a92-70f1-4559-b31a-968363222c72", - "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", - "requirement": "REQUIRED", - "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticator": "direct-grant-validate-otp", - "requirement": "REQUIRED", - "priority": 20, - "userSetupAllowed": false, - "autheticatorFlow": false - } - ] - }, - { - "id": "617c8570-c7aa-4b4a-8b52-3bb94dc7ba04", - "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", - "requirement": "REQUIRED", - "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticator": "auth-otp-form", - "requirement": "REQUIRED", - "priority": 20, - "userSetupAllowed": false, - "autheticatorFlow": false - } - ] - }, - { - "id": "7bf3d19b-f551-4c1f-9188-850923a403b1", - "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", - "requirement": "REQUIRED", - "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "requirement": "REQUIRED", - "priority": 20, - "flowAlias": "Account verification options", - "userSetupAllowed": false, - "autheticatorFlow": true - } - ] - }, - { - "id": "ff1dd822-547c-4e25-8c9e-5b7a3dd30504", - "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", - "requirement": "REQUIRED", - "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticator": "reset-otp", - "requirement": "REQUIRED", - "priority": 20, - "userSetupAllowed": false, - "autheticatorFlow": false - } - ] - }, - { - "id": "3b586469-9250-4bbf-9af0-f3ad81ce2eaa", - "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", - "requirement": "ALTERNATIVE", - "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "requirement": "ALTERNATIVE", - "priority": 20, - "flowAlias": "Handle Existing Account", - "userSetupAllowed": false, - "autheticatorFlow": true - } - ] - }, - { - "id": "80831f25-c4ab-4ad8-ad6c-f1d7b570d919", - "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", - "requirement": "REQUIRED", - "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "requirement": "CONDITIONAL", - "priority": 20, - "flowAlias": "First broker login - Conditional OTP", - "userSetupAllowed": false, - "autheticatorFlow": true - } - ] - }, - { - "id": "dec78792-6cf8-4752-9833-18c1386423df", - "alias": "browser", - "description": "browser based authentication", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "auth-cookie", - "requirement": "ALTERNATIVE", - "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticator": "auth-spnego", - "requirement": "DISABLED", - "priority": 20, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticator": "identity-provider-redirector", - "requirement": "ALTERNATIVE", - "priority": 25, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "requirement": "ALTERNATIVE", - "priority": 30, - "flowAlias": "forms", - "userSetupAllowed": false, - "autheticatorFlow": true - } - ] - }, - { - "id": "14c06286-51e3-4abc-af50-8554c57c9f8f", - "alias": "clients", - "description": "Base authentication for clients", - "providerId": "client-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "client-secret", - "requirement": "ALTERNATIVE", - "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticator": "client-jwt", - "requirement": "ALTERNATIVE", - "priority": 20, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticator": "client-secret-jwt", - "requirement": "ALTERNATIVE", - "priority": 30, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticator": "client-x509", - "requirement": "ALTERNATIVE", - "priority": 40, - "userSetupAllowed": false, - "autheticatorFlow": false - } - ] + "requiredCredentials" : [ "password" ], + "otpPolicyType" : "totp", + "otpPolicyAlgorithm" : "HmacSHA1", + "otpPolicyInitialCounter" : 0, + "otpPolicyDigits" : 6, + "otpPolicyLookAheadWindow" : 1, + "otpPolicyPeriod" : 30, + "otpSupportedApplications" : [ "FreeOTP", "Google Authenticator" ], + "webAuthnPolicyRpEntityName" : "keycloak", + "webAuthnPolicySignatureAlgorithms" : [ "ES256" ], + "webAuthnPolicyRpId" : "", + "webAuthnPolicyAttestationConveyancePreference" : "not specified", + "webAuthnPolicyAuthenticatorAttachment" : "not specified", + "webAuthnPolicyRequireResidentKey" : "not specified", + "webAuthnPolicyUserVerificationRequirement" : "not specified", + "webAuthnPolicyCreateTimeout" : 0, + "webAuthnPolicyAvoidSameAuthenticatorRegister" : false, + "webAuthnPolicyAcceptableAaguids" : [ ], + "webAuthnPolicyPasswordlessRpEntityName" : "keycloak", + "webAuthnPolicyPasswordlessSignatureAlgorithms" : [ "ES256" ], + "webAuthnPolicyPasswordlessRpId" : "", + "webAuthnPolicyPasswordlessAttestationConveyancePreference" : "not specified", + "webAuthnPolicyPasswordlessAuthenticatorAttachment" : "not specified", + "webAuthnPolicyPasswordlessRequireResidentKey" : "not specified", + "webAuthnPolicyPasswordlessUserVerificationRequirement" : "not specified", + "webAuthnPolicyPasswordlessCreateTimeout" : 0, + "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister" : false, + "webAuthnPolicyPasswordlessAcceptableAaguids" : [ ], + "users" : [ { + "id" : "b19092bd-b767-4175-9d9a-e8749b15012b", + "createdTimestamp" : 1658485873780, + "username" : "admin", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "firstName" : "Zuul", + "lastName" : "Administrator", + "email" : "admin@example.com", + "credentials" : [ { + "id" : "77e16b56-2d90-43f9-8064-ec1330dc9d60", + "type" : "password", + "createdDate" : 1658485888588, + "secretData" : "{\"value\":\"3mxFAhY4UWLcnbooTrxgKZEH3dp5H78VlxVPU0KRFveg6YtIm+onPncXU8qx3PYDcfZjCbKuPghhFSJhaXxVnw==\",\"salt\":\"PomCTIfrYWl4leXhqAkUsQ==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "default-roles-zuul-demo" ], + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "2c3b0f44-0488-42fe-acac-2d5e483b9806", + "createdTimestamp" : 1658485982882, + "username" : "user1", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "firstName" : "User", + "lastName" : "One", + "email" : "user1@example.com", + "credentials" : [ { + "id" : "52b67f84-8383-4684-8d45-ab9838ebda07", + "type" : "password", + "createdDate" : 1658485994760, + "secretData" : "{\"value\":\"RII4dJdPibGBeya28QKmZaZkumVXj0GltwHlUdv7w7IR7X+1JtlPke2Z7UfZYeb09CkHSj4xk3EaCMaQl4uUoA==\",\"salt\":\"C68vc47R5kfemCeGAbOKIQ==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "default-roles-zuul-demo" ], + "notBefore" : 0, + "groups" : [ "/example-tenant-admin" ] + }, { + "id" : "832d027d-5ef8-4968-b950-e57944dde6d6", + "createdTimestamp" : 1658486020309, + "username" : "user2", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "firstName" : "User", + "lastName" : "Two", + "email" : "user2@example.com", + "credentials" : [ { + "id" : "304178ea-219a-4495-bca1-76361e841ce0", + "type" : "password", + "createdDate" : 1658486030056, + "secretData" : "{\"value\":\"uDqEwwwGuxSMttycSI+YMRkRxHGozlWxGJ4h2YZ21CXNSpx3BN/i4xAJ47nhhZzDoZKYPVqiTpWXFgVpaYb2jA==\",\"salt\":\"ADhTsq7HIlBEZ0KmurA9/Q==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "default-roles-zuul-demo" ], + "notBefore" : 0, + "groups" : [ ] + } ], + "scopeMappings" : [ { + "clientScope" : "offline_access", + "roles" : [ "offline_access" ] + } ], + "clientScopeMappings" : { + "account" : [ { + "client" : "account-console", + "roles" : [ "manage-account" ] + } ] + }, + "clients" : [ { + "id" : "38f83427-736e-4570-9f68-25a8b53f0b51", + "clientId" : "account", + "name" : "${client_account}", + "rootUrl" : "${authBaseUrl}", + "baseUrl" : "/realms/zuul-demo/account/", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ "/realms/zuul-demo/account/*" ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "a124c422-d35d-4ad9-bb85-4df87ae56fb3", + "clientId" : "account-console", + "name" : "${client_account-console}", + "rootUrl" : "${authBaseUrl}", + "baseUrl" : "/realms/zuul-demo/account/", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ "/realms/zuul-demo/account/*" ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "pkce.code.challenge.method" : "S256" }, - { - "id": "54d29594-0183-486f-abb2-c1998d4e45c5", - "alias": "direct grant", - "description": "OpenID Connect Resource Owner Grant", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "direct-grant-validate-username", - "requirement": "REQUIRED", - "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticator": "direct-grant-validate-password", - "requirement": "REQUIRED", - "priority": 20, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "requirement": "CONDITIONAL", - "priority": 30, - "flowAlias": "Direct Grant - Conditional OTP", - "userSetupAllowed": false, - "autheticatorFlow": true - } - ] + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "protocolMappers" : [ { + "id" : "849fdec6-5827-4e3e-b9bb-25fe91a8c3af", + "name" : "audience resolve", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-audience-resolve-mapper", + "consentRequired" : false, + "config" : { } + } ], + "defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "24514749-6257-4d58-bc00-e68592d4f8ee", + "clientId" : "admin-cli", + "name" : "${client_admin-cli}", + "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" : { }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "1f0c06e8-299d-4818-aada-4c59e326cbf8", + "clientId" : "broker", + "name" : "${client_broker}", + "surrogateAuthRequired" : false, + "enabled" : true, + "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" : { }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "a9986aaf-a807-43db-b27e-bfcb4a6ed680", + "clientId" : "realm-management", + "name" : "${client_realm-management}", + "surrogateAuthRequired" : false, + "enabled" : true, + "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" : { }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "952f56db-5191-41fd-ac39-572ba28d9ee5", + "clientId" : "security-admin-console", + "name" : "${client_security-admin-console}", + "rootUrl" : "${authAdminUrl}", + "baseUrl" : "/admin/zuul-demo/console/", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ "/admin/zuul-demo/console/*" ], + "webOrigins" : [ "+" ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "pkce.code.challenge.method" : "S256" }, - { - "id": "e6734c68-8029-4b27-bf56-7eccb2202903", - "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", - "requirement": "REQUIRED", - "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false - } - ] + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "protocolMappers" : [ { + "id" : "661a797e-0734-46b8-8c4b-17b1a58cf05a", + "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", "roles", "profile", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "99377c87-2fb8-4d8b-a628-6281350e7e9b", + "clientId" : "zuul", + "rootUrl" : "http://localhost:9000", + "adminUrl" : "http://localhost:9000", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ "http://localhost:3000/*", "http://localhost:9000/*" ], + "webOrigins" : [ "*" ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : true, + "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", + "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" }, - { - "id": "0e26f9c0-bc34-4d38-b3fe-472580bff946", - "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", - "requirement": "REQUIRED", - "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "requirement": "REQUIRED", - "priority": 20, - "flowAlias": "User creation or linking", - "userSetupAllowed": false, - "autheticatorFlow": true - } - ] + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : true, + "nodeReRegistrationTimeout" : -1, + "protocolMappers" : [ { + "id" : "ab67e3ef-562f-4cfa-96ad-02d9e29ed483", + "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" + } + } ], + "defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "zuul_aud", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + } ], + "clientScopes" : [ { + "id" : "f9da357f-356a-45a7-8ad8-b0e47413c30e", + "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" : "" }, - { - "id": "6f56d749-5f06-4e2a-a043-cd4103383899", - "alias": "forms", - "description": "Username, password, otp and other auth forms.", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "auth-username-password-form", - "requirement": "REQUIRED", - "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "requirement": "CONDITIONAL", - "priority": 20, - "flowAlias": "Browser - Conditional OTP", - "userSetupAllowed": false, - "autheticatorFlow": true - } - ] + "protocolMappers" : [ { + "id" : "2f31c90f-8237-4a54-b852-a2df857c6fc8", + "name" : "allowed web origins", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-allowed-origins-mapper", + "consentRequired" : false, + "config" : { } + } ] + }, { + "id" : "554bde36-edad-4c3b-a2f8-1461ad4181c8", + "name" : "role_list", + "description" : "SAML role list", + "protocol" : "saml", + "attributes" : { + "consent.screen.text" : "${samlRoleListScopeConsentText}", + "display.on.consent.screen" : "true" }, - { - "id": "4a95b320-312e-468a-978e-215747302385", - "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", - "requirement": "REQUIRED", - "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "requirement": "REQUIRED", - "priority": 20, - "flowAlias": "Authentication Options", - "userSetupAllowed": false, - "autheticatorFlow": true - } - ] + "protocolMappers" : [ { + "id" : "ab8dfb24-9c7e-4018-bafe-ef3fc2dd10c6", + "name" : "role list", + "protocol" : "saml", + "protocolMapper" : "saml-role-list-mapper", + "consentRequired" : false, + "config" : { + "single" : "false", + "attribute.nameformat" : "Basic", + "attribute.name" : "Role" + } + } ] + }, { + "id" : "c68d1141-1188-4adb-9c88-033ea1bff03a", + "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}" }, - { - "id": "17678e79-9819-47f8-bbfe-3b68efb92f3d", - "alias": "registration", - "description": "registration flow", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "registration-page-form", - "requirement": "REQUIRED", - "priority": 10, - "flowAlias": "registration form", - "userSetupAllowed": false, - "autheticatorFlow": true - } - ] + "protocolMappers" : [ { + "id" : "e97f7b55-12d6-40cb-9e60-b0cee354ae8f", + "name" : "address", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-address-mapper", + "consentRequired" : false, + "config" : { + "user.attribute.formatted" : "formatted", + "user.attribute.country" : "country", + "user.attribute.postal_code" : "postal_code", + "userinfo.token.claim" : "true", + "user.attribute.street" : "street", + "id.token.claim" : "true", + "user.attribute.region" : "region", + "access.token.claim" : "true", + "user.attribute.locality" : "locality" + } + } ] + }, { + "id" : "18ceed25-cdb0-4911-8419-b8228d5947a2", + "name" : "zuul_aud", + "description" : "zuul audience mapper", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "true" }, - { - "id": "7c23bfaa-420a-4975-a547-254bb0873457", - "alias": "registration form", - "description": "registration form", - "providerId": "form-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "registration-user-creation", - "requirement": "REQUIRED", - "priority": 20, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticator": "registration-profile-action", - "requirement": "REQUIRED", - "priority": 40, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticator": "registration-password-action", - "requirement": "REQUIRED", - "priority": 50, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticator": "registration-recaptcha-action", - "requirement": "DISABLED", - "priority": 60, - "userSetupAllowed": false, - "autheticatorFlow": false - } - ] + "protocolMappers" : [ { + "id" : "e54ce7f5-fb11-425f-ad3a-628db83c1ef2", + "name" : "zuul_aud_mapper", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-audience-mapper", + "consentRequired" : false, + "config" : { + "included.client.audience" : "zuul", + "id.token.claim" : "true", + "access.token.claim" : "true", + "userinfo.token.claim" : "true" + } + } ] + }, { + "id" : "1f3ab062-a2bd-4929-9a49-434fa94b7f55", + "name" : "microprofile-jwt", + "description" : "Microprofile - JWT built-in scope", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "false" }, - { - "id": "ef21d1d8-4afd-4baf-a44d-34d657f673a4", - "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", - "requirement": "REQUIRED", - "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticator": "reset-credential-email", - "requirement": "REQUIRED", - "priority": 20, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticator": "reset-password", - "requirement": "REQUIRED", - "priority": 30, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "requirement": "CONDITIONAL", - "priority": 40, - "flowAlias": "Reset - Conditional OTP", - "userSetupAllowed": false, - "autheticatorFlow": true - } - ] + "protocolMappers" : [ { + "id" : "80212ba7-8894-4b23-b73c-b29fb6157c41", + "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" : "1abd1cea-5b14-41ef-b865-bb81af679932", + "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" : "220b27ee-6e55-49c0-86a7-e2f915166bc7", + "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}" }, - { - "id": "21fea2f5-c4e3-46af-a573-54f36aa25b4b", - "alias": "saml ecp", - "description": "SAML ECP Profile Authentication Flow", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "http-basic-authenticator", - "requirement": "REQUIRED", - "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false - } - ] - } - ], - "authenticatorConfig": [ - { - "id": "d093ee16-2996-4916-ba26-827877401a45", - "alias": "create unique user config", - "config": { - "require.password.update.after.registration": "false" + "protocolMappers" : [ { + "id" : "82f49e59-01d9-4435-9d57-8b51ba068f95", + "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" : "4b7d2541-a139-4f35-8b0a-7f786abf8e25", + "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" : "f862a715-e18d-491f-ad37-0c77253e1519", + "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}" }, - { - "id": "9cf393f2-4659-473a-a1d8-2318e107fcfe", - "alias": "review profile config", - "config": { - "update.profile.on.first.login": "missing" + "protocolMappers" : [ { + "id" : "195a1039-aeca-4e57-888b-1a1e802fa64d", + "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" : "4ccb27c0-2574-48b7-92bd-ec246306a944", + "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" : "a03e0161-18f1-4d82-8482-4c6b1b6803bf", + "name" : "offline_access", + "description" : "OpenID Connect built-in scope: offline_access", + "protocol" : "openid-connect", + "attributes" : { + "consent.screen.text" : "${offlineAccessScopeConsentText}", + "display.on.consent.screen" : "true" } - ], - "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": {} + }, { + "id" : "3bc63b52-838d-48bc-b0b6-b0805901c4b0", + "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}" }, - { - "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": {} + "protocolMappers" : [ { + "id" : "f654cdcf-ad35-483d-bc9a-36e27cf2d459", + "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" : "f6b687b6-94d6-4cfd-b167-eac16d07d1ff", + "name" : "audience resolve", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-audience-resolve-mapper", + "consentRequired" : false, + "config" : { } + }, { + "id" : "f59d4345-6702-43ca-82d9-dc2d60c9b66e", + "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" : "6cdcdd5e-8a80-4366-b38f-49005db03f27", + "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" }, - { - "alias": "delete_account", - "name": "Delete Account", - "providerId": "delete_account", - "enabled": false, - "defaultAction": false, - "priority": 60, - "config": {} + "protocolMappers" : [ { + "id" : "941b04f7-0f19-49b5-a11b-0790dcf83e3a", + "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" : "197b457a-28ff-48c2-a879-be4eceeeaed6", + "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}" }, - { - "alias": "update_user_locale", - "name": "Update User Locale", - "providerId": "update_user_locale", - "enabled": true, - "defaultAction": false, - "priority": 1000, - "config": {} + "protocolMappers" : [ { + "id" : "8247fb9a-2a89-4491-ae97-9a3ef846a32d", + "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" : "7b71b659-4929-454d-a2f1-9e84c3ebd970", + "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" : "58bbcd2b-77a8-4a98-b51e-b9802b0d300c", + "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" : "2bdf1469-3ce1-408d-a6db-616a687efd20", + "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" : "ee34f510-aa37-47f0-81b6-eee4d0bd2073", + "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" : "a7e5ad5f-afe7-4e10-833a-db4ca4502083", + "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" : "6731543a-4fd5-4064-8cd6-438d59f07d98", + "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" : "dfbc8ef9-be5e-476b-a4cb-8a4130b00ad7", + "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" : "3c2b96fc-fcff-4847-b7a5-6c915adad250", + "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" : "6487b361-8548-4131-9636-f5e6c1db962d", + "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" : "4e93de0c-7f96-499e-8014-fc260fd82960", + "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" : "7e202343-ae40-4149-985b-3f4cbf9b1588", + "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" : "e9175df7-260b-4fe7-8655-db30ccc9c424", + "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" : "59663d46-72dd-4ea6-b106-7a264d01ebcb", + "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" + } + } ] + } ], + "defaultDefaultClientScopes" : [ "profile", "roles", "role_list", "acr", "email", "web-origins" ], + "defaultOptionalClientScopes" : [ "microprofile-jwt", "phone", "offline_access", "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" : "c6df0a44-af38-4795-8990-ffc359a2b62b", + "name" : "Consent Required", + "providerId" : "consent-required", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { } + }, { + "id" : "fa50952d-682b-4bf3-936c-2e35abfd810a", + "name" : "Allowed Client Scopes", + "providerId" : "allowed-client-templates", + "subType" : "authenticated", + "subComponents" : { }, + "config" : { + "allow-default-scopes" : [ "true" ] + } + }, { + "id" : "adb59412-5ad2-4531-bcde-cdd3048e8698", + "name" : "Max Clients Limit", + "providerId" : "max-clients", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "max-clients" : [ "200" ] + } + }, { + "id" : "270f6a69-5b86-4a4b-b54c-1900ea3d325d", + "name" : "Allowed Protocol Mapper Types", + "providerId" : "allowed-protocol-mappers", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "allowed-protocol-mapper-types" : [ "oidc-sha256-pairwise-sub-mapper", "oidc-full-name-mapper", "saml-user-attribute-mapper", "oidc-usermodel-property-mapper", "saml-role-list-mapper", "oidc-address-mapper", "saml-user-property-mapper", "oidc-usermodel-attribute-mapper" ] + } + }, { + "id" : "a107bb7f-7b95-4c80-bf36-0793913507b9", + "name" : "Trusted Hosts", + "providerId" : "trusted-hosts", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "host-sending-registration-request-must-match" : [ "true" ], + "client-uris-must-match" : [ "true" ] + } + }, { + "id" : "5e3c37e6-455a-4cab-9058-59d7ba1e6a91", + "name" : "Full Scope Disabled", + "providerId" : "scope", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { } + }, { + "id" : "829ff854-352f-453e-ae71-b3ed893167e5", + "name" : "Allowed Protocol Mapper Types", + "providerId" : "allowed-protocol-mappers", + "subType" : "authenticated", + "subComponents" : { }, + "config" : { + "allowed-protocol-mapper-types" : [ "saml-role-list-mapper", "saml-user-property-mapper", "oidc-full-name-mapper", "oidc-address-mapper", "oidc-usermodel-property-mapper", "oidc-usermodel-attribute-mapper", "saml-user-attribute-mapper", "oidc-sha256-pairwise-sub-mapper" ] + } + }, { + "id" : "eb1f66db-d2a2-435f-958d-f385db20740e", + "name" : "Allowed Client Scopes", + "providerId" : "allowed-client-templates", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "allow-default-scopes" : [ "true" ] + } + } ], + "org.keycloak.keys.KeyProvider" : [ { + "id" : "34336282-9f58-471a-9794-440b180296aa", + "name" : "rsa-generated", + "providerId" : "rsa-generated", + "subComponents" : { }, + "config" : { + "privateKey" : [ "MIIEowIBAAKCAQEA0fI5OZ/LCxI/TNxVpWi9q6EeaOv/yshrPOFevhg5eJ52G2uzvv7XWN4FNWMg3ln97qmZPm6PvB6utCSM776WZ5O3t1zy6KXc5LKRSF9WW/RdoLlrIAvo9R2XRpdClfc4906Dl8q8NlxC6klVkP5OwTzuH9fh36qf2BAb39F6AhosY8lfWiE7+QpF7CEvYkZTdp02PtqbV2R7DMjzhPs3aLLbJybu9BVfsdNbHdsk6owauJMtvJhIvcigk5azUk6leVp27010zv+DuWVtbMhzX+qnYy4+SnkyfKPPX1lYtQzw6pDTJuWTjcpSKgZ/68xKyVQWzNqsrSZyGWXEtmsTvwIDAQABAoIBABZMpRliyJ0/bevLB2nPSz4cme9JCd1PFlzcD0Ufq5MNObxF30ytItB0nJcEFWyooFkmOHmyIqA3nhTSQQoB81XDEeNvAvoFDAZalSXbwn934C3/I8q+mRrX7sZ3sIHfRXS3KTEJPp6DhDQAYA9Lg6/wudY2Bu7L37iCVYOba1snnUFaqSGzBqkx68SisYUpjj9LpBHwgoAO4UwTzijN6UUS0MwpxJ1Y10A3c5v3XfzAYTm9FhzT5TyrO6NgXCQIbfC5tbTdSPXbsrYiNucmqlqB/Ym/f1KSM8peBQSFt8h0yjjeVRYe2/oVfiWeDeBHdYZCCpY20RJtSKjScOcwFNkCgYEA8G3xS8DfTZonCx4mR/SU+9Yjx8KU+g/erRHhwUHATY0zV+usz/ZTBz7wfVSBLQ7MDzpbpjOVF0CjFeNgI3kS4ju0smEylwZ8ghlDa578ITArV5Wb9uECdu86ZzNzugeoGx5pZYEkqEvO1MfS3tecX8h02eCwvUCZ26WJTDu1ev0CgYEA34rmzlXqDyrBENvwKNvfL/NfTpQ6lLfAAirRov8UvKjtcRo131PDIfGzsFDMrN3640rKyY1HOiW2SDYK0NrLOmPxzitcXxVjBtqcI879qAQeMWYLdFA3R7kJE1hn8aNoC+hzjpUYjaoBXZFqQrw+HvEl0UTRErhn2FCpnnuhHGsCgYB4zqVp7PXHJmfr3DBcA3dAuy6rVwhIReUbjVqkIoef8A0WZmMk855wi52HPAjUPRUnqakXb9/AGUhY3TEXYeStLFb1MoWbATVaoZsQ3ofNnUiJ77C4cgMtA7OS23WQyfwb9EH9MNDch4rbu7QnKKt4LV8Zgx8LWQR2K007stzTeQKBgEv7ZqBTy48ENUOJvoPk2GPwEUEgWRkbrAQQkdkZYu8NbEe5ZP5X9kefZDhV/nvzp8eCMJN5lfBJyT+aPurms5VRS2uMiYU4O8SkodX6EXX+kFSyHsqWSXPzjQYqUoprxH3nsP+R5hqg4n3vY3LzkRnrVbNf9MCsNX/tSvo3g3epAoGBAM50hFFzEo6V+Tk+grZfGI47vTmQ0w+oTtImHsu6kD8fMbfP/+eq28n5BnI7yBK0kmP5Z2pOByHQfc0Dn90O7J1kwaxi4URwNUIFmWfofL11fRFtRHALH/U/LiFprkDwnmjp8qt8MutlyElcpyuDCKFWidq8lx4CfqnINzFfdWh7" ], + "keyUse" : [ "SIG" ], + "certificate" : [ "MIICoTCCAYkCBgGCJXQ9oDANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDDAl6dXVsLWRlbW8wHhcNMjIwNzIyMTAyNzMxWhcNMzIwNzIyMTAyOTExWjAUMRIwEAYDVQQDDAl6dXVsLWRlbW8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDR8jk5n8sLEj9M3FWlaL2roR5o6//KyGs84V6+GDl4nnYba7O+/tdY3gU1YyDeWf3uqZk+bo+8Hq60JIzvvpZnk7e3XPLopdzkspFIX1Zb9F2guWsgC+j1HZdGl0KV9zj3ToOXyrw2XELqSVWQ/k7BPO4f1+Hfqp/YEBvf0XoCGixjyV9aITv5CkXsIS9iRlN2nTY+2ptXZHsMyPOE+zdostsnJu70FV+x01sd2yTqjBq4ky28mEi9yKCTlrNSTqV5WnbvTXTO/4O5ZW1syHNf6qdjLj5KeTJ8o89fWVi1DPDqkNMm5ZONylIqBn/rzErJVBbM2qytJnIZZcS2axO/AgMBAAEwDQYJKoZIhvcNAQELBQADggEBAKYr1dxc8eMlgaZWXvd4vbP1ugbqUswt3kgBbrrFmzfzocU1/Hd0f5YW5ziq1oBqB2Ke6ByTt5h7M4q5ss+w6Gb3bLyNV9wmP1SQO0JBk0TvRo7CId68SZwAPNH4Bmo5z9efhdvvDevnM79bKw65D/xu3t4YPUAOHfd+idVQkk38DWdmKSFz/4VZ2luNrQFBQS0KW39K30BaR/65xGRlefNKVqYBFYDb4u9QUfiD6Q4jckXPCMEKgOD4oGlUmHnmTufoRzH9W0jqOy5856gHgXD7+lJtvRjj+alHJ4on7ABk+yPSgEuI3WCVSITGd0KW5gaB8YjyhG7NAV4rKXpPv20=" ], + "priority" : [ "100" ] + } + }, { + "id" : "558fe888-18c5-48e2-9c45-a072532315f7", + "name" : "hmac-generated", + "providerId" : "hmac-generated", + "subComponents" : { }, + "config" : { + "kid" : [ "bed016a0-d455-4ef2-827a-e5deb8569f3e" ], + "secret" : [ "0Aph42iALP396xPal2boz2dTNi9j8gculw6H_g6ohYO1vdJdwjBUsH-GD-GCBQk49Rtxa1yNZDWrr1f-2inGfw" ], + "priority" : [ "100" ], + "algorithm" : [ "HS256" ] + } + }, { + "id" : "67d8a3df-b709-4726-a929-e592530f7889", + "name" : "rsa-enc-generated", + "providerId" : "rsa-enc-generated", + "subComponents" : { }, + "config" : { + "privateKey" : [ "MIIEpAIBAAKCAQEAjXU8U4JzDj2z5+L/kAuBZGayBnqwbsRaafkQcDf2OztpEaqPZt9KbQtK7x9eev4VkXepRl6FhNgfqTfwlLWd9CQZl5dXgKXC/9kZo4WhNiKfRpFdfcFZTULKJWROgPddx8pz3tw9yU4wZDrAA5wGo6qC9v7QMu4A/ARTpGjl5gRnEFDswM9OuHGqd7oAxCskn0XPVbOVcAbs2KPB7jNopyuGsUpox3uxv79sAjEMY64Y/Da9lkBZBpCkh8v3hZtGAQTvGf4VZLx8e00U1BrKomuJuBMvaCBrnyFHGGpVxF/f4oFBcVVceJOxIamuW7ZEpWP3uLMqZhs5N02jKEHsMwIDAQABAoIBADxYhRHDFr4+iF8mMYwLvHlObifhcx7t2PxYajDVTs/Z+mq07lQecauvI5p7MV0aw6y49Ze1fxQ69tbDrHhkOiwKsng+8e9Jx7RJs2xU938Wqgf6v32EXEpfcckdz3epD3LJ5UBviFQ2bOJ1RbDEgPiVJyWjcsInUngwiFYdAeZr2oWbdTovPI5bvJFi2uAjbqgMXz4FyVrQshZFpSHNVEeuRowjfmnMVjRD2KFNG4rvF7Cs6yp4/NZ0z4FtyRp+Nw0n7tb1uri2/RSPeSt0pXKRbVtiIVd6NwX4e403SSK2orF01WWSVBbw7BnDZBgwtiY38QkYy9PJp1KlwDGC8bECgYEA2gW//RAIA6+vWktD2ru4LYPZ0237W5C8nWRrGZG+jqlSQIxW5t14NTCte/Mlz1Ixyolj0n4Vqe3p3PYp5kgrpQ/2WcS4St3LQgr+apLO8LNLQpn2/CfbAYRq5MfOyXvC0fTyC39t0Zx/rz4CPbl9NEjRLYGW0+G+EkDeSD3VGM8CgYEAphlA5mi2PJttOL2nWovOhh6ZUM0spg+eSU9qDcQtmBwdhnApvv68viIDvJ/1Yhjg1xEndBUkxm6wppWrIKOJzWlrpqYxgMaesVBPIBeGjhs8CpEadCzFjBeHzLeckf9Tg5uOq/zud8i2O/2gdsW+O/mgXebPFaDKarNJ/RFBx10CgYAbqjceaXZa4WQi1QIbM06/l8eAKKiIieklH6TnbbL8b0fcoPI2AUxLk42v+L6HfCqNRLnB7/XXxjSysRnH+shu/YOJCIlKFrLAAp0d8NsJd/bI0Liibnw/yQm3VhNLRcUG10oPY3AP9QfOuNOX+X2FsOe6GoOFB1QTBOAv0ojDyQKBgQChViJOLCPnlululfl64BYI/KvbapAV9VHAz9yp5oaLG7QqI8TI+tMGtC+BBrbpRFURUl5KSUdhX59DEFoa5/8wgVfJleC7vzHh9aoF+BQA8L9PXEaf1eVFAI4t8aOpxKKwUWcx9AsGWvV4LBGzjLwNsZlt3IYF8hrrrZ8Oq7aDGQKBgQDMDce7sMLGSI2rZcPmn15DqBxKr9fmtHH1/DGGvXlYNcI2UvzI5Lq7bOK4uu87NnHoKhSjd9EsENmFc0iYxf1F9r+3BGtYYLOWClLBhM7GXiQeGFKFGpXso7I2HVZUMpUDJaTmlmF3xlAb+VchOHvfCyFvTO4xMm7wnemDQ/ZJ+g==" ], + "keySize" : [ "2048" ], + "keyUse" : [ "ENC" ], + "certificate" : [ "MIICoTCCAYkCBgGCJXQ99zANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDDAl6dXVsLWRlbW8wHhcNMjIwNzIyMTAyNzMxWhcNMzIwNzIyMTAyOTExWjAUMRIwEAYDVQQDDAl6dXVsLWRlbW8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCNdTxTgnMOPbPn4v+QC4FkZrIGerBuxFpp+RBwN/Y7O2kRqo9m30ptC0rvH156/hWRd6lGXoWE2B+pN/CUtZ30JBmXl1eApcL/2RmjhaE2Ip9GkV19wVlNQsolZE6A913HynPe3D3JTjBkOsADnAajqoL2/tAy7gD8BFOkaOXmBGcQUOzAz064cap3ugDEKySfRc9Vs5VwBuzYo8HuM2inK4axSmjHe7G/v2wCMQxjrhj8Nr2WQFkGkKSHy/eFm0YBBO8Z/hVkvHx7TRTUGsqia4m4Ey9oIGufIUcYalXEX9/igUFxVVx4k7Ehqa5btkSlY/e4sypmGzk3TaMoQewzAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAFIjuy2Aac1Y5jggKdRwZ8Xlg8S4B7RxmvNg15G8DX1m1PaAcb6aZ6lwDp78aEMxlpihn/G5mJ2MEmFL9Y95lJlCXlB0pHSGf4+vKJ6ZgZ9IUdP6tKyLsnCY+wR2Ag4444Upwd4yJGj3QBAArrdIezTOpEoPUn51j8KDJUXJXRkeJFE4Kgl5S3TmCGwvpCJQYfLjj9Lw60Evepi9fjaTjQxp16aDNHDvB/d+7VnJoZnj2ruU/N93FUaUgv4W2AAjcJj+9le8MGTIWLXezhCOgdDYRBMxTiBAwV+BbktXdQZiDvYyJp8tCxp0w9K3WNRgEX+2jS94+yYWMxKVVlHmtWA=" ], + "active" : [ "false" ], + "priority" : [ "100" ], + "enabled" : [ "false" ], + "algorithm" : [ "RSA-OAEP" ] + } + }, { + "id" : "18071daa-3312-45d7-9687-80f4b713b351", + "name" : "aes-generated", + "providerId" : "aes-generated", + "subComponents" : { }, + "config" : { + "kid" : [ "ebdbe2a3-cac0-4ceb-a691-349f9a857a4a" ], + "secret" : [ "G77Dlvi7V6exa6LZZjv0Aw" ], + "priority" : [ "100" ] + } + } ] + }, + "internationalizationEnabled" : false, + "supportedLocales" : [ ], + "authenticationFlows" : [ { + "id" : "905f0ff4-552b-41cd-a293-54615e1cca5e", + "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" : "56aefed5-e44b-418b-9acf-5c8ad5a99b38", + "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" : "ee2217ce-1a43-45da-9e42-53e967301b68", + "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" : "a2675999-6a95-4eda-b46b-b6c23f897bb7", + "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" : "7088d6ca-06a0-48bb-a2bb-dc0ecd21bae2", + "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" : "d5a6c5aa-aa28-4d8a-b5a1-2d0df85a9979", + "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" : "940b59eb-3977-4354-8ad0-a60a513a91ca", + "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" : "653228d9-a3a7-41b1-b6c1-eaea8b2f4e87", + "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" : "9c6a2dff-b711-4bbd-8606-d8939ba4d9ca", + "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" : "5c564dc9-1ea5-4977-ad52-eb6a1213b88e", + "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" : "28f28120-2107-4cd5-a2bf-a139653db782", + "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" : "c7ccf556-1270-4adb-8c9a-d50d0370acda", + "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" : "2dd78821-8a10-4701-8131-22f02eeca193", + "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" : "3d5aad69-76b9-4859-8500-9ded3a096e22", + "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" : "ffd3082c-2a68-425c-a248-95fc076b211c", + "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" : "1fcb0e73-4ec2-4605-9e49-cc7588a6c818", + "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" : "39102862-55f7-4134-be4f-918b2cbfa169", + "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" : "27a49f8e-4a94-4818-991e-eefddf680431", + "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" : "c3056f11-5d2d-4e77-9a17-94168854f540", + "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" : "a91298df-ebf0-4c98-8587-6c680a2c4ccd", + "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" : "e1a83662-2baf-4955-bf07-6c3d50bc1196", + "alias" : "create unique user config", + "config" : { + "require.password.update.after.registration" : "false" + } + }, { + "id" : "d683f0f4-7685-40e1-9521-5761fdd9e111", + "alias" : "review profile config", + "config" : { + "update.profile.on.first.login" : "missing" } - ], - "browserFlow": "browser", - "registrationFlow": "registration", - "directGrantFlow": "direct grant", - "resetCredentialsFlow": "reset credentials", - "clientAuthenticationFlow": "clients", - "dockerAuthenticationFlow": "docker auth", - "attributes": { - "clientOfflineSessionMaxLifespan": "0", - "clientSessionIdleTimeout": "0", - "clientSessionMaxLifespan": "0", - "clientOfflineSessionIdleTimeout": "0" + } ], + "requiredActions" : [ { + "alias" : "CONFIGURE_TOTP", + "name" : "Configure OTP", + "providerId" : "CONFIGURE_TOTP", + "enabled" : true, + "defaultAction" : false, + "priority" : 10, + "config" : { } + }, { + "alias" : "terms_and_conditions", + "name" : "Terms and Conditions", + "providerId" : "terms_and_conditions", + "enabled" : false, + "defaultAction" : false, + "priority" : 20, + "config" : { } + }, { + "alias" : "UPDATE_PASSWORD", + "name" : "Update Password", + "providerId" : "UPDATE_PASSWORD", + "enabled" : true, + "defaultAction" : false, + "priority" : 30, + "config" : { } + }, { + "alias" : "UPDATE_PROFILE", + "name" : "Update Profile", + "providerId" : "UPDATE_PROFILE", + "enabled" : true, + "defaultAction" : false, + "priority" : 40, + "config" : { } + }, { + "alias" : "VERIFY_EMAIL", + "name" : "Verify Email", + "providerId" : "VERIFY_EMAIL", + "enabled" : true, + "defaultAction" : false, + "priority" : 50, + "config" : { } + }, { + "alias" : "delete_account", + "name" : "Delete Account", + "providerId" : "delete_account", + "enabled" : false, + "defaultAction" : false, + "priority" : 60, + "config" : { } + }, { + "alias" : "update_user_locale", + "name" : "Update User Locale", + "providerId" : "update_user_locale", + "enabled" : true, + "defaultAction" : false, + "priority" : 1000, + "config" : { } + } ], + "browserFlow" : "browser", + "registrationFlow" : "registration", + "directGrantFlow" : "direct grant", + "resetCredentialsFlow" : "reset credentials", + "clientAuthenticationFlow" : "clients", + "dockerAuthenticationFlow" : "docker auth", + "attributes" : { + "cibaBackchannelTokenDeliveryMode" : "poll", + "cibaExpiresIn" : "120", + "cibaAuthRequestedUserHint" : "login_hint", + "oauth2DeviceCodeLifespan" : "600", + "clientOfflineSessionMaxLifespan" : "0", + "oauth2DevicePollingInterval" : "5", + "clientSessionIdleTimeout" : "0", + "parRequestUriLifespan" : "60", + "clientSessionMaxLifespan" : "0", + "clientOfflineSessionIdleTimeout" : "0", + "cibaInterval" : "5" + }, + "keycloakVersion" : "18.0.2", + "userManagedAccessAllowed" : false, + "clientProfiles" : { + "profiles" : [ ] }, - "keycloakVersion": "12.0.1", - "userManagedAccessAllowed": false -} + "clientPolicies" : { + "policies" : [ ] + } +}
\ No newline at end of file diff --git a/doc/source/howtos/openid-with-keycloak.rst b/doc/source/howtos/openid-with-keycloak.rst index 74d3a27c0..7fb258fea 100644 --- a/doc/source/howtos/openid-with-keycloak.rst +++ b/doc/source/howtos/openid-with-keycloak.rst @@ -73,6 +73,19 @@ Finally, go back to the clients list and pick the ``zuul`` client again. Click on ``Client Scopes``, and add the ``zuul_aud`` scope to the ``Assigned Default Client Scopes``. +Configuring JWT signing algorithms +.................................. + +.. note:: + + Skip this step if you are using a keycloak version prior to 18.0. + +Due to current limitations with the pyJWT library, Zuul does not support every default +signing algorithm used by Keycloak. + +Go to `my_realm->Settings->Keys`, then choose `rsa-enc-generated` (this should be mapped +to "RSA-OAEP") if available. Then set `enabled` to false and save your changes. + (Optional) Set up a social identity provider ............................................ diff --git a/doc/source/tutorials/keycloak.rst b/doc/source/tutorials/keycloak.rst index a6c011eed..5242a4f05 100644 --- a/doc/source/tutorials/keycloak.rst +++ b/doc/source/tutorials/keycloak.rst @@ -28,6 +28,15 @@ to `/etc/hosts`. Make sure you have a line that looks like this: 127.0.0.1 localhost keycloak +If you are using podman, you need to add the following option in $HOME/.config/containers/containers.conf: + +.. code-block:: + + [containers] + no_hosts=true + +This way your /etc/hosts settings will not interfere with podman's networking. + Restart Zuul Containers ----------------------- @@ -37,14 +46,14 @@ that we can update Zuul's configuration to add authentication. .. code-block:: shell cd zuul/doc/source/examples - sudo -E docker-compose -p zuul-tutorial down + sudo -E docker-compose-compose -p zuul-tutorial down Restart the containers with a new Zuul configuration. .. code-block:: shell cd zuul/doc/source/examples - ZUUL_TUTORIAL_CONFIG="./keycloak/etc_zuul/" sudo -E docker-compose -p zuul-tutorial up -d + ZUUL_TUTORIAL_CONFIG="./keycloak/etc_zuul/" sudo -E docker-compose-compose -p zuul-tutorial up -d This tells docker-compose to use these Zuul `config files <https://opendev.org/zuul/zuul/src/branch/master/doc/source/examples/keycloak>`_. @@ -58,7 +67,7 @@ with this command: .. code-block:: shell cd zuul/doc/source/examples/keycloak - sudo -E docker-compose -p zuul-tutorial-keycloak up -d + sudo -E docker-compose-compose -p zuul-tutorial-keycloak up -d Once Keycloak is running, you can visit the web interface at http://localhost:8082/ diff --git a/playbooks/zuul-stream/fixtures/test-stream.yaml b/playbooks/zuul-stream/fixtures/test-stream.yaml index 0c10304b2..0326ae54e 100644 --- a/playbooks/zuul-stream/fixtures/test-stream.yaml +++ b/playbooks/zuul-stream/fixtures/test-stream.yaml @@ -64,3 +64,15 @@ - name: Print binary data command: echo -e '\x80abc' + + - name: Find any console log files + find: + paths: /tmp + patterns: 'console-*.log' + register: _tmp_files + + # We check this list in zuul-stream/functional.yaml to make sure + # we're cleaning up console log files. + - name: Dump tmp files + debug: + var: _tmp_files diff --git a/playbooks/zuul-stream/functional.yaml b/playbooks/zuul-stream/functional.yaml index 3b79e7b40..b8a44a87c 100644 --- a/playbooks/zuul-stream/functional.yaml +++ b/playbooks/zuul-stream/functional.yaml @@ -19,12 +19,37 @@ ZUUL_JOB_LOG_CONFIG: "{{ ansible_user_dir}}/logging.json" ZUUL_JOBDIR: "{{ ansible_user_dir}}" PYTHONPATH: "{{ python_path }}" + register: _success_output + + - name: Save raw output to file + copy: + content: '{{ _success_output.stdout }}' + dest: 'console-job-output-success-19887.txt' - name: Save output shell: | mv job-output.txt job-output-success-19887.txt mv job-output.json job-output-success-19887.json + - name: Check protocol version + assert: + that: + - "'[node1] Reports streaming version: 1' in _success_output.stdout" + + # Streamer puts out a line like + # [node1] Starting to log 916b2084-4bbb-80e5-248e-000000000016-1-node1 for task TASK: Print binary data + # One of the tasks in job-output shows find: results; + # the console file for this task should not be there. + - name: Validate temporary files removed + shell: | + for f in $(grep 'Starting to log' console-job-output-success-19887.txt | awk '{print $5}'); do + echo "Checking ${f}" + if grep -q '"path": "/tmp/console-'${f}'.log"' job-output-success-19887.txt; then + echo "*** /tmp/${f}.log still exists" + exit 1 + fi + done + # NOTE(ianw) 2022-07 : we deliberatly have this second step to run # against the console setup by the infrastructure executor in the # job pre playbooks as a backwards compatability sanity check. @@ -37,6 +62,12 @@ ZUUL_JOB_LOG_CONFIG: "{{ ansible_user_dir}}/logging.json" ZUUL_JOBDIR: "{{ ansible_user_dir}}" PYTHONPATH: "{{ python_path }}" + register: _success_output + + - name: Save raw output to file + copy: + content: '{{ _success_output.stdout }}' + dest: 'console-job-output-success-19885.txt' - name: Save output shell: | @@ -46,8 +77,10 @@ - name: Validate outputs include_tasks: validate.yaml loop: - - job-output-success-19887.txt - - job-output-success-19885.txt + - { node: 'node1', filename: 'job-output-success-19887.txt' } + - { node: 'node2', filename: 'job-output-success-19887.txt' } + - { node: 'node1', filename: 'job-output-success-19885.txt' } + - { node: 'node2', filename: 'job-output-success-19885.txt' } # failure case diff --git a/playbooks/zuul-stream/post.yaml b/playbooks/zuul-stream/post.yaml index a93771881..4beb8d1f9 100644 --- a/playbooks/zuul-stream/post.yaml +++ b/playbooks/zuul-stream/post.yaml @@ -18,6 +18,7 @@ with_items: - logging.json - ansible.cfg + - console-job-output-success-19887.txt - job-output-success-19887.txt - job-output-success-19887.json - job-output-success-19885.txt diff --git a/playbooks/zuul-stream/validate.yaml b/playbooks/zuul-stream/validate.yaml index 73ccd873a..81c613406 100644 --- a/playbooks/zuul-stream/validate.yaml +++ b/playbooks/zuul-stream/validate.yaml @@ -1,38 +1,29 @@ - name: Validate output - setupvar shell: | - egrep "^.*\| node1 \|\s+\"setupvar\": {" {{ item }} - egrep "^.*\| node2 \|\s+\"setupvar\": {" {{ item }} + egrep "^.*\| {{ item.node }} \|\s+\"setupvar\": {" {{ item.filename }} - name: Validate output - shell task shell: | - egrep "^.*\| node1 \| 1: lo:" {{ item }} - egrep "^.*\| node2 \| 1: lo:" {{ item }} + egrep "^.*\| {{ item.node }} \| 1: lo:" {{ item.filename }} - name: Validate output - loop with items shell: | - egrep "^.+\| node1 \| ok: Item: item1" {{ item }} - egrep "^.+\| node1 \| ok: Item: item2" {{ item }} - egrep "^.+\| node1 \| ok: Item: item3" {{ item }} - egrep "^.+\| node2 \| ok: Item: item1" {{ item }} - egrep "^.+\| node2 \| ok: Item: item2" {{ item }} - egrep "^.+\| node2 \| ok: Item: item3" {{ item }} + egrep "^.+\| {{ item.node }} \| ok: Item: item1" {{ item.filename }} + egrep "^.+\| {{ item.node }} \| ok: Item: item2" {{ item.filename }} + egrep "^.+\| {{ item.node }} \| ok: Item: item3" {{ item.filename }} - name: Validate output - loop with complex items shell: | - egrep "^.+\| node1 \| ok: Item: Runtime" {{ item }} - egrep "^.+\| node2 \| ok: Item: Runtime" {{ item }} + egrep "^.+\| {{ item.node }} \| ok: Item: Runtime" {{ item.filename }} - name: Validate output - failed shell task shell: | - egrep "^.+\| node1 \| Exception: Test module failure exception task" {{ item }} - egrep "^.+\| node2 \| Exception: Test module failure exception task" {{ item }} + egrep "^.+\| {{ item.node }} \| Exception: Test module failure exception task" {{ item.filename }} - name: Validate output - item loop with exception shell: | - egrep "^.+\| node1 \| Exception: Test module failure exception loop" {{ item }} - egrep "^.+\| node2 \| Exception: Test module failure exception loop" {{ item }} + egrep "^.+\| {{ item.node }} \| Exception: Test module failure exception loop" {{ item.filename }} - name: Validate output - binary data shell: | - egrep "^.*\| node1 \| \\\\x80abc" {{ item }} - egrep "^.*\| node2 \| \\\\x80abc" {{ item }} + egrep "^.*\| {{ item.node }} \| \\\\x80abc" {{ item.filename }} @@ -1,12 +1,12 @@ [metadata] name = zuul summary = A Project Gating System -description-file = +description_file = README.rst author = Zuul Team -author-email = zuul-discuss@lists.zuul-ci.org -home-page = https://zuul-ci.org/ -python-requires = >=3.8 +author_email = zuul-discuss@lists.zuul-ci.org +home_page = https://zuul-ci.org/ +python_requires = >=3.8 classifier = Intended Audience :: Information Technology Intended Audience :: System Administrators diff --git a/tests/fixtures/layouts/special-characters-job.yaml b/tests/fixtures/layouts/special-characters-job.yaml new file mode 100644 index 000000000..20308c6d6 --- /dev/null +++ b/tests/fixtures/layouts/special-characters-job.yaml @@ -0,0 +1,2 @@ +- job: + name: "a@b/c" diff --git a/tests/remote/test_remote_zuul_stream.py b/tests/remote/test_remote_zuul_stream.py index 1f6b7fff7..1c705127e 100644 --- a/tests/remote/test_remote_zuul_stream.py +++ b/tests/remote/test_remote_zuul_stream.py @@ -29,7 +29,7 @@ class FunctionalZuulStreamMixIn: self.log_console_port = 19000 + int( self.ansible_core_version.split('.')[1]) self.executor_server.log_console_port = self.log_console_port - self.wait_timeout = 120 + self.wait_timeout = 180 self.fake_nodepool.remote_ansible = True ansible_remote = os.environ.get('ZUUL_REMOTE_IPV4') diff --git a/tests/unit/test_gerrit.py b/tests/unit/test_gerrit.py index aa2bb1758..47545b9be 100644 --- a/tests/unit/test_gerrit.py +++ b/tests/unit/test_gerrit.py @@ -13,6 +13,7 @@ # under the License. import os +import threading import textwrap from unittest import mock @@ -868,3 +869,58 @@ class TestGerritFake(ZuulTestCase): # The Gerrit connection method filters out the queried change ret = self.fake_gerrit._getSubmittedTogether(C1, None) self.assertEqual(ret, [(4, 1)]) + + +class TestGerritConnection(ZuulTestCase): + config_file = 'zuul-gerrit-web.conf' + tenant_config_file = 'config/single-tenant/main.yaml' + + def test_zuul_query_ltime(self): + # Add a lock around the event queue iterator so that we can + # ensure that multiple events arrive before the first is + # processed. + lock = threading.Lock() + + orig_iterEvents = self.fake_gerrit.gerrit_event_connector.\ + event_queue._iterEvents + + def _iterEvents(*args, **kw): + with lock: + return orig_iterEvents(*args, **kw) + + self.patch(self.fake_gerrit.gerrit_event_connector.event_queue, + '_iterEvents', _iterEvents) + + A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A') + B = self.fake_gerrit.addFakeChange('org/project', 'master', 'B') + B.setDependsOn(A, 1) + # Hold the connection queue processing so these events get + # processed together + with lock: + self.fake_gerrit.addEvent(A.addApproval('Code-Review', 2)) + self.fake_gerrit.addEvent(B.addApproval('Approved', 1)) + self.fake_gerrit.addEvent(B.addApproval('Code-Review', 2)) + self.waitUntilSettled() + self.assertHistory([]) + # One query for each change in the above cluster of events. + self.assertEqual(A.queried, 1) + self.assertEqual(B.queried, 1) + self.fake_gerrit.addEvent(A.addApproval('Approved', 1)) + self.waitUntilSettled() + self.assertHistory([ + dict(name="project-merge", result="SUCCESS", changes="1,1"), + dict(name="project-test1", result="SUCCESS", changes="1,1"), + dict(name="project-test2", result="SUCCESS", changes="1,1"), + dict(name="project-merge", result="SUCCESS", changes="1,1 2,1"), + dict(name="project-test1", result="SUCCESS", changes="1,1 2,1"), + dict(name="project-test2", result="SUCCESS", changes="1,1 2,1"), + ], ordered=False) + # One query due to the event on change A, followed by a query + # to verify the merge. + self.assertEqual(A.queried, 3) + # No query for change B necessary since our cache is up to + # date with respect for the triggering event. One query to + # verify the merge. + self.assertEqual(B.queried, 2) + self.assertEqual(A.data['status'], 'MERGED') + self.assertEqual(B.data['status'], 'MERGED') diff --git a/tests/unit/test_scheduler.py b/tests/unit/test_scheduler.py index 5e0385be3..66c508fea 100644 --- a/tests/unit/test_scheduler.py +++ b/tests/unit/test_scheduler.py @@ -226,6 +226,18 @@ class TestSchedulerZoneFallback(ZuulTestCase): def test_jobs_executed(self): "Test that jobs are executed and a change is merged per zone" self.hold_jobs_in_queue = True + + # Validate that the reported executor stats are correct. Since + # the executor accepts zoned and unzoned job it should be counted + # in both metrics. + self.assertReportedStat( + 'zuul.executors.online', value='1', kind='g') + self.assertReportedStat( + 'zuul.executors.unzoned.online', value='1', kind='g') + self.assertReportedStat( + 'zuul.executors.zone.test-provider_vpn.online', + value='1', kind='g') + A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A') A.addApproval('Code-Review', 2) self.fake_gerrit.addEvent(A.addApproval('Approved', 1)) diff --git a/tests/unit/test_web.py b/tests/unit/test_web.py index 53546c1de..ba1931436 100644 --- a/tests/unit/test_web.py +++ b/tests/unit/test_web.py @@ -1005,6 +1005,11 @@ class TestWeb(BaseTestWeb): job = self.get_url("api/tenant/tenant-one/job/noop").json() self.assertEqual("noop", job[0]["name"]) + @simple_layout('layouts/special-characters-job.yaml') + def test_web_job_special_characters(self): + job = self.get_url("api/tenant/tenant-one/job/a%40b%2Fc").json() + self.assertEqual("a@b/c", job[0]["name"]) + def test_freeze_jobs(self): # Test can get a list of the jobs for a given project+pipeline+branch. resp = self.get_url( diff --git a/tests/zuul_client/test_zuulclient.py b/tests/zuul_client/test_zuulclient.py index cc7d789c7..9d381c99b 100644 --- a/tests/zuul_client/test_zuulclient.py +++ b/tests/zuul_client/test_zuulclient.py @@ -629,3 +629,92 @@ class TestZuulClientBuildInfo(TestZuulClientQueryData, x['url'] == 'http://example.com/docs' for x in artifacts), output) + + +class TestZuulClientJobGraph(BaseTestWeb): + def _split_pretty_table(self, output): + lines = output.decode().split('\n') + headers = [x.strip() for x in lines[1].split('|') if x != ''] + # Trim headers and last line of the table + return [dict(zip(headers, + [x.strip() for x in l.split('|') if x != ''])) + for l in lines[3:-2]] + + def test_job_graph(self): + """Test the job-graph command""" + p = subprocess.Popen( + ['zuul-client', + '--zuul-url', self.base_url, + 'job-graph', + '--tenant', 'tenant-one', + '--pipeline', 'check', + '--project', 'org/project1', + '--branch', 'master', + ], + stdout=subprocess.PIPE) + output, err = p.communicate() + self.assertEqual(p.returncode, 0, (output, err)) + results = self._split_pretty_table(output) + expected = [ + {'Job': 'project-merge', 'Dependencies': ''}, + {'Job': 'project-test1', 'Dependencies': 'project-merge'}, + {'Job': 'project-test2', 'Dependencies': 'project-merge'}, + {'Job': 'project1-project2-integration', + 'Dependencies': 'project-merge'} + ] + self.assertEqual(results, expected) + + def test_job_graph_dot(self): + """Test the job-graph command dot output""" + p = subprocess.Popen( + ['zuul-client', + '--format', 'dot', + '--zuul-url', self.base_url, + 'job-graph', + '--tenant', 'tenant-one', + '--pipeline', 'check', + '--project', 'org/project1', + '--branch', 'master', + ], + stdout=subprocess.PIPE) + output, err = p.communicate() + self.assertEqual(p.returncode, 0, (output, err)) + expected = textwrap.dedent('''\ + digraph job_graph { + rankdir=LR; + node [shape=box]; + "project-merge"; + "project-merge" -> "project-test1" [dir=back]; + "project-merge" -> "project-test2" [dir=back]; + "project-merge" -> "project1-project2-integration" [dir=back]; + } + ''').encode('utf8') + self.assertEqual(output.strip(), expected.strip()) + + +class TestZuulClientFreezeJob(BaseTestWeb): + def test_freeze_job(self): + """Test the freeze-job command""" + p = subprocess.Popen( + ['zuul-client', + '--zuul-url', self.base_url, + 'freeze-job', + '--tenant', 'tenant-one', + '--pipeline', 'check', + '--project', 'org/project1', + '--branch', 'master', + '--job', 'project-test1', + ], + stdout=subprocess.PIPE) + output, err = p.communicate() + self.assertEqual(p.returncode, 0, (output, err)) + output = output.decode('utf8') + for s in [ + 'Job: project-test1', + 'Branch: master', + 'Ansible Version:', + 'Workspace Scheme: golang', + ('gerrit:common-config:playbooks/project-test1.yaml' + '@master [trusted]'), + ]: + self.assertIn(s, output) diff --git a/tools/test-setup-docker.sh b/tools/test-setup-docker.sh index b90a422c2..a0fcf9f5a 100755 --- a/tools/test-setup-docker.sh +++ b/tools/test-setup-docker.sh @@ -1,6 +1,18 @@ #!/bin/bash -set -eu +# This runs ZooKeeper and databases in docker containers, which are +# required for tests. + +# This setup needs to be run as a user that can run docker or podman, or by +# setting $ROOTCMD to a user substitution tool like "sudo" in the calling +# environment. + +set -xeu + +# Default ROOTCMD to the 'env' command, otherwise variable assignments will be +# interpreted as command when no ROOTCMD is given. The reason for that is +# Bash's simple command expansion. +ROOTCMD=${ROOTCMD:-env} cd $(dirname $0) SCRIPT_DIR="$(pwd)" @@ -29,9 +41,9 @@ fi MYSQL="${DOCKER} exec zuul-test-mysql mysql -u root -pinsecure_worker" if [ "${COMPOSE}" == "docker-compose" ]; then - docker-compose rm -sf + ${ROOTCMD} docker-compose rm -sf else - podman-compose down + ${ROOTCMD} podman-compose down fi CA_DIR=$SCRIPT_DIR/ca @@ -39,15 +51,14 @@ CA_DIR=$SCRIPT_DIR/ca mkdir -p $CA_DIR $SCRIPT_DIR/zk-ca.sh $CA_DIR zuul-test-zookeeper -export USER_ID=$(id -u) -${COMPOSE} up -d +${ROOTCMD} USER_ID=$(id -u) ${COMPOSE} up -d echo "Waiting for mysql" -timeout 30 bash -c "until ${MYSQL} -e 'show databases'; do sleep 0.5; done" +timeout 30 bash -c "until ${ROOTCMD} ${MYSQL} -e 'show databases'; do sleep 0.5; done" echo echo "Setting up permissions for zuul tests" -${MYSQL} -e "GRANT ALL PRIVILEGES ON *.* TO 'openstack_citest'@'%' identified by 'openstack_citest' WITH GRANT OPTION;" -${MYSQL} -u openstack_citest -popenstack_citest -e "SET default_storage_engine=MYISAM; DROP DATABASE IF EXISTS openstack_citest; CREATE DATABASE openstack_citest CHARACTER SET utf8;" +${ROOTCMD} ${MYSQL} -e "GRANT ALL PRIVILEGES ON *.* TO 'openstack_citest'@'%' identified by 'openstack_citest' WITH GRANT OPTION;" +${ROOTCMD} ${MYSQL} -u openstack_citest -popenstack_citest -e "SET default_storage_engine=MYISAM; DROP DATABASE IF EXISTS openstack_citest; CREATE DATABASE openstack_citest CHARACTER SET utf8;" echo "Finished" @@ -1,11 +1,8 @@ [tox] minversion = 3.2 skipsdist = True -envlist = linters,py3{-docker} +envlist = linters,py3 ignore_basepython_conflict = True -# TODO(stephenfin): It would be good to set 'requires = tox-docker', but doing -# so borks the tools/pip.sh wrapper we're using here and probably isn't what -# we want in the gate :( [testenv] basepython = python3 @@ -19,7 +16,6 @@ setenv = SQLALCHEMY_WARN_20={env:SQLALCHEMY_WARN_20:1} PYTHONWARNINGS=always::DeprecationWarning:zuul.driver.sql.sqlconnection,always::DeprecationWarning:tests.base,always::DeprecationWarning:tests.unit.test_database,always::DeprecationWarning:zuul.driver.sql.alembic.env,always::DeprecationWarning:zuul.driver.sql.alembic.script passenv = - DOCKER_* NODEPOOL_ZK_HOST OS_LOG_CAPTURE OS_LOG_DEFAULTS @@ -40,8 +36,6 @@ whitelist_externals = bash deps = -r{toxinidir}/requirements.txt -r{toxinidir}/test-requirements.txt -docker = - docker: zookeeper:3.4 commands = bash -c 'stestr run --slowest --concurrency=`python -c "import multiprocessing; print(max(int(multiprocessing.cpu_count()-1),1))"` {posargs}' @@ -96,7 +90,6 @@ deps = [testenv:remote] passenv = - DOCKER_* NODEPOOL_ZK_HOST OS_LOG_CAPTURE OS_LOG_DEFAULTS diff --git a/web/package.json b/web/package.json index 7b6eef9cd..3122e8839 100644 --- a/web/package.json +++ b/web/package.json @@ -13,6 +13,7 @@ "@softwarefactory-project/re-ansi": "^0.5.0", "axios": "^0.26.0", "broadcast-channel": "^4.5.0", + "d3-graphviz": "2.6.1", "js-yaml": "^3.13.0", "lodash": "^4.17.10", "moment": "^2.22.2", @@ -57,7 +58,7 @@ "start:openstack": "REACT_APP_ZUUL_API='https://zuul.openstack.org/api/' react-scripts start", "start:multi": "REACT_APP_ZUUL_API='https://softwarefactory-project.io/zuul/api/' react-scripts start", "start": "react-scripts start", - "build": "react-scripts build", + "build": "react-scripts --max_old_space_size=4096 build", "test": "react-scripts test --env=jsdom --watchAll=false", "eject": "react-scripts eject", "lint": "eslint --ext .js --ext .jsx src", diff --git a/web/src/actions/freezejob.js b/web/src/actions/freezejob.js new file mode 100644 index 000000000..13b32f124 --- /dev/null +++ b/web/src/actions/freezejob.js @@ -0,0 +1,84 @@ +// Copyright 2018 Red Hat, Inc +// Copyright 2022 Acme Gating, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +import * as API from '../api' + +export const FREEZE_JOB_FETCH_REQUEST = 'FREEZE_JOB_FETCH_REQUEST' +export const FREEZE_JOB_FETCH_SUCCESS = 'FREEZE_JOB_FETCH_SUCCESS' +export const FREEZE_JOB_FETCH_FAIL = 'FREEZE_JOB_FETCH_FAIL' + +export const requestFreezeJob = () => ({ + type: FREEZE_JOB_FETCH_REQUEST +}) + +export function makeFreezeJobKey(pipeline, project, branch, job) { + return JSON.stringify({ + pipeline, project, branch, job + }) +} + +export const receiveFreezeJob = (tenant, freezeJobKey, freezeJob) => { + return { + type: FREEZE_JOB_FETCH_SUCCESS, + tenant: tenant, + freezeJobKey: freezeJobKey, + freezeJob: freezeJob, + receivedAt: Date.now(), + } +} + +const failedFreezeJob = error => ({ + type: FREEZE_JOB_FETCH_FAIL, + error +}) + +const fetchFreezeJob = (tenant, pipeline, project, branch, job) => dispatch => { + dispatch(requestFreezeJob()) + const freezeJobKey = makeFreezeJobKey(pipeline, project, branch, job) + return API.fetchFreezeJob(tenant.apiPrefix, + pipeline, + project, + branch, + job) + .then(response => dispatch(receiveFreezeJob( + tenant.name, freezeJobKey, response.data))) + .catch(error => dispatch(failedFreezeJob(error))) +} + +const shouldFetchFreezeJob = (tenant, pipeline, project, branch, job, state) => { + const freezeJobKey = makeFreezeJobKey(pipeline, project, branch, job) + const tenantFreezeJobs = state.freezejob.freezeJobs[tenant.name] + if (tenantFreezeJobs) { + const freezeJob = tenantFreezeJobs[freezeJobKey] + if (!freezeJob) { + return true + } + if (freezeJob.isFetching) { + return false + } + return false + } + return true +} + +export const fetchFreezeJobIfNeeded = (tenant, pipeline, project, branch, job, + force) => ( + dispatch, getState) => { + if (force || shouldFetchFreezeJob(tenant, pipeline, project, branch, job, + getState())) { + return dispatch(fetchFreezeJob(tenant, pipeline, project, branch, job)) + } + return Promise.resolve() +} diff --git a/web/src/actions/jobgraph.js b/web/src/actions/jobgraph.js new file mode 100644 index 000000000..44ca2cb07 --- /dev/null +++ b/web/src/actions/jobgraph.js @@ -0,0 +1,83 @@ +// Copyright 2018 Red Hat, Inc +// Copyright 2022 Acme Gating, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +import * as API from '../api' + +export const JOB_GRAPH_FETCH_REQUEST = 'JOB_GRAPH_FETCH_REQUEST' +export const JOB_GRAPH_FETCH_SUCCESS = 'JOB_GRAPH_FETCH_SUCCESS' +export const JOB_GRAPH_FETCH_FAIL = 'JOB_GRAPH_FETCH_FAIL' + +export const requestJobGraph = () => ({ + type: JOB_GRAPH_FETCH_REQUEST +}) + +export function makeJobGraphKey(project, pipeline, branch) { + return JSON.stringify({ + project: project, pipeline: pipeline, branch: branch + }) +} + +export const receiveJobGraph = (tenant, jobGraphKey, jobGraph) => { + return { + type: JOB_GRAPH_FETCH_SUCCESS, + tenant: tenant, + jobGraphKey: jobGraphKey, + jobGraph: jobGraph, + receivedAt: Date.now(), + } +} + +const failedJobGraph = error => ({ + type: JOB_GRAPH_FETCH_FAIL, + error +}) + +const fetchJobGraph = (tenant, project, pipeline, branch) => dispatch => { + dispatch(requestJobGraph()) + const jobGraphKey = makeJobGraphKey(project, pipeline, branch) + return API.fetchJobGraph(tenant.apiPrefix, + project, + pipeline, + branch) + .then(response => dispatch(receiveJobGraph( + tenant.name, jobGraphKey, response.data))) + .catch(error => dispatch(failedJobGraph(error))) +} + +const shouldFetchJobGraph = (tenant, project, pipeline, branch, state) => { + const jobGraphKey = makeJobGraphKey(project, pipeline, branch) + const tenantJobGraphs = state.jobgraph.jobGraphs[tenant.name] + if (tenantJobGraphs) { + const jobGraph = tenantJobGraphs[jobGraphKey] + if (!jobGraph) { + return true + } + if (jobGraph.isFetching) { + return false + } + return false + } + return true +} + +export const fetchJobGraphIfNeeded = (tenant, project, pipeline, branch, + force) => ( + dispatch, getState) => { + if (force || shouldFetchJobGraph(tenant, project, pipeline, branch, + getState())) { + return dispatch(fetchJobGraph(tenant, project, pipeline, branch)) + } + return Promise.resolve() +} diff --git a/web/src/actions/pipelines.js b/web/src/actions/pipelines.js new file mode 100644 index 000000000..7139524f2 --- /dev/null +++ b/web/src/actions/pipelines.js @@ -0,0 +1,62 @@ +// Copyright 2018 Red Hat, Inc +// Copyright 2022 Acme Gating, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +import * as API from '../api' + +export const PIPELINES_FETCH_REQUEST = 'PIPELINES_FETCH_REQUEST' +export const PIPELINES_FETCH_SUCCESS = 'PIPELINES_FETCH_SUCCESS' +export const PIPELINES_FETCH_FAIL = 'PIPELINES_FETCH_FAIL' + +export const requestPipelines = () => ({ + type: PIPELINES_FETCH_REQUEST +}) + +export const receivePipelines = (tenant, json) => ({ + type: PIPELINES_FETCH_SUCCESS, + tenant: tenant, + pipelines: json, + receivedAt: Date.now() +}) + +const failedPipelines = error => ({ + type: PIPELINES_FETCH_FAIL, + error +}) + +const fetchPipelines = (tenant) => dispatch => { + dispatch(requestPipelines()) + return API.fetchPipelines(tenant.apiPrefix) + .then(response => dispatch(receivePipelines(tenant.name, response.data))) + .catch(error => dispatch(failedPipelines(error))) +} + +const shouldFetchPipelines = (tenant, state) => { + const pipelines = state.pipelines.pipelines[tenant.name] + if (!pipelines || pipelines.length === 0) { + return true + } + if (pipelines.isFetching) { + return false + } + return false +} + +export const fetchPipelinesIfNeeded = (tenant, force) => ( + dispatch, getState) => { + if (force || shouldFetchPipelines(tenant, getState())) { + return dispatch(fetchPipelines(tenant)) + } + return Promise.resolve() +} diff --git a/web/src/api.js b/web/src/api.js index 7b574a6b6..1ba39998c 100644 --- a/web/src/api.js +++ b/web/src/api.js @@ -130,6 +130,13 @@ function fetchStatus(apiPrefix) { function fetchChangeStatus(apiPrefix, changeId) { return Axios.get(apiUrl + apiPrefix + 'status/change/' + changeId) } +function fetchFreezeJob(apiPrefix, pipelineName, projectName, branchName, jobName) { + return Axios.get(apiUrl + apiPrefix + + 'pipeline/' + pipelineName + + '/project/' + projectName + + '/branch/' + branchName + + '/freeze-job/' + jobName) +} function fetchBuild(apiPrefix, buildId) { return Axios.get(apiUrl + apiPrefix + 'build/' + buildId) } @@ -150,6 +157,9 @@ function fetchBuildsets(apiPrefix, queryString) { } return Axios.get(apiUrl + apiPrefix + path) } +function fetchPipelines(apiPrefix) { + return Axios.get(apiUrl + apiPrefix + 'pipelines') +} function fetchProject(apiPrefix, projectName) { return Axios.get(apiUrl + apiPrefix + 'project/' + projectName) } @@ -159,6 +169,13 @@ function fetchProjects(apiPrefix) { function fetchJob(apiPrefix, jobName) { return Axios.get(apiUrl + apiPrefix + 'job/' + jobName) } +function fetchJobGraph(apiPrefix, projectName, pipelineName, branchName) { + return Axios.get(apiUrl + apiPrefix + + 'pipeline/' + pipelineName + + '/project/' + projectName + + '/branch/' + branchName + + '/freeze-jobs') +} function fetchJobs(apiPrefix) { return Axios.get(apiUrl + apiPrefix + 'jobs') } @@ -305,9 +322,12 @@ export { fetchBuilds, fetchBuildset, fetchBuildsets, + fetchFreezeJob, + fetchPipelines, fetchProject, fetchProjects, fetchJob, + fetchJobGraph, fetchJobs, fetchLabels, fetchNodes, diff --git a/web/src/containers/build/Console.jsx b/web/src/containers/build/Console.jsx index 0b365df62..194e314ee 100644 --- a/web/src/containers/build/Console.jsx +++ b/web/src/containers/build/Console.jsx @@ -50,6 +50,19 @@ class TaskOutput extends React.Component { renderResults(value) { const interesting_results = [] + + // This was written to assume "value" is an array of + // key/value mappings to output. This seems to be a + // good assumption for the most part, but "package:" for + // whatever reason outputs a result that is just an array of + // strings with what packages were installed. So, if we + // see an array of strings as the value, we just swizzle + // that into a key/value so it displays usefully. + const isAllStrings = value.every(i => typeof i === 'string') + if (isAllStrings) { + value = [ {output: [...value]} ] + } + value.forEach((result, idx) => { const keys = Object.entries(result).filter( ([key, value]) => shouldIncludeKey( diff --git a/web/src/containers/freezejob/FreezeJobToolbar.jsx b/web/src/containers/freezejob/FreezeJobToolbar.jsx new file mode 100644 index 000000000..c067b7205 --- /dev/null +++ b/web/src/containers/freezejob/FreezeJobToolbar.jsx @@ -0,0 +1,200 @@ +// Copyright 2020 BMW Group +// Copyright 2022 Acme Gating, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +import React, { useEffect, useState } from 'react' +import { connect } from 'react-redux' +import PropTypes from 'prop-types' +import { + Button, + TextInput, + Dropdown, + DropdownItem, + DropdownPosition, + DropdownToggle, + Toolbar, + ToolbarContent, + ToolbarGroup, + ToolbarItem, + ToolbarToggleGroup, +} from '@patternfly/react-core' + +import { fetchPipelinesIfNeeded } from '../../actions/pipelines' + +function FreezeJobToolbar(props) { + const { tenant, fetchPipelinesIfNeeded } = props + + useEffect(() => { + fetchPipelinesIfNeeded(tenant) + }, [fetchPipelinesIfNeeded, tenant]) + + const tenantPipelines = props.pipelines.pipelines[tenant.name] + const pipelines = tenantPipelines ? tenantPipelines.map(p => { return p.name }) : [] + + const [isPipelineOpen, setIsPipelineOpen] = useState(false) + const [currentPipeline, setCurrentPipeline] = useState(props.defaultPipeline || '') + const [currentProject, setCurrentProject] = useState(props.defaultProject || '') + const [currentBranch, setCurrentBranch] = useState(props.defaultBranch || '') + const [currentJob, setCurrentJob] = useState(props.defaultJob || '') + + if (!currentPipeline && pipelines.length) { + // We may have gotten a list of pipelines after we loaded the page + setCurrentPipeline(pipelines[0]) + } + + function handlePipelineSelect(event) { + setCurrentPipeline(event.target.innerText) + setIsPipelineOpen(false) + } + + function handlePipelineToggle(isOpen) { + setIsPipelineOpen(isOpen) + } + + function handleProjectChange(newValue) { + setCurrentProject(newValue) + } + + function handleBranchChange(newValue) { + setCurrentBranch(newValue) + } + + function handleJobChange(newValue) { + setCurrentJob(newValue) + } + + function handleInputSend(event) { + // In case the event comes from a key press, only accept "Enter" + if (event.key && event.key !== 'Enter') { + return + } + + // Ignore empty values + if (!currentBranch || !currentProject || !currentJob) { + return + } + + // Notify the parent component about the filter change + props.onChange(currentPipeline, currentProject, currentBranch, currentJob) + } + + function renderFreezeJobToolbar () { + return <> + <Toolbar collapseListedFiltersBreakpoint="md"> + <ToolbarContent> + <ToolbarToggleGroup breakpoint="md"> + <ToolbarGroup variant="filter-group"> + + <ToolbarItem key="pipeline"> + <Dropdown + onSelect={handlePipelineSelect} + position={DropdownPosition.left} + toggle={ + <DropdownToggle + onToggle={handlePipelineToggle} + style={{ width: '100%' }} + > + Pipeline: {currentPipeline} + </DropdownToggle> + } + isOpen={isPipelineOpen} + dropdownItems={pipelines.map((pipeline) => ( + <DropdownItem key={pipeline}>{pipeline}</DropdownItem> + ))} + style={{ width: '100%' }} + menuAppendTo={document.body} + /> + </ToolbarItem> + + <ToolbarItem key="project"> + <TextInput + name="project" + id="project-input" + type="search" + placeholder="Project" + defaultValue={props.defaultProject} + onChange={handleProjectChange} + onKeyDown={(event) => handleInputSend(event)} + /> + </ToolbarItem> + + <ToolbarItem key="branch"> + <TextInput + name="branch" + id="branch-input" + type="search" + placeholder="Branch" + defaultValue={props.defaultBranch} + onChange={handleBranchChange} + onKeyDown={(event) => handleInputSend(event)} + /> + </ToolbarItem> + + <ToolbarItem key="job"> + <TextInput + name="job" + id="job-input" + type="search" + placeholder="Job" + defaultValue={props.defaultJob} + onChange={handleJobChange} + onKeyDown={(event) => handleInputSend(event)} + /> + </ToolbarItem> + + <ToolbarItem key="button"> + <Button + onClick={(event) => handleInputSend(event)} + > + Freeze Job + </Button> + </ToolbarItem> + + </ToolbarGroup> + </ToolbarToggleGroup> + </ToolbarContent> + </Toolbar> + </> + } + + return ( + <div> + {renderFreezeJobToolbar()} + </div> + ) +} + +FreezeJobToolbar.propTypes = { + fetchPipelinesIfNeeded: PropTypes.func, + tenant: PropTypes.object, + pipelines: PropTypes.object, + onChange: PropTypes.func.isRequired, + defaultPipeline: PropTypes.string, + defaultProject: PropTypes.string, + defaultBranch: PropTypes.string, + defaultJob: PropTypes.string, +} + +function mapStateToProps(state) { + return { + tenant: state.tenant, + pipelines: state.pipelines, + } +} + +const mapDispatchToProps = { + fetchPipelinesIfNeeded +} + +export default connect(mapStateToProps, mapDispatchToProps)(FreezeJobToolbar) diff --git a/web/src/containers/jobgraph/JobGraph.jsx b/web/src/containers/jobgraph/JobGraph.jsx new file mode 100644 index 000000000..75d77161d --- /dev/null +++ b/web/src/containers/jobgraph/JobGraph.jsx @@ -0,0 +1,78 @@ +// Copyright 2022 Acme Gating, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +import React, { useState } from 'react' +import PropTypes from 'prop-types' +import { connect } from 'react-redux' +import { useHistory, useLocation } from 'react-router-dom' + +import JobGraphToolbar from './JobGraphToolbar' +import JobGraphDisplay from './JobGraphDisplay' + +function JobGraph(props) { + const [currentPipeline, setCurrentPipeline] = useState() + const [currentBranch, setCurrentBranch] = useState() + const history = useHistory() + const location = useLocation() + + if (!currentBranch) { + const urlParams = new URLSearchParams(location.search) + const branch = urlParams.get('branch') + const pipeline = urlParams.get('pipeline') + if (pipeline && branch) { + setCurrentPipeline(pipeline) + setCurrentBranch(branch) + } + } + + function onChange(pipeline, branch) { + setCurrentPipeline(pipeline) + setCurrentBranch(branch) + + const searchParams = new URLSearchParams('') + searchParams.append('branch', branch) + searchParams.append('pipeline', pipeline) + history.push({ + pathname: location.pathname, + search: searchParams.toString(), + }) + } + + return ( + <> + <JobGraphToolbar + project={props.project} + onChange={onChange} + defaultBranch={currentBranch} + defaultPipeline={currentPipeline} + /> + {currentPipeline && currentBranch && + <JobGraphDisplay + project={props.project} + pipeline={currentPipeline} + branch={currentBranch} + />} + </> + ) +} + +JobGraph.propTypes = { + project: PropTypes.object.isRequired, + tenant: PropTypes.object, + dispatch: PropTypes.func, +} + +export default connect((state) => ({ + tenant: state.tenant, +}))(JobGraph) diff --git a/web/src/containers/jobgraph/JobGraphDisplay.jsx b/web/src/containers/jobgraph/JobGraphDisplay.jsx new file mode 100644 index 000000000..e5cff9cbc --- /dev/null +++ b/web/src/containers/jobgraph/JobGraphDisplay.jsx @@ -0,0 +1,145 @@ +// Copyright 2022 Acme Gating, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +import React, { useState, useEffect} from 'react' +import PropTypes from 'prop-types' +import { connect } from 'react-redux' +import * as d3 from 'd3' +import { useHistory } from 'react-router-dom' + +import { makeJobGraphKey, fetchJobGraphIfNeeded } from '../../actions/jobgraph' +import { graphviz } from 'd3-graphviz' + +function makeDot(tenant, pipeline, project, branch, jobGraph) { + let ret = 'digraph job_graph {\n' + ret += ' rankdir=LR;\n' + ret += ' node [shape=box];\n' + jobGraph.forEach((job) => { + const searchParams = new URLSearchParams('') + searchParams.append('pipeline', pipeline) + searchParams.append('project', project.name) + searchParams.append('job', job.name) + searchParams.append('branch', branch) + // This will appear in the DOM as an "a href=" but we will + // manipulate the DOM later to add an onClick callback to make + // this an internal link. + const url = (tenant.linkPrefix + + '/freeze-job?' + searchParams.toString()) + // Escape ampersands to get it through graphviz and d3; these will + // appear unescaped in the DOM. + const escaped_url = url.replace(/&/g, '&') + ret += ' "' + job.name + '" [URL="' + escaped_url + '"];\n' + if (job.dependencies.length) { + job.dependencies.forEach((dep) => { + let soft = ' [dir=back]' + if (dep.soft) { + soft = ' [style=dashed dir=back]' + } + ret += ' "' + dep.name + '" -> "' + job.name + '"' + soft + ';\n' + }) + } + }) + ret += '}\n' + return ret +} + +function GraphViz(props) { + const history = useHistory() + + useEffect(() => { + const gv = graphviz('#graphviz') + .options({ + fit: false, + zoom: true, + tweenPaths: false, + scale: 0.75, + }).renderDot(props.dot) + + // Fix up the initial values of the internal transform data; + // without this the first time we pan the graph jumps. + const element = d3.select('.zuul-job-graph > svg') + const transform = element[0][0].firstElementChild.attributes.transform.value + const match = transform.match(/translate\(\d+,(\d+)\).*/) + if (match && match.length > 0) { + const val = parseInt(match[1]) + gv._translation.y = val + gv._originalTransform.y = val + } + + // Disable scroll wheel zooming because it interferes with window + // scrolling + gv.zoomSelection().on('wheel.zoom', null) + + // Mutate the links to be internal links + d3.select('.zuul-job-graph').selectAll('.node a').on('click', event => { + d3.event.preventDefault() + history.push(event.attributes['xlink:href']) + }) + }, [props.dot, history]) + + return ( + <div className="zuul-job-graph" id="graphviz"/> + ) +} + +GraphViz.propTypes = { + dot: PropTypes.string.isRequired, +} + +function JobGraphDisplay(props) { + const [dot, setDot] = useState() + const {fetchJobGraphIfNeeded, tenant, project, pipeline, branch} = props + + useEffect(() => { + fetchJobGraphIfNeeded(tenant, project.name, pipeline, branch) + }, [fetchJobGraphIfNeeded, tenant, project, pipeline, branch]) + + const tenantJobGraph = props.jobgraph.jobGraphs[tenant.name] + const jobGraphKey = makeJobGraphKey(props.project.name, + props.pipeline, + props.branch) + const jobGraph = tenantJobGraph ? tenantJobGraph[jobGraphKey] : undefined + useEffect(() => { + if (jobGraph) { + setDot(makeDot(tenant, pipeline, project, branch, jobGraph)) + } + }, [tenant, pipeline, project, branch, jobGraph]) + return ( + <> + {dot && <GraphViz dot={dot}/>} + </> + ) +} + +JobGraphDisplay.propTypes = { + fetchJobGraphIfNeeded: PropTypes.func, + tenant: PropTypes.object, + project: PropTypes.object.isRequired, + pipeline: PropTypes.string.isRequired, + branch: PropTypes.string.isRequired, + jobgraph: PropTypes.object, + dispatch: PropTypes.func, + state: PropTypes.object, +} +function mapStateToProps(state) { + return { + tenant: state.tenant, + jobgraph: state.jobgraph, + state: state, + } +} + +const mapDispatchToProps = { fetchJobGraphIfNeeded } + +export default connect(mapStateToProps, mapDispatchToProps)(JobGraphDisplay) diff --git a/web/src/containers/jobgraph/JobGraphToolbar.jsx b/web/src/containers/jobgraph/JobGraphToolbar.jsx new file mode 100644 index 000000000..0e293d56c --- /dev/null +++ b/web/src/containers/jobgraph/JobGraphToolbar.jsx @@ -0,0 +1,145 @@ +// Copyright 2020 BMW Group +// Copyright 2022 Acme Gating, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +import React, { useState } from 'react' +import PropTypes from 'prop-types' +import { + Button, + TextInput, + Dropdown, + DropdownItem, + DropdownPosition, + DropdownToggle, + Toolbar, + ToolbarContent, + ToolbarGroup, + ToolbarItem, + ToolbarToggleGroup, +} from '@patternfly/react-core' + + +function JobGraphToolbar(props) { + const pipelineSet = new Set() + props.project.configs.forEach((config) => { + config.pipelines.forEach((pipeline) => { + pipelineSet.add(pipeline.name) + }) + }) + const pipelines = Array.from(pipelineSet) + + const [isPipelineOpen, setIsPipelineOpen] = useState(false) + const [currentPipeline, setCurrentPipeline] = useState(props.defaultPipeline || pipelines[0]) + const [currentBranch, setCurrentBranch] = useState(props.defaultBranch || '') + + function handlePipelineSelect(event) { + setCurrentPipeline(event.target.innerText) + setIsPipelineOpen(false) + } + + function handlePipelineToggle(isOpen) { + setIsPipelineOpen(isOpen) + } + + function handleBranchChange(newValue) { + setCurrentBranch(newValue) + } + + function handleInputSend(event) { + // In case the event comes from a key press, only accept "Enter" + if (event.key && event.key !== 'Enter') { + return + } + + // Ignore empty values + if (!currentBranch) { + return + } + + // Clear the input field + setCurrentBranch('') + // Notify the parent component about the filter change + props.onChange(currentPipeline, currentBranch) + } + + function renderJobGraphToolbar () { + return <> + <Toolbar collapseListedFiltersBreakpoint="md"> + <ToolbarContent> + <ToolbarToggleGroup breakpoint="md"> + <ToolbarGroup variant="filter-group"> + + <ToolbarItem key="pipeline"> + <Dropdown + onSelect={handlePipelineSelect} + position={DropdownPosition.left} + toggle={ + <DropdownToggle + onToggle={handlePipelineToggle} + style={{ width: '100%' }} + > + Pipeline: {currentPipeline} + </DropdownToggle> + } + isOpen={isPipelineOpen} + dropdownItems={pipelines.map((pipeline) => ( + <DropdownItem key={pipeline}>{pipeline}</DropdownItem> + ))} + style={{ width: '100%' }} + menuAppendTo={document.body} + /> + </ToolbarItem> + + <ToolbarItem key="branch"> + <TextInput + name="branch" + id="branch-input" + type="search" + placeholder="Branch" + defaultValue={props.defaultBranch} + onChange={handleBranchChange} + onKeyDown={(event) => handleInputSend(event)} + /> + </ToolbarItem> + + <ToolbarItem key="button"> + <Button + onClick={(event) => handleInputSend(event)} + > + View Job Graph + </Button> + </ToolbarItem> + + </ToolbarGroup> + </ToolbarToggleGroup> + </ToolbarContent> + </Toolbar> + </> + } + + return ( + <div> + {renderJobGraphToolbar()} + </div> + ) +} + +JobGraphToolbar.propTypes = { + project: PropTypes.object.isRequired, + onChange: PropTypes.func.isRequired, + defaultBranch: PropTypes.string, + defaultPipeline: PropTypes.string, +} + +export default JobGraphToolbar diff --git a/web/src/containers/jobs/Jobs.jsx b/web/src/containers/jobs/Jobs.jsx index d7ab4bc69..71395f1d1 100644 --- a/web/src/containers/jobs/Jobs.jsx +++ b/web/src/containers/jobs/Jobs.jsx @@ -62,7 +62,7 @@ class JobsList extends React.Component { const createNode = (job, extra) => ({ text: ( <React.Fragment> - <Link to={linkPrefix + job.name}>{job.name}</Link> + <Link to={linkPrefix + encodeURIComponent(job.name)}>{job.name}</Link> {extra && (<span> ({extra})</span>)} {job.description && ( <span style={{marginLeft: '10px'}}>{job.description}</span> diff --git a/web/src/containers/project/Project.jsx b/web/src/containers/project/Project.jsx index c355b6af7..993d4d927 100644 --- a/web/src/containers/project/Project.jsx +++ b/web/src/containers/project/Project.jsx @@ -1,4 +1,5 @@ // Copyright 2018 Red Hat, Inc +// Copyright 2022 Acme Gating, LLC // // Licensed under the Apache License, Version 2.0 (the "License"); you may // not use this file except in compliance with the License. You may obtain @@ -12,68 +13,44 @@ // License for the specific language governing permissions and limitations // under the License. -import * as React from 'react' +import React, { useState } from 'react' import PropTypes from 'prop-types' import { - Nav, - NavItem, - TabContainer, - TabPane, - TabContent, -} from 'patternfly-react' + Tabs, + Tab, +} from '@patternfly/react-core' import ProjectVariant from './ProjectVariant' -class Project extends React.Component { - static propTypes = { - project: PropTypes.object.isRequired, - } - - state = { - variantIdx: 0, - } +function Project(props) { + const [variantIdx, setVariantIdx] = useState(0) + const { project } = props - renderVariantTitle (variant, selected) { - let title = variant.default_branch - if (selected) { - title = <strong>{title}</strong> - } + function renderVariantTitle (variant) { + let title = variant.source_context.project === project.name ? + variant.source_context.branch : variant.source_context.project return title } - render () { - const { project } = this.props - const { variantIdx } = this.state + return ( + <React.Fragment> + <Tabs activeKey={variantIdx} + onSelect={(event, tabIndex) => setVariantIdx(tabIndex)} + isBox> + {project.configs.map((variant, idx) => ( + <Tab key={idx} eventKey={idx} + title={renderVariantTitle(variant)}> + <ProjectVariant variant={variant} /> + </Tab> + ))} + </Tabs> + </React.Fragment> + ) +} - return ( - <React.Fragment> - <h2>{project.canonical_name}</h2> - <TabContainer id="zuul-project"> - <div> - <Nav bsClass="nav nav-tabs nav-tabs-pf"> - {project.configs.map((variant, idx) => ( - <NavItem - key={idx} - onClick={() => this.setState({variantIdx: idx})}> - <div> - {this.renderVariantTitle(variant, variantIdx === idx)} - </div> - </NavItem> - ))} - </Nav> - <TabContent> - <TabPane> - {project.configs[variantIdx] && ( - <ProjectVariant variant={project.configs[variantIdx]} /> - )} - </TabPane> - </TabContent> - </div> - </TabContainer> - </React.Fragment> - ) - } +Project.propTypes = { + project: PropTypes.object.isRequired, } export default Project diff --git a/web/src/containers/project/ProjectVariant.jsx b/web/src/containers/project/ProjectVariant.jsx index 74be10a55..51a093a32 100644 --- a/web/src/containers/project/ProjectVariant.jsx +++ b/web/src/containers/project/ProjectVariant.jsx @@ -18,64 +18,68 @@ import { connect } from 'react-redux' import { Link } from 'react-router-dom' -class ProjectVariant extends React.Component { - static propTypes = { - tenant: PropTypes.object, - variant: PropTypes.object.isRequired - } +function ProjectVariant(props) { + const { tenant, variant } = props + const rows = [] - render () { - const { tenant, variant } = this.props - const rows = [] + rows.push({label: 'Merge mode', value: variant.merge_mode}) - rows.push({label: 'Merge mode', value: variant.merge_mode}) + if (variant.templates.length > 0) { + const templateList = ( + <ul className='list-group'> + {variant.templates.map((item, idx) => ( + <li className='list-group-item' key={idx}>{item}</li>))} + </ul> + ) + rows.push({label: 'Templates', value: templateList}) + } - if (variant.templates.length > 0) { - const templateList = ( + variant.pipelines.forEach(pipeline => { + // TODO: either adds job link anchor to load the right variant + // and/or show the job variant config in a modal? + const jobList = ( + <React.Fragment> + {pipeline.queue_name && ( + <p><strong>Queue: </strong> {pipeline.queue_name} </p>)} <ul className='list-group'> - {variant.templates.map((item, idx) => ( - <li className='list-group-item' key={idx}>{item}</li>))} + {pipeline.jobs.map((item, idx) => ( + <li className='list-group-item' key={idx}> + <Link to={tenant.linkPrefix + '/job/' + item[0].name}> + {item[0].name} + </Link> + </li> + ))} </ul> - ) - rows.push({label: 'Templates', value: templateList}) - } + </React.Fragment> + ) + rows.push({label: pipeline.name + ' jobs', value: jobList}) + }) - variant.pipelines.forEach(pipeline => { - // TODO: either adds job link anchor to load the right variant - // and/or show the job variant config in a modal? - const jobList = ( - <React.Fragment> - {pipeline.queue_name && ( - <p><strong>Queue: </strong> {pipeline.queue_name} </p>)} - <ul className='list-group'> - {pipeline.jobs.map((item, idx) => ( - <li className='list-group-item' key={idx}> - <Link to={tenant.linkPrefix + '/job/' + item[0].name}> - {item[0].name} - </Link> - </li> - ))} - </ul> - </React.Fragment> - ) - rows.push({label: pipeline.name + ' jobs', value: jobList}) - }) + return ( + <div> + <table className='table table-striped table-bordered'> + <tbody> + {rows.map(item => ( + <tr key={item.label}> + <td style={{width: '10%'}}>{item.label}</td> + <td>{item.value}</td> + </tr> + ))} + </tbody> + </table> + </div> + ) +} - return ( - <div> - <table className='table table-striped table-bordered'> - <tbody> - {rows.map(item => ( - <tr key={item.label}> - <td style={{width: '10%'}}>{item.label}</td> - <td>{item.value}</td> - </tr> - ))} - </tbody> - </table> - </div> - ) +ProjectVariant.propTypes = { + tenant: PropTypes.object, + variant: PropTypes.object.isRequired +} + +function mapStateToProps(state) { + return { + tenant: state.tenant, } } -export default connect(state => ({tenant: state.tenant}))(ProjectVariant) +export default connect(mapStateToProps)(ProjectVariant) diff --git a/web/src/index.css b/web/src/index.css index eddbca673..e47cfc63c 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -3,6 +3,12 @@ body { padding: 0; } +/* Make the H2 header inline-block so that the refresh icon/button can + share space with it floating on the right. */ +h2 { + display: inline-block; +} + .pf-c-title { padding-bottom: 10px; } @@ -443,3 +449,9 @@ details.foldable[open] summary::before { ); } } + +/* The box size calculation compared to the text size seems off, but + this looks better */ +.zuul-job-graph text { + font-size: 12px; +} diff --git a/web/src/pages/FreezeJob.jsx b/web/src/pages/FreezeJob.jsx new file mode 100644 index 000000000..a78cc7b56 --- /dev/null +++ b/web/src/pages/FreezeJob.jsx @@ -0,0 +1,149 @@ +// Copyright 2022 Acme Gating, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +import React, { useEffect, useState } from 'react' +import PropTypes from 'prop-types' +import { connect } from 'react-redux' +import { useHistory, useLocation } from 'react-router-dom' +import { + PageSection, + PageSectionVariants, + Text, + TextContent, +} from '@patternfly/react-core' +import ReactJson from 'react-json-view' + +import FreezeJobToolbar from '../containers/freezejob/FreezeJobToolbar' +import { makeFreezeJobKey, fetchFreezeJobIfNeeded } from '../actions/freezejob' + +function FreezeJobPage(props) { + const { tenant, fetchFreezeJobIfNeeded } = props + + const [currentPipeline, setCurrentPipeline] = useState() + const [currentProject, setCurrentProject] = useState() + const [currentBranch, setCurrentBranch] = useState() + const [currentJob, setCurrentJob] = useState() + const history = useHistory() + const location = useLocation() + + if (!currentBranch) { + const urlParams = new URLSearchParams(location.search) + const pipeline = urlParams.get('pipeline') + const project = urlParams.get('project') + const branch = urlParams.get('branch') + const job = urlParams.get('job') + if (pipeline && branch && project && job) { + setCurrentPipeline(pipeline) + setCurrentProject(project) + setCurrentBranch(branch) + setCurrentJob(job) + } + } + + useEffect(() => { + document.title = 'Zuul Frozen Job' + if (currentPipeline && currentProject && currentBranch && currentJob) { + fetchFreezeJobIfNeeded(tenant, currentPipeline, currentProject, + currentBranch, currentJob) + } + }, [fetchFreezeJobIfNeeded, tenant, currentPipeline, currentProject, + currentBranch, currentJob]) + + function onChange(pipeline, project, branch, job) { + setCurrentPipeline(pipeline) + setCurrentProject(project) + setCurrentBranch(branch) + setCurrentJob(job) + + const searchParams = new URLSearchParams('') + searchParams.append('pipeline', pipeline) + searchParams.append('project', project) + searchParams.append('branch', branch) + searchParams.append('job', job) + history.push({ + pathname: location.pathname, + search: searchParams.toString(), + }) + + if (currentPipeline && currentProject && currentBranch && currentJob) { + fetchFreezeJobIfNeeded(tenant, currentPipeline, currentProject, + currentBranch, currentJob) + } + } + + const tenantJobs = props.freezejob.freezeJobs[tenant.name] + const freezeJobKey = makeFreezeJobKey(currentPipeline, + currentProject, + currentBranch, + currentJob) + const job = tenantJobs ? tenantJobs[freezeJobKey] : undefined + function renderFrozenJob() { + return ( + <span style={{whiteSpace: 'pre-wrap'}}> + <ReactJson + src={job} + name={null} + collapsed={false} + sortKeys={true} + enableClipboard={false} + displayDataTypes={false}/> + </span> + ) + } + + return ( + <> + <PageSection variant={PageSectionVariants.light}> + <TextContent> + <Text component="h1">Freeze Job</Text> + <Text component="p"> + Freezing a job asks Zuul to combine all the + <i>project</i> and <i>job</i> configuration + stanzas for a job as if a change for a given + project and branch were to be enqueued into + a specific pipeline. The resulting job + configuration is displayed below. + </Text> + </TextContent> + <FreezeJobToolbar + onChange={onChange} + defaultPipeline={currentPipeline} + defaultProject={currentProject} + defaultBranch={currentBranch} + defaultJob={currentJob} + /> + {job && renderFrozenJob(job)} + </PageSection> + </> + ) +} + +FreezeJobPage.propTypes = { + fetchFreezeJobIfNeeded: PropTypes.func, + tenant: PropTypes.object, + freezejob: PropTypes.object, +} + +function mapStateToProps(state) { + return { + tenant: state.tenant, + freezejob: state.freezejob, + } +} + +const mapDispatchToProps = { + fetchFreezeJobIfNeeded +} + +export default connect(mapStateToProps, mapDispatchToProps)(FreezeJobPage) diff --git a/web/src/pages/Project.jsx b/web/src/pages/Project.jsx index 8f76e9ff1..06e8612c7 100644 --- a/web/src/pages/Project.jsx +++ b/web/src/pages/Project.jsx @@ -1,4 +1,5 @@ // Copyright 2018 Red Hat, Inc +// Copyright 2022 Acme Gating, LLC // // Licensed under the Apache License, Version 2.0 (the "License"); you may // not use this file except in compliance with the License. You may obtain @@ -12,62 +13,74 @@ // License for the specific language governing permissions and limitations // under the License. -import * as React from 'react' +import React, { useEffect, useCallback } from 'react' import { connect } from 'react-redux' import PropTypes from 'prop-types' -import { PageSection, PageSectionVariants } from '@patternfly/react-core' - +import { + PageSection, + PageSectionVariants, + Text, + TextContent, +} from '@patternfly/react-core' import Project from '../containers/project/Project' +import JobGraph from '../containers/jobgraph/JobGraph' import { fetchProjectIfNeeded } from '../actions/project' import { Fetchable } from '../containers/Fetching' -class ProjectPage extends React.Component { - static propTypes = { - match: PropTypes.object.isRequired, - tenant: PropTypes.object, - remoteData: PropTypes.object, - dispatch: PropTypes.func - } - - updateData = (force) => { - this.props.dispatch(fetchProjectIfNeeded( - this.props.tenant, this.props.match.params.projectName, force)) - } +function ProjectPage(props) { + const { tenant, fetchProjectIfNeeded, remoteData } = props + const { projectName } = props.match.params + const tenantProjects = remoteData.projects[tenant.name] - componentDidMount () { - document.title = 'Zuul Project | ' + this.props.match.params.projectName - if (this.props.tenant.name) { - this.updateData() + const updateData = useCallback((force) => { + if (tenant.name) { + fetchProjectIfNeeded(tenant, projectName, force) } - } + }, [tenant, projectName, fetchProjectIfNeeded]) - componentDidUpdate (prevProps) { - if (this.props.tenant.name !== prevProps.tenant.name) { - this.updateData() - } - } + useEffect(() => { + document.title = 'Zuul Project | ' + projectName + updateData() + }, [tenant, projectName, updateData]) - render () { - const { remoteData } = this.props - const tenantProjects = remoteData.projects[this.props.tenant.name] - const projectName = this.props.match.params.projectName - return ( + return ( + <> <PageSection variant={PageSectionVariants.light}> - <PageSection style={{paddingRight: '5px'}}> - <Fetchable - isFetching={remoteData.isFetching} - fetchCallback={this.updateData} - /> - </PageSection> + <TextContent> + <Text component="h2">Project {projectName}</Text> + <Fetchable + isFetching={remoteData.isFetching} + fetchCallback={updateData} + /> + </TextContent> {tenantProjects && tenantProjects[projectName] && - <Project project={tenantProjects[projectName]} />} - </PageSection> - ) + <> + <Project project={tenantProjects[projectName]} /> + <JobGraph project={tenantProjects[projectName]} /> + </> + } + </PageSection> + </> + ) +} + +ProjectPage.propTypes = { + match: PropTypes.object.isRequired, + tenant: PropTypes.object, + remoteData: PropTypes.object, + fetchProjectIfNeeded: PropTypes.func, +} + +function mapStateToProps(state) { + return { + tenant: state.tenant, + remoteData: state.project, } } -export default connect(state => ({ - tenant: state.tenant, - remoteData: state.project, -}))(ProjectPage) +const mapDispatchToProps = { + fetchProjectIfNeeded, +} + +export default connect(mapStateToProps, mapDispatchToProps)(ProjectPage) diff --git a/web/src/reducers/freezejob.js b/web/src/reducers/freezejob.js new file mode 100644 index 000000000..6e7db4991 --- /dev/null +++ b/web/src/reducers/freezejob.js @@ -0,0 +1,55 @@ +// Copyright 2018 Red Hat, Inc +// Copyright 2022 Acme Gating, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +import { + FREEZE_JOB_FETCH_FAIL, + FREEZE_JOB_FETCH_REQUEST, + FREEZE_JOB_FETCH_SUCCESS +} from '../actions/freezejob' + +export default (state = { + isFetching: false, + freezeJobs: {}, +}, action) => { + let stateFreezeJobs + switch (action.type) { + case FREEZE_JOB_FETCH_REQUEST: + return { + isFetching: true, + freezeJobs: state.freezeJobs, + } + case FREEZE_JOB_FETCH_SUCCESS: + stateFreezeJobs = !state.freezeJobs[action.tenant] ? + { ...state.freezeJobs, [action.tenant]: {} } : + { ...state.freezeJobs } + return { + isFetching: false, + freezeJobs: { + ...stateFreezeJobs, + [action.tenant]: { + ...stateFreezeJobs[action.tenant], + [action.freezeJobKey]: action.freezeJob + } + } + } + case FREEZE_JOB_FETCH_FAIL: + return { + isFetching: false, + freezeJobs: state.freezeJobs, + } + default: + return state + } +} diff --git a/web/src/reducers/index.js b/web/src/reducers/index.js index 325e4f3dd..83c86c105 100644 --- a/web/src/reducers/index.js +++ b/web/src/reducers/index.js @@ -19,16 +19,19 @@ import autoholds from './autoholds' import configErrors from './configErrors' import change from './change' import component from './component' +import freezejob from './freezejob' import notifications from './notifications' import build from './build' import info from './info' import job from './job' +import jobgraph from './jobgraph' import jobs from './jobs' import labels from './labels' import logfile from './logfile' import nodes from './nodes' import openapi from './openapi' import project from './project' +import pipelines from './pipelines' import projects from './projects' import preferences from './preferences' import status from './status' @@ -44,14 +47,17 @@ const reducers = { change, component, configErrors, + freezejob, notifications, info, job, + jobgraph, jobs, labels, logfile, nodes, openapi, + pipelines, project, projects, status, diff --git a/web/src/reducers/jobgraph.js b/web/src/reducers/jobgraph.js new file mode 100644 index 000000000..0a015fb45 --- /dev/null +++ b/web/src/reducers/jobgraph.js @@ -0,0 +1,55 @@ +// Copyright 2018 Red Hat, Inc +// Copyright 2022 Acme Gating, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +import { + JOB_GRAPH_FETCH_FAIL, + JOB_GRAPH_FETCH_REQUEST, + JOB_GRAPH_FETCH_SUCCESS +} from '../actions/jobgraph' + +export default (state = { + isFetching: false, + jobGraphs: {}, +}, action) => { + let stateJobGraphs + switch (action.type) { + case JOB_GRAPH_FETCH_REQUEST: + return { + isFetching: true, + jobGraphs: state.jobGraphs, + } + case JOB_GRAPH_FETCH_SUCCESS: + stateJobGraphs = !state.jobGraphs[action.tenant] ? + { ...state.jobGraphs, [action.tenant]: {} } : + { ...state.jobGraphs } + return { + isFetching: false, + jobGraphs: { + ...stateJobGraphs, + [action.tenant]: { + ...stateJobGraphs[action.tenant], + [action.jobGraphKey]: action.jobGraph + } + } + } + case JOB_GRAPH_FETCH_FAIL: + return { + isFetching: false, + jobGraphs: state.jobGraphs, + } + default: + return state + } +} diff --git a/web/src/reducers/pipelines.js b/web/src/reducers/pipelines.js new file mode 100644 index 000000000..550352572 --- /dev/null +++ b/web/src/reducers/pipelines.js @@ -0,0 +1,45 @@ +// Copyright 2018 Red Hat, Inc +// Copyright 2022 Acme Gating, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +import { + PIPELINES_FETCH_FAIL, + PIPELINES_FETCH_REQUEST, + PIPELINES_FETCH_SUCCESS +} from '../actions/pipelines' + +export default (state = { + isFetching: false, + pipelines: {}, +}, action) => { + switch (action.type) { + case PIPELINES_FETCH_REQUEST: + return { + isFetching: true, + pipelines: state.pipelines, + } + case PIPELINES_FETCH_SUCCESS: + return { + isFetching: false, + pipelines: { ...state.pipelines, [action.tenant]: action.pipelines }, + } + case PIPELINES_FETCH_FAIL: + return { + isFetching: false, + pipelines: state.pipelines, + } + default: + return state + } +} diff --git a/web/src/routes.js b/web/src/routes.js index c2c3a8584..e1bfdae16 100644 --- a/web/src/routes.js +++ b/web/src/routes.js @@ -13,6 +13,7 @@ // under the License. import ComponentsPage from './pages/Components' +import FreezeJobPage from './pages/FreezeJob' import StatusPage from './pages/Status' import ChangeStatusPage from './pages/ChangeStatus' import ProjectPage from './pages/Project' @@ -79,6 +80,10 @@ const routes = () => [ component: BuildsetsPage }, { + to: '/freeze-job', + component: FreezeJobPage + }, + { to: '/status/change/:changeId', component: ChangeStatusPage }, diff --git a/web/yarn.lock b/web/yarn.lock index ff3dd3bf8..fc1675a0a 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -4711,24 +4711,52 @@ d3-color@1: resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-1.4.1.tgz#c52002bf8846ada4424d55d97982fef26eb3bc8a" integrity sha512-p2sTHSLCJI2QKunbGb7ocOh7DgTAn8IrLx21QRc/BSnodXM4sv6aLQlnfpvehFMLZEfBc6g9pH9SWQccFYfJ9Q== -d3-ease@^1.0.0: +d3-dispatch@1, d3-dispatch@^1.0.3: + version "1.0.6" + resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-1.0.6.tgz#00d37bcee4dd8cd97729dd893a0ac29caaba5d58" + integrity sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA== + +d3-drag@1: + version "1.2.5" + resolved "https://registry.yarnpkg.com/d3-drag/-/d3-drag-1.2.5.tgz#2537f451acd39d31406677b7dc77c82f7d988f70" + integrity sha512-rD1ohlkKQwMZYkQlYVCrSFxsWPzI97+W+PaEIBNTMxRuxz9RF0Hi5nJWHGVJ3Om9d2fRTe1yOBINJyy/ahV95w== + dependencies: + d3-dispatch "1" + d3-selection "1" + +d3-ease@1, d3-ease@^1.0.0: version "1.0.7" resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-1.0.7.tgz#9a834890ef8b8ae8c558b2fe55bd57f5993b85e2" integrity sha512-lx14ZPYkhNx0s/2HX5sLFUI3mbasHjSSpwO/KaaNACweVwxUruKyWVcb293wMv1RqTPZyZ8kSZ2NogUZNcLOFQ== -d3-format@1: +d3-format@1, d3-format@^1.2.0: version "1.4.5" resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-1.4.5.tgz#374f2ba1320e3717eb74a9356c67daee17a7edb4" integrity sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ== -d3-interpolate@1, d3-interpolate@^1.1.1: +d3-graphviz@2.6.1: + version "2.6.1" + resolved "https://registry.yarnpkg.com/d3-graphviz/-/d3-graphviz-2.6.1.tgz#61b93fe330e6339198fd2090f8080d7d4282c514" + integrity sha512-878AFSagQyr5tTOrM7YiVYeUC2/NoFcOB3/oew+LAML0xekyJSw9j3WOCUMBsc95KYe9XBYZ+SKKuObVya1tJQ== + dependencies: + d3-dispatch "^1.0.3" + d3-format "^1.2.0" + d3-interpolate "^1.1.5" + d3-path "^1.0.5" + d3-selection "^1.1.0" + d3-timer "^1.0.6" + d3-transition "^1.1.1" + d3-zoom "^1.5.0" + viz.js "^1.8.2" + +d3-interpolate@1, d3-interpolate@^1.1.1, d3-interpolate@^1.1.5: version "1.4.0" resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-1.4.0.tgz#526e79e2d80daa383f9e0c1c1c7dcc0f0583e987" integrity sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA== dependencies: d3-color "1" -d3-path@1: +d3-path@1, d3-path@^1.0.5: version "1.0.9" resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-1.0.9.tgz#48c050bb1fe8c262493a8caf5524e3e9591701cf" integrity sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg== @@ -4746,6 +4774,11 @@ d3-scale@^1.0.0: d3-time "1" d3-time-format "2" +d3-selection@1, d3-selection@^1.1.0: + version "1.4.2" + resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-1.4.2.tgz#dcaa49522c0dbf32d6c1858afc26b6094555bc5c" + integrity sha512-SJ0BqYihzOjDnnlfyeHT0e30k0K1+5sR3d5fNueCNeuhZTnGw4M4o8mqJchSwgKMXCNFo+e2VTChiSJ0vYtXkg== + d3-shape@^1.0.0, d3-shape@^1.2.0: version "1.3.7" resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-1.3.7.tgz#df63801be07bc986bc54f63789b4fe502992b5d7" @@ -4765,11 +4798,34 @@ d3-time@1: resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-1.1.0.tgz#b1e19d307dae9c900b7e5b25ffc5dcc249a8a0f1" integrity sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA== -d3-timer@^1.0.0: +d3-timer@1, d3-timer@^1.0.0, d3-timer@^1.0.6: version "1.0.10" resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-1.0.10.tgz#dfe76b8a91748831b13b6d9c793ffbd508dd9de5" integrity sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw== +d3-transition@1, d3-transition@^1.1.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-1.3.2.tgz#a98ef2151be8d8600543434c1ca80140ae23b398" + integrity sha512-sc0gRU4PFqZ47lPVHloMn9tlPcv8jxgOQg+0zjhfZXMQuvppjG6YuwdMBE0TuqCZjeJkLecku/l9R0JPcRhaDA== + dependencies: + d3-color "1" + d3-dispatch "1" + d3-ease "1" + d3-interpolate "1" + d3-selection "^1.1.0" + d3-timer "1" + +d3-zoom@^1.5.0: + version "1.8.3" + resolved "https://registry.yarnpkg.com/d3-zoom/-/d3-zoom-1.8.3.tgz#b6a3dbe738c7763121cd05b8a7795ffe17f4fc0a" + integrity sha512-VoLXTK4wvy1a0JpH2Il+F2CiOhVu7VRXWF5M/LroMIh3/zBAC3WAt7QoIvPibOavVo20hN6/37vwAsdBejLyKQ== + dependencies: + d3-dispatch "1" + d3-drag "1" + d3-interpolate "1" + d3-selection "1" + d3-transition "1" + d3@~3.5.0, d3@~3.5.17: version "3.5.17" resolved "https://registry.yarnpkg.com/d3/-/d3-3.5.17.tgz#bc46748004378b21a360c9fc7cf5231790762fb8" @@ -15208,6 +15264,11 @@ victory-zoom-container@^36.2.1, victory-zoom-container@^36.3.0: prop-types "^15.5.8" victory-core "^36.3.0" +viz.js@^1.8.2: + version "1.8.2" + resolved "https://registry.yarnpkg.com/viz.js/-/viz.js-1.8.2.tgz#d9cc04cd99f98ec986bf9054db76a6cbcdc5d97a" + integrity sha512-W+1+N/hdzLpQZEcvz79n2IgUE9pfx6JLdHh3Kh8RGvLL8P1LdJVQmi2OsDcLdY4QVID4OUy+FPelyerX0nJxIQ== + vm-browserify@^1.0.1: version "1.1.2" resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" diff --git a/zuul/ansible/2.8/library/command.py b/zuul/ansible/2.8/library/command.py index 26830e4a7..5f7f31947 100755 --- a/zuul/ansible/2.8/library/command.py +++ b/zuul/ansible/2.8/library/command.py @@ -168,7 +168,15 @@ _log_lines = [] class Console(object): def __init__(self, log_uuid): - self.logfile_name = LOG_STREAM_FILE.format(log_uuid=log_uuid) + # The streamer currently will not ask us for output from + # loops. This flag uuid was set in the action plugin if this + # call was part of a loop. This avoids us leaving behind + # files that will never be read, but also means no other + # special-casing for any of this path. + if log_uuid == 'in-loop-ignore': + self.logfile_name = os.devnull + else: + self.logfile_name = LOG_STREAM_FILE.format(log_uuid=log_uuid) def __enter__(self): self.logfile = open(self.logfile_name, 'ab', buffering=0) diff --git a/zuul/ansible/2.9/library/command.py b/zuul/ansible/2.9/library/command.py index 26830e4a7..5f7f31947 100755 --- a/zuul/ansible/2.9/library/command.py +++ b/zuul/ansible/2.9/library/command.py @@ -168,7 +168,15 @@ _log_lines = [] class Console(object): def __init__(self, log_uuid): - self.logfile_name = LOG_STREAM_FILE.format(log_uuid=log_uuid) + # The streamer currently will not ask us for output from + # loops. This flag uuid was set in the action plugin if this + # call was part of a loop. This avoids us leaving behind + # files that will never be read, but also means no other + # special-casing for any of this path. + if log_uuid == 'in-loop-ignore': + self.logfile_name = os.devnull + else: + self.logfile_name = LOG_STREAM_FILE.format(log_uuid=log_uuid) def __enter__(self): self.logfile = open(self.logfile_name, 'ab', buffering=0) diff --git a/zuul/ansible/base/action/command.py b/zuul/ansible/base/action/command.py index f36da86f6..3bb88d8e5 100644 --- a/zuul/ansible/base/action/command.py +++ b/zuul/ansible/base/action/command.py @@ -25,15 +25,21 @@ class ActionModule(command.ActionModule): if self._task.action in ( 'command', 'shell', 'ansible.builtin.command', 'ansible.builtin.shell'): - # Get a unique key for ZUUL_LOG_ID_MAP. ZUUL_LOG_ID_MAP - # is read-only since we are forked. Use it to add a - # counter to the log id so that if we run the same task - # more than once, we get a unique log file. See comments - # in paths.py for details. - log_host = paths._sanitize_filename( - task_vars.get('inventory_hostname')) - key = "%s-%s" % (self._task._uuid, log_host) - count = paths.ZUUL_LOG_ID_MAP.get(key, 0) - self._task.args['zuul_log_id'] = "%s-%s-%s" % ( - self._task._uuid, count, log_host) + # This is a bit lame, but we do not log loops in the + # zuul_stream.py callback. This allows us to not write + # out command.py output to files that will never be read. + if 'ansible_loop_var' in task_vars: + self._task.args['zuul_log_id'] = 'in-loop-ignore' + else: + # Get a unique key for ZUUL_LOG_ID_MAP. ZUUL_LOG_ID_MAP + # is read-only since we are forked. Use it to add a + # counter to the log id so that if we run the same task + # more than once, we get a unique log file. See comments + # in paths.py for details. + log_host = paths._sanitize_filename( + task_vars.get('inventory_hostname')) + key = "%s-%s" % (self._task._uuid, log_host) + count = paths.ZUUL_LOG_ID_MAP.get(key, 0) + self._task.args['zuul_log_id'] = "%s-%s-%s" % ( + self._task._uuid, count, log_host) return super(ActionModule, self).run(tmp, task_vars) diff --git a/zuul/ansible/base/callback/zuul_stream.py b/zuul/ansible/base/callback/zuul_stream.py index 720261cb2..f31983ed6 100644 --- a/zuul/ansible/base/callback/zuul_stream.py +++ b/zuul/ansible/base/callback/zuul_stream.py @@ -48,6 +48,7 @@ from zuul.ansible import paths from zuul.ansible import logconfig LOG_STREAM_PORT = int(os.environ.get("ZUUL_CONSOLE_PORT", 19885)) +LOG_STREAM_VERSION = 0 def zuul_filter_result(result): @@ -103,6 +104,7 @@ class CallbackModule(default.CallbackModule): self._items_done = False self._deferred_result = None self._playbook_name = None + self._zuul_console_version = 0 def configure_logger(self): # ansible appends timestamp, user and pid to the log lines emitted @@ -129,9 +131,7 @@ class CallbackModule(default.CallbackModule): else: self._display.display(msg) - def _read_log(self, host, ip, port, log_id, task_name, hosts): - self._log("[%s] Starting to log %s for task %s" - % (host, log_id, task_name), job=False, executor=True) + def _read_log_connect(self, host, ip, port): logger_retries = 0 while True: try: @@ -141,6 +141,7 @@ class CallbackModule(default.CallbackModule): # logs continously. Without this we can easily trip the 5 # second timeout. s.settimeout(None) + return s except socket.timeout: self._log( "Timeout exception waiting for the logger. " @@ -151,7 +152,7 @@ class CallbackModule(default.CallbackModule): "Timeout exception waiting for the logger. " "Please check connectivity to [%s:%s]" % (ip, port)) - return + return None except Exception: if logger_retries % 10 == 0: self._log("[%s] Waiting on logger" % host, @@ -159,31 +160,77 @@ class CallbackModule(default.CallbackModule): logger_retries += 1 time.sleep(0.1) continue - msg = "%s\n" % log_id - s.send(msg.encode("utf-8")) - buff = s.recv(4096) - buffering = True - while buffering: - if b'\n' in buff: - (line, buff) = buff.split(b'\n', 1) - # We can potentially get binary data here. In order to - # being able to handle that use the backslashreplace - # error handling method. This decodes unknown utf-8 - # code points to escape sequences which exactly represent - # the correct data without throwing a decoding exception. - done = self._log_streamline( - host, line.decode("utf-8", "backslashreplace")) - if done: - return + + def _read_log(self, host, ip, port, log_id, task_name, hosts): + self._log("[%s] Starting to log %s for task %s" + % (host, log_id, task_name), job=False, executor=True) + + s = self._read_log_connect(host, ip, port) + if s is None: + # Can't connect; _read_log_connect() already logged an + # error for us, just bail + return + + # Find out what version we are running against + s.send(f'v:{LOG_STREAM_VERSION}\n'.encode('utf-8')) + buff = s.recv(1024).decode('utf-8').strip() + + # NOTE(ianw) 2022-07-21 : zuul_console from < 6.3.0 do not + # understand this protocol. They will assume the send + # above is a log request and send back the not found + # message in a loop. So to handle this we disconnect and + # reconnect. When we decide to remove this, we can remove + # anything in the "version 0" path. + if buff == '[Zuul] Log not found': + s.close() + s = self._read_log_connect(host, ip, port) + if s is None: + return + else: + self._zuul_console_version = int(buff) + self._log('[%s] Reports streaming version: %d' % + (host, self._zuul_console_version), + job=False, executor=True) + + if self._zuul_console_version >= 1: + msg = 's:%s\n' % log_id + else: + msg = '%s\n' % log_id + + s.send(msg.encode("utf-8")) + buff = s.recv(4096) + buffering = True + while buffering: + if b'\n' in buff: + (line, buff) = buff.split(b'\n', 1) + # We can potentially get binary data here. In order to + # being able to handle that use the backslashreplace + # error handling method. This decodes unknown utf-8 + # code points to escape sequences which exactly represent + # the correct data without throwing a decoding exception. + done = self._log_streamline( + host, line.decode("utf-8", "backslashreplace")) + if done: + if self._zuul_console_version > 0: + try: + # reestablish connection and tell console to + # clean up + s = self._read_log_connect(host, ip, port) + s.send(f'f:{log_id}\n'.encode('utf-8')) + s.close() + except Exception: + # Don't worry if this fails + pass + return + else: + more = s.recv(4096) + if not more: + buffering = False else: - more = s.recv(4096) - if not more: - buffering = False - else: - buff += more - if buff: - self._log_streamline( - host, buff.decode("utf-8", "backslashreplace")) + buff += more + if buff: + self._log_streamline( + host, buff.decode("utf-8", "backslashreplace")) def _log_streamline(self, host, line): if "[Zuul] Task exit code" in line: diff --git a/zuul/ansible/base/library/command.py b/zuul/ansible/base/library/command.py index d496b037c..3c22849ae 100755 --- a/zuul/ansible/base/library/command.py +++ b/zuul/ansible/base/library/command.py @@ -263,7 +263,15 @@ _log_lines = [] class Console(object): def __init__(self, log_uuid): - self.logfile_name = LOG_STREAM_FILE.format(log_uuid=log_uuid) + # The streamer currently will not ask us for output from + # loops. This flag uuid was set in the action plugin if this + # call was part of a loop. This avoids us leaving behind + # files that will never be read, but also means no other + # special-casing for any of this path. + if log_uuid == 'in-loop-ignore': + self.logfile_name = os.devnull + else: + self.logfile_name = LOG_STREAM_FILE.format(log_uuid=log_uuid) def __enter__(self): self.logfile = open(self.logfile_name, 'ab', buffering=0) diff --git a/zuul/ansible/base/library/zuul_console.py b/zuul/ansible/base/library/zuul_console.py index 9dffbbc3a..aa999cac1 100755 --- a/zuul/ansible/base/library/zuul_console.py +++ b/zuul/ansible/base/library/zuul_console.py @@ -24,6 +24,14 @@ import subprocess import threading import time +# This is the version we report to the zuul_stream callback. It is +# expected that this (zuul_console) process can be long-lived, so if +# there are updates this ensures a later streaming callback can still +# talk to us. +ZUUL_CONSOLE_PROTO_VERSION = 1 +# This is the template for the file name of the log-file written out +# by the command.py override command in the executor's Ansible +# install. LOG_STREAM_FILE = '/tmp/console-{log_uuid}.log' LOG_STREAM_PORT = 19885 @@ -162,15 +170,49 @@ class Server(object): ret = buffer.decode('utf-8') x = ret.find('\n') if x > 0: - return ret[:x] + return ret[:x].strip() except UnicodeDecodeError: pass - def handleOneConnection(self, conn): - log_uuid = self.get_command(conn) + def _clean_uuid(self, log_uuid): # use path split to make use the input isn't trying to be clever # and construct some path like /tmp/console-/../../something - log_uuid = os.path.split(log_uuid.rstrip())[-1] + return os.path.split(log_uuid)[-1] + + def handleOneConnection(self, conn): + # V1 protocol + # ----------- + # v:<ver> get version number, <ver> is remote version + # s:<uuid> send logs for <uuid> + # f:<uuid> finalise/cleanup <uuid> + while True: + command = self.get_command(conn) + if command.startswith('v:'): + # NOTE(ianw) : remote sends its version. We currently + # don't have anything to do with this value, so ignore + # for now. + cmd = '%s\n' % (ZUUL_CONSOLE_PROTO_VERSION) + conn.send(cmd.encode('utf-8')) + continue + elif command.startswith('f:'): + log_uuid = self._clean_uuid(command[2:]) + try: + os.unlink(self.path.format(log_uuid=log_uuid)) + except Exception: + # something might have cleaned /tmp + pass + continue + elif command.startswith('s:'): + log_uuid = self._clean_uuid(command[2:]) + break + else: + # NOTE(ianw): 2022-07-21 In releases < 6.3.0 the streaming + # side would just send a raw uuid and nothing else; so by + # default assume that is what is coming in here. We can + # remove this fallback when we decide it is no longer + # necessary. + log_uuid = self._clean_uuid(command) + break # FIXME: this won't notice disconnects until it tries to send console = None diff --git a/zuul/driver/gerrit/gerritconnection.py b/zuul/driver/gerrit/gerritconnection.py index 1ec334915..2213293f8 100644 --- a/zuul/driver/gerrit/gerritconnection.py +++ b/zuul/driver/gerrit/gerritconnection.py @@ -103,10 +103,12 @@ class GerritChangeData(object): SSH = 1 HTTP = 2 - def __init__(self, fmt, data, related=None, files=None): + def __init__(self, fmt, data, related=None, files=None, + zuul_query_ltime=None): self.format = fmt self.data = data self.files = files + self.zuul_query_ltime = zuul_query_ltime if fmt == self.SSH: self.parseSSH(data) @@ -329,19 +331,20 @@ class GerritEventConnector(threading.Thread): self.connection.clearConnectionCacheOnBranchEvent(event) - self._getChange(event) + self._getChange(event, connection_event.zuul_event_ltime) self.connection.logEvent(event) self.connection.sched.addTriggerEvent( self.connection.driver_name, event ) - def _getChange(self, event): + def _getChange(self, event, connection_event_ltime): # Grab the change if we are managing the project or if it exists in the # cache as it may be a dependency if event.change_number: refresh = True change_key = self.connection.source.getChangeKey(event) - if self.connection._change_cache.get(change_key) is None: + change = self.connection._change_cache.get(change_key) + if change is None: refresh = False for tenant in self.connection.sched.abide.tenants.values(): # TODO(fungi): it would be better to have some simple means @@ -353,6 +356,13 @@ class GerritEventConnector(threading.Thread): event.project_name))): refresh = True break + else: + # We have a cache entry for this change Get the + # query ltime for the cache entry; if it's after the + # event ltime, we don't need to refresh. + if (change.zuul_query_ltime and + change.zuul_query_ltime > connection_event_ltime): + refresh = False if refresh: # Call _getChange for the side effect of updating the @@ -1418,15 +1428,20 @@ class GerritConnection(ZKChangeCacheMixin, ZKBranchCacheMixin, BaseConnection): def queryChange(self, number, event=None): for attempt in range(3): + # Get a query ltime -- any events before this point should be + # included in our change data. + zuul_query_ltime = self.sched.zk_client.getCurrentLtime() try: if self.session: data, related, files = self.queryChangeHTTP( number, event=event) return GerritChangeData(GerritChangeData.HTTP, - data, related, files) + data, related, files, + zuul_query_ltime=zuul_query_ltime) else: data = self.queryChangeSSH(number, event=event) - return GerritChangeData(GerritChangeData.SSH, data) + return GerritChangeData(GerritChangeData.SSH, data, + zuul_query_ltime=zuul_query_ltime) except Exception: if attempt >= 3: raise diff --git a/zuul/driver/gerrit/gerritmodel.py b/zuul/driver/gerrit/gerritmodel.py index f0ec32f77..0ac3e7f9d 100644 --- a/zuul/driver/gerrit/gerritmodel.py +++ b/zuul/driver/gerrit/gerritmodel.py @@ -35,8 +35,10 @@ class GerritChange(Change): self.approvals = [] self.missing_labels = set() self.commit = None + self.zuul_query_ltime = None def update(self, data, connection): + self.zuul_query_ltime = data.zuul_query_ltime if data.format == data.SSH: self.updateFromSSH(data.data, connection) else: @@ -51,6 +53,7 @@ class GerritChange(Change): "approvals": self.approvals, "missing_labels": list(self.missing_labels), "commit": self.commit, + "zuul_query_ltime": self.zuul_query_ltime, }) return d @@ -62,6 +65,7 @@ class GerritChange(Change): self.approvals = data["approvals"] self.missing_labels = set(data["missing_labels"]) self.commit = data.get("commit") + self.zuul_query_ltime = data.get("zuul_query_ltime") def updateFromSSH(self, data, connection): if self.patchset is None: diff --git a/zuul/executor/server.py b/zuul/executor/server.py index eac7fa7e5..e00612e9e 100644 --- a/zuul/executor/server.py +++ b/zuul/executor/server.py @@ -3157,6 +3157,10 @@ class ExecutorServer(BaseMergeServer): self.allow_unzoned = get_default(self.config, 'executor', 'allow_unzoned', False) + # If this executor has no zone configured it is implicitly unzoned + if self.zone is None: + self.allow_unzoned = True + # Those attributes won't change, so it's enough to set them once on the # component info. self.component_info.zone = self.zone diff --git a/zuul/merger/merger.py b/zuul/merger/merger.py index c3e20e21b..34b495fcc 100644 --- a/zuul/merger/merger.py +++ b/zuul/merger/merger.py @@ -733,10 +733,8 @@ class Repo(object): return log = get_annotated_logger(self.log, zuul_event_id) log.debug("Set remote url to %s", redact_url(url)) + self._git_set_remote_url(self.createRepoObject(zuul_event_id), url) self.remote_url = url - self._git_set_remote_url( - self.createRepoObject(zuul_event_id), - self.remote_url) def mapLine(self, commit, filename, lineno, zuul_event_id=None): repo = self.createRepoObject(zuul_event_id) @@ -1244,11 +1242,13 @@ class Merger(object): item['connection'], item['project'], repo_state, item['ref'], item['newrev']) item = items[-1] - repo = self.getRepo(item['connection'], item['project']) # A list of branch names the last item appears in. item_in_branches = [] if item.get('newrev'): - item_in_branches = repo.contains(item['newrev']) + lock = repo_locks.getRepoLock(item['connection'], item['project']) + with lock: + repo = self.getRepo(item['connection'], item['project']) + item_in_branches = repo.contains(item['newrev']) return (True, repo_state, item_in_branches) def getFiles(self, connection_name, project_name, branch, files, dirs=[]): diff --git a/zuul/scheduler.py b/zuul/scheduler.py index 272235757..dfc922cf1 100644 --- a/zuul/scheduler.py +++ b/zuul/scheduler.py @@ -438,12 +438,12 @@ class Scheduler(threading.Thread): mergers_online = 0 for executor_component in self.component_registry.all("executor"): - if executor_component.allow_unzoned or not executor_component.zone: + if executor_component.allow_unzoned: if executor_component.state == BaseComponent.RUNNING: executors_unzoned_online += 1 if executor_component.accepting_work: executors_unzoned_accepting += 1 - else: + if executor_component.zone: zone_stats = zoned_executor_stats.setdefault( executor_component.zone, executor_stats_default.copy()) diff --git a/zuul/web/__init__.py b/zuul/web/__init__.py index f06dd0b52..644b82bec 100755 --- a/zuul/web/__init__.py +++ b/zuul/web/__init__.py @@ -32,6 +32,7 @@ import ssl import threading import uuid import prometheus_client +import urllib.parse import zuul.executor.common from zuul import exceptions @@ -1170,6 +1171,7 @@ class ZuulWebAPI(object): @cherrypy.tools.json_out( content_type='application/json; charset=utf-8', handler=json_handler) def job(self, tenant_name, job_name): + job_name = urllib.parse.unquote_plus(job_name) tenant = self._getTenantOrRaise(tenant_name) job_variants = tenant.layout.jobs.get(job_name) result = [] diff --git a/zuul/zk/event_queues.py b/zuul/zk/event_queues.py index 52ffd582e..ad7529791 100644 --- a/zuul/zk/event_queues.py +++ b/zuul/zk/event_queues.py @@ -909,7 +909,7 @@ class ConnectionEventQueue(ZooKeeperEventQueue): self._put({'event_data': data}) def __iter__(self): - for data, ack_ref, _ in self._iterEvents(): + for data, ack_ref, zstat in self._iterEvents(): if not data: self.log.warning("Malformed event found: %s", data) self._remove(ack_ref.path) @@ -918,6 +918,7 @@ class ConnectionEventQueue(ZooKeeperEventQueue): event = model.ConnectionEvent.fromDict( data.get('event_data', data)) event.ack_ref = ack_ref + event.zuul_event_ltime = zstat.creation_transaction_id yield event |