From e7e19384396558ff1eb8af57ff1c0ebf02a39038 Mon Sep 17 00:00:00 2001 From: Giuseppe Graziano Date: Thu, 5 Sep 2024 17:38:38 +0200 Subject: [PATCH 1/2] Initial tests for admin-client Closes #31869 Signed-off-by: Giuseppe Graziano --- admin-client/pom.xml | 30 + .../resource/IdentityProvidersResource.java | 10 +- .../resource/OrganizationMembersResource.java | 10 + .../OrganizationsMembersResource.java | 35 + .../resource/OrganizationsResource.java | 3 + .../constants/ServiceAccountConstants.java | 38 + .../common/crypto/CryptoConstants.java | 27 + .../java/org/keycloak/crypto/Algorithm.java | 57 + .../java/org/keycloak/events/EventType.java | 216 +++ .../org/keycloak/models/AccountRoles.java | 36 + .../java/org/keycloak/models/Constants.java | 190 ++ .../idm/IdentityProviderRepresentation.java | 24 +- .../idm/UserRepresentation.java | 13 +- .../java/org/keycloak/utils/StringUtil.java | 110 ++ .../testsuite/AbstractAdminClientTest.java | 116 ++ .../client/testsuite/AdminClientTest.java | 199 ++ .../org/keycloak/client/testsuite/Assert.java | 96 + .../keycloak/client/testsuite/ClientTest.java | 870 +++++++++ .../client/testsuite/RealmRolesTest.java | 533 ++++++ .../keycloak/client/testsuite/RealmTest.java | 926 ++++++++++ .../test/resources/admin-test/testrealm.json | 105 ++ .../client-descriptions/client-oidc.json | 6 + .../saml-entity-descriptor.xml | 99 + .../import/import-without-clients.json | 660 +++++++ .../import/import-without-roles.json | 1257 +++++++++++++ .../partial-authentication-flows-import.json | 23 + .../test/resources/import/partial-import.json | 637 +++++++ .../import/sample-authz-partial-import.json | 58 + .../testrealm-authenticator-config-null.json | 25 + .../import/testrealm-keycloak-6146-error.json | 265 +++ .../import/testrealm-keycloak-6146.json | 266 +++ .../import/testrealm-user-null-attr.json | 1617 +++++++++++++++++ .../src/test/resources/testrealm.json | 694 +++++++ .../client/testsuite/common/Creator.java | 192 ++ .../client/testsuite/common/OAuthClient.java | 24 +- .../KeycloakClientTestExtension.java | 51 + .../testsuite/framework/KeycloakVersion.java | 16 + .../client/testsuite/model/CibaConfig.java | 54 + .../testsuite/model/OAuth2DeviceConfig.java | 45 + .../client/testsuite/model/ParConfig.java | 32 + .../testsuite/model/RealmAttributes.java | 60 + .../keycloak/testsuite/client/Keycloak.java | 188 ++ .../testsuite/util/AdminClientUtil.java | 135 ++ .../org/keycloak/testsuite/util/ApiUtil.java | 321 ++++ .../testsuite/util/ClientScopeBuilder.java | 59 + .../testsuite/util/CredentialBuilder.java | 46 + .../testsuite/util/KeycloakModelUtils.java | 48 + .../util/MailServerConfiguration.java | 30 + .../keycloak/testsuite/util/MediaType.java | 47 + .../keycloak/testsuite/util/RealmBuilder.java | 16 +- .../keycloak/testsuite/util/ServerURLs.java | 24 +- 51 files changed, 10614 insertions(+), 25 deletions(-) create mode 100644 admin-client/src/main/java/org/keycloak/admin/client/resource/OrganizationsMembersResource.java create mode 100644 admin-client/src/main/java/org/keycloak/common/constants/ServiceAccountConstants.java create mode 100644 admin-client/src/main/java/org/keycloak/common/crypto/CryptoConstants.java create mode 100755 admin-client/src/main/java/org/keycloak/crypto/Algorithm.java create mode 100755 admin-client/src/main/java/org/keycloak/events/EventType.java create mode 100644 admin-client/src/main/java/org/keycloak/models/AccountRoles.java create mode 100755 admin-client/src/main/java/org/keycloak/models/Constants.java create mode 100644 admin-client/src/main/java/org/keycloak/utils/StringUtil.java create mode 100644 testsuite/admin-client-tests/src/test/java/org/keycloak/client/testsuite/AbstractAdminClientTest.java create mode 100644 testsuite/admin-client-tests/src/test/java/org/keycloak/client/testsuite/AdminClientTest.java create mode 100644 testsuite/admin-client-tests/src/test/java/org/keycloak/client/testsuite/Assert.java create mode 100644 testsuite/admin-client-tests/src/test/java/org/keycloak/client/testsuite/ClientTest.java create mode 100644 testsuite/admin-client-tests/src/test/java/org/keycloak/client/testsuite/RealmRolesTest.java create mode 100644 testsuite/admin-client-tests/src/test/java/org/keycloak/client/testsuite/RealmTest.java create mode 100644 testsuite/admin-client-tests/src/test/resources/admin-test/testrealm.json create mode 100644 testsuite/admin-client-tests/src/test/resources/client-descriptions/client-oidc.json create mode 100644 testsuite/admin-client-tests/src/test/resources/client-descriptions/saml-entity-descriptor.xml create mode 100644 testsuite/admin-client-tests/src/test/resources/import/import-without-clients.json create mode 100644 testsuite/admin-client-tests/src/test/resources/import/import-without-roles.json create mode 100644 testsuite/admin-client-tests/src/test/resources/import/partial-authentication-flows-import.json create mode 100644 testsuite/admin-client-tests/src/test/resources/import/partial-import.json create mode 100644 testsuite/admin-client-tests/src/test/resources/import/sample-authz-partial-import.json create mode 100644 testsuite/admin-client-tests/src/test/resources/import/testrealm-authenticator-config-null.json create mode 100644 testsuite/admin-client-tests/src/test/resources/import/testrealm-keycloak-6146-error.json create mode 100644 testsuite/admin-client-tests/src/test/resources/import/testrealm-keycloak-6146.json create mode 100644 testsuite/admin-client-tests/src/test/resources/import/testrealm-user-null-attr.json create mode 100644 testsuite/admin-client-tests/src/test/resources/testrealm.json create mode 100644 testsuite/framework/src/main/java/org/keycloak/client/testsuite/common/Creator.java create mode 100644 testsuite/framework/src/main/java/org/keycloak/client/testsuite/framework/KeycloakVersion.java create mode 100644 testsuite/framework/src/main/java/org/keycloak/client/testsuite/model/CibaConfig.java create mode 100644 testsuite/framework/src/main/java/org/keycloak/client/testsuite/model/OAuth2DeviceConfig.java create mode 100644 testsuite/framework/src/main/java/org/keycloak/client/testsuite/model/ParConfig.java create mode 100644 testsuite/framework/src/main/java/org/keycloak/client/testsuite/model/RealmAttributes.java create mode 100644 testsuite/framework/src/main/java/org/keycloak/testsuite/client/Keycloak.java create mode 100644 testsuite/framework/src/main/java/org/keycloak/testsuite/util/AdminClientUtil.java create mode 100644 testsuite/framework/src/main/java/org/keycloak/testsuite/util/ApiUtil.java create mode 100644 testsuite/framework/src/main/java/org/keycloak/testsuite/util/ClientScopeBuilder.java create mode 100644 testsuite/framework/src/main/java/org/keycloak/testsuite/util/CredentialBuilder.java create mode 100644 testsuite/framework/src/main/java/org/keycloak/testsuite/util/KeycloakModelUtils.java create mode 100644 testsuite/framework/src/main/java/org/keycloak/testsuite/util/MailServerConfiguration.java create mode 100644 testsuite/framework/src/main/java/org/keycloak/testsuite/util/MediaType.java diff --git a/admin-client/pom.xml b/admin-client/pom.xml index 8c907d9..34a68c3 100755 --- a/admin-client/pom.xml +++ b/admin-client/pom.xml @@ -116,6 +116,7 @@ org/keycloak/Token.java, org/keycloak/TokenIdGenerator.java, org/keycloak/crypto/KeyUse.java, + org/keycloak/crypto/Algorithm.java, org/keycloak/json/*.java, org/keycloak/representations/adapters/action/GlobalRequestResult.java, org/keycloak/representations/idm/**/*.java, @@ -131,6 +132,7 @@ org/keycloak/util/EnumWithStableIndex.java, org/keycloak/util/JsonSerialization.java, org/keycloak/util/SystemPropertiesJsonParserFactory.java, + org/keycloak/util/Time.java, @@ -150,6 +152,34 @@ org/keycloak/common/util/StringPropertyReplacer.java, org/keycloak/common/util/SystemEnvProperties.java, org/keycloak/common/util/Time.java, + org/keycloak/common/crypto/CryptoConstants.java, + org/keycloak/common/constants/ServiceAccountConstants.java, + + + + org.keycloak + keycloak-server-spi + ${keycloak.version} + jar + sources + true + ${project.build.directory}/unpacked + + org/keycloak/utils/StringUtil.java, + + + + org.keycloak + keycloak-server-spi-private + ${keycloak.version} + jar + sources + true + ${project.build.directory}/unpacked + + org/keycloak/models/Constants.java, + org/keycloak/models/AccountRoles.java, + org/keycloak/events/EventType.java, diff --git a/admin-client/src/main/java/org/keycloak/admin/client/resource/IdentityProvidersResource.java b/admin-client/src/main/java/org/keycloak/admin/client/resource/IdentityProvidersResource.java index 4edf835..0cee01e 100644 --- a/admin-client/src/main/java/org/keycloak/admin/client/resource/IdentityProvidersResource.java +++ b/admin-client/src/main/java/org/keycloak/admin/client/resource/IdentityProvidersResource.java @@ -47,7 +47,15 @@ public interface IdentityProvidersResource { @GET @Path("instances") @Produces(MediaType.APPLICATION_JSON) - List find(@QueryParam("search") String search, @QueryParam("briefRepresentation") Boolean briefRepresentation, @QueryParam("first") Integer firstResult, @QueryParam("max") Integer maxResults); + List find(@QueryParam("search") String search, @QueryParam("briefRepresentation") Boolean briefRepresentation, + @QueryParam("first") Integer firstResult, @QueryParam("max") Integer maxResults); + + @GET + @Path("instances") + @Produces(MediaType.APPLICATION_JSON) + List find(@QueryParam("search") String search, @QueryParam("briefRepresentation") Boolean briefRepresentation, + @QueryParam("first") Integer firstResult, @QueryParam("max") Integer maxResults, + @QueryParam("realmOnly") Boolean realmOnly); @POST @Path("instances") diff --git a/admin-client/src/main/java/org/keycloak/admin/client/resource/OrganizationMembersResource.java b/admin-client/src/main/java/org/keycloak/admin/client/resource/OrganizationMembersResource.java index bdd3fc5..e503f66 100644 --- a/admin-client/src/main/java/org/keycloak/admin/client/resource/OrganizationMembersResource.java +++ b/admin-client/src/main/java/org/keycloak/admin/client/resource/OrganizationMembersResource.java @@ -81,4 +81,14 @@ Response inviteUser(@FormParam("email") String email, @Path("invite-existing-user") @Consumes(MediaType.APPLICATION_FORM_URLENCODED) Response inviteExistingUser(@FormParam("id") String id); + + @Path("count") + @GET + @Produces(MediaType.APPLICATION_JSON) + Long count(); + + @Path("{id}/organizations") + @GET + @Produces(MediaType.APPLICATION_JSON) + List getOrganizations(@PathParam("id") String id); } diff --git a/admin-client/src/main/java/org/keycloak/admin/client/resource/OrganizationsMembersResource.java b/admin-client/src/main/java/org/keycloak/admin/client/resource/OrganizationsMembersResource.java new file mode 100644 index 0000000..f70b1d7 --- /dev/null +++ b/admin-client/src/main/java/org/keycloak/admin/client/resource/OrganizationsMembersResource.java @@ -0,0 +1,35 @@ +/* + * Copyright 2024 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * 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. + */ + +package org.keycloak.admin.client.resource; + +import java.util.List; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import org.keycloak.representations.idm.OrganizationRepresentation; + +public interface OrganizationsMembersResource { + + @Path("{id}/organizations") + @GET + @Produces(MediaType.APPLICATION_JSON) + List getOrganizations(@PathParam("id") String id); +} diff --git a/admin-client/src/main/java/org/keycloak/admin/client/resource/OrganizationsResource.java b/admin-client/src/main/java/org/keycloak/admin/client/resource/OrganizationsResource.java index c42c1c1..296d501 100644 --- a/admin-client/src/main/java/org/keycloak/admin/client/resource/OrganizationsResource.java +++ b/admin-client/src/main/java/org/keycloak/admin/client/resource/OrganizationsResource.java @@ -105,4 +105,7 @@ List searchByAttribute( @QueryParam("first") Integer first, @QueryParam("max") Integer max ); + + @Path("members") + OrganizationsMembersResource members(); } diff --git a/admin-client/src/main/java/org/keycloak/common/constants/ServiceAccountConstants.java b/admin-client/src/main/java/org/keycloak/common/constants/ServiceAccountConstants.java new file mode 100644 index 0000000..4dc0dd1 --- /dev/null +++ b/admin-client/src/main/java/org/keycloak/common/constants/ServiceAccountConstants.java @@ -0,0 +1,38 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * 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. + */ + +package org.keycloak.common.constants; + +/** + * @author Marek Posolda + */ +public interface ServiceAccountConstants { + + String CLIENT_AUTH = "client_auth"; + + String SERVICE_ACCOUNT_USER_PREFIX = "service-account-"; + + String CLIENT_ID_PROTOCOL_MAPPER = "Client ID"; + String CLIENT_HOST_PROTOCOL_MAPPER = "Client Host"; + String CLIENT_ADDRESS_PROTOCOL_MAPPER = "Client IP Address"; + + String CLIENT_ID_SESSION_NOTE = "clientId"; + String CLIENT_ID = "client_id"; + String CLIENT_HOST = "clientHost"; + String CLIENT_ADDRESS = "clientAddress"; + +} diff --git a/admin-client/src/main/java/org/keycloak/common/crypto/CryptoConstants.java b/admin-client/src/main/java/org/keycloak/common/crypto/CryptoConstants.java new file mode 100644 index 0000000..1c53e61 --- /dev/null +++ b/admin-client/src/main/java/org/keycloak/common/crypto/CryptoConstants.java @@ -0,0 +1,27 @@ +package org.keycloak.common.crypto; + +/** + * @author Marek Posolda + */ +public class CryptoConstants { + + // JWE algorithms + public static final String A128KW = "A128KW"; + public static final String RSA1_5 = "RSA1_5"; + public static final String RSA_OAEP = "RSA-OAEP"; + public static final String RSA_OAEP_256 = "RSA-OAEP-256"; + public static final String ECDH_ES = "ECDH-ES"; + public static final String ECDH_ES_A128KW = "ECDH-ES+A128KW"; + public static final String ECDH_ES_A192KW = "ECDH-ES+A192KW"; + public static final String ECDH_ES_A256KW = "ECDH-ES+A256KW"; + + // Constant for the OCSP provider + // public static final String OCSP = "OCSP"; + + /** Name of Java security provider used with non-fips BouncyCastle. Should be used in non-FIPS environment */ + public static final String BC_PROVIDER_ID = "BC"; + + /** Name of Java security provider used with fips BouncyCastle. Should be used in FIPS environment */ + public static final String BCFIPS_PROVIDER_ID = "BCFIPS"; + +} diff --git a/admin-client/src/main/java/org/keycloak/crypto/Algorithm.java b/admin-client/src/main/java/org/keycloak/crypto/Algorithm.java new file mode 100755 index 0000000..ab6efb6 --- /dev/null +++ b/admin-client/src/main/java/org/keycloak/crypto/Algorithm.java @@ -0,0 +1,57 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * 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. + */ +package org.keycloak.crypto; + +import org.keycloak.common.crypto.CryptoConstants; + +public interface Algorithm { + + /* RSA signing algorithms */ + String HS256 = "HS256"; + String HS384 = "HS384"; + String HS512 = "HS512"; + String RS256 = "RS256"; + String RS384 = "RS384"; + String RS512 = "RS512"; + String PS256 = "PS256"; + String PS384 = "PS384"; + String PS512 = "PS512"; + + /* ECDSA signing algorithms */ + String ES256 = "ES256"; + String ES384 = "ES384"; + String ES512 = "ES512"; + + /* EdDSA signing algorithms */ + String EdDSA = "EdDSA"; + /* EdDSA Curve */ + String Ed25519 = "Ed25519"; + String Ed448 = "Ed448"; + + /* RSA Encryption Algorithms */ + String RSA1_5 = CryptoConstants.RSA1_5; + String RSA_OAEP = CryptoConstants.RSA_OAEP; + String RSA_OAEP_256 = CryptoConstants.RSA_OAEP_256; + + /* AES */ + String AES = "AES"; + + String ECDH_ES = CryptoConstants.ECDH_ES; + String ECDH_ES_A128KW = CryptoConstants.ECDH_ES_A128KW; + String ECDH_ES_A192KW = CryptoConstants.ECDH_ES_A192KW; + String ECDH_ES_A256KW = CryptoConstants.ECDH_ES_A256KW; +} diff --git a/admin-client/src/main/java/org/keycloak/events/EventType.java b/admin-client/src/main/java/org/keycloak/events/EventType.java new file mode 100755 index 0000000..16697c5 --- /dev/null +++ b/admin-client/src/main/java/org/keycloak/events/EventType.java @@ -0,0 +1,216 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * 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. + */ + +package org.keycloak.events; + +import java.util.Map; +import java.util.Objects; +import org.keycloak.util.EnumWithStableIndex; + +/** + * @author Stian Thorgersen + */ +public enum EventType implements EnumWithStableIndex { + + LOGIN(0, true), + LOGIN_ERROR(0x10000 + LOGIN.getStableIndex(), true), + REGISTER(1, true), + REGISTER_ERROR(0x10000 + REGISTER.getStableIndex(), true), + LOGOUT(2, true), + LOGOUT_ERROR(0x10000 + LOGOUT.getStableIndex(), true), + + CODE_TO_TOKEN(3, true), + CODE_TO_TOKEN_ERROR(0x10000 + CODE_TO_TOKEN.getStableIndex(), true), + + CLIENT_LOGIN(4, true), + CLIENT_LOGIN_ERROR(0x10000 + CLIENT_LOGIN.getStableIndex(), true), + + REFRESH_TOKEN(5, false), + REFRESH_TOKEN_ERROR(0x10000 + REFRESH_TOKEN.getStableIndex(), false), + + /** + * @deprecated see KEYCLOAK-2266 + */ + @Deprecated + VALIDATE_ACCESS_TOKEN(6, false), + @Deprecated + VALIDATE_ACCESS_TOKEN_ERROR(0x10000 + VALIDATE_ACCESS_TOKEN.getStableIndex(), false), + INTROSPECT_TOKEN(7, false), + INTROSPECT_TOKEN_ERROR(0x10000 + INTROSPECT_TOKEN.getStableIndex(), false), + + FEDERATED_IDENTITY_LINK(8, true), + FEDERATED_IDENTITY_LINK_ERROR(0x10000 + FEDERATED_IDENTITY_LINK.getStableIndex(), true), + REMOVE_FEDERATED_IDENTITY(9, true), + REMOVE_FEDERATED_IDENTITY_ERROR(0x10000 + REMOVE_FEDERATED_IDENTITY.getStableIndex(), true), + + + UPDATE_EMAIL(10, true), + UPDATE_EMAIL_ERROR(0x10000 + UPDATE_EMAIL.getStableIndex(), true), + UPDATE_PROFILE(11, true), + UPDATE_PROFILE_ERROR(0x10000 + UPDATE_PROFILE.getStableIndex(), true), + + @Deprecated + UPDATE_PASSWORD(12, true), + @Deprecated + UPDATE_PASSWORD_ERROR(0x10000 + UPDATE_PASSWORD.getStableIndex(), true), + + @Deprecated + UPDATE_TOTP(13, true), + @Deprecated + UPDATE_TOTP_ERROR(0x10000 + UPDATE_TOTP.getStableIndex(), true), + VERIFY_EMAIL(14, true), + VERIFY_EMAIL_ERROR(0x10000 + VERIFY_EMAIL.getStableIndex(), true), + VERIFY_PROFILE(15, true), + VERIFY_PROFILE_ERROR(0x10000 + VERIFY_PROFILE.getStableIndex(), true), + + @Deprecated + REMOVE_TOTP(16, true), + @Deprecated + REMOVE_TOTP_ERROR(0x10000 + REMOVE_TOTP.getStableIndex(), true), + + GRANT_CONSENT(17, true), + GRANT_CONSENT_ERROR(0x10000 + GRANT_CONSENT.getStableIndex(), true), + UPDATE_CONSENT(18, true), + UPDATE_CONSENT_ERROR(0x10000 + UPDATE_CONSENT.getStableIndex(), true), + REVOKE_GRANT(19, true), + REVOKE_GRANT_ERROR(0x10000 + REVOKE_GRANT.getStableIndex(), true), + + SEND_VERIFY_EMAIL(20, true), + SEND_VERIFY_EMAIL_ERROR(0x10000 + SEND_VERIFY_EMAIL.getStableIndex(), true), + SEND_RESET_PASSWORD(21, true), + SEND_RESET_PASSWORD_ERROR(0x10000 + SEND_RESET_PASSWORD.getStableIndex(), true), + SEND_IDENTITY_PROVIDER_LINK(22, true), + SEND_IDENTITY_PROVIDER_LINK_ERROR(0x10000 + SEND_IDENTITY_PROVIDER_LINK.getStableIndex(), true), + RESET_PASSWORD(23, true), + RESET_PASSWORD_ERROR(0x10000 + RESET_PASSWORD.getStableIndex(), true), + + RESTART_AUTHENTICATION(24, true), + RESTART_AUTHENTICATION_ERROR(0x10000 + RESTART_AUTHENTICATION.getStableIndex(), true), + + INVALID_SIGNATURE(25, false), + INVALID_SIGNATURE_ERROR(0x10000 + INVALID_SIGNATURE.getStableIndex(), false), + REGISTER_NODE(26, false), + REGISTER_NODE_ERROR(0x10000 + REGISTER_NODE.getStableIndex(), false), + UNREGISTER_NODE(27, false), + UNREGISTER_NODE_ERROR(0x10000 + UNREGISTER_NODE.getStableIndex(), false), + + USER_INFO_REQUEST(28, false), + USER_INFO_REQUEST_ERROR(0x10000 + USER_INFO_REQUEST.getStableIndex(), false), + + IDENTITY_PROVIDER_LINK_ACCOUNT(29, true), + IDENTITY_PROVIDER_LINK_ACCOUNT_ERROR(0x10000 + IDENTITY_PROVIDER_LINK_ACCOUNT.getStableIndex(), true), + IDENTITY_PROVIDER_LOGIN(30, false), + IDENTITY_PROVIDER_LOGIN_ERROR(0x10000 + IDENTITY_PROVIDER_LOGIN.getStableIndex(), false), + IDENTITY_PROVIDER_FIRST_LOGIN(31, true), + IDENTITY_PROVIDER_FIRST_LOGIN_ERROR(0x10000 + IDENTITY_PROVIDER_FIRST_LOGIN.getStableIndex(), true), + IDENTITY_PROVIDER_POST_LOGIN(32, true), + IDENTITY_PROVIDER_POST_LOGIN_ERROR(0x10000 + IDENTITY_PROVIDER_POST_LOGIN.getStableIndex(), true), + IDENTITY_PROVIDER_RESPONSE(33, false), + IDENTITY_PROVIDER_RESPONSE_ERROR(0x10000 + IDENTITY_PROVIDER_RESPONSE.getStableIndex(), false), + IDENTITY_PROVIDER_RETRIEVE_TOKEN(34, false), + IDENTITY_PROVIDER_RETRIEVE_TOKEN_ERROR(0x10000 + IDENTITY_PROVIDER_RETRIEVE_TOKEN.getStableIndex(), false), + IMPERSONATE(35, true), + IMPERSONATE_ERROR(0x10000 + IMPERSONATE.getStableIndex(), true), + CUSTOM_REQUIRED_ACTION(36, true), + CUSTOM_REQUIRED_ACTION_ERROR(0x10000 + CUSTOM_REQUIRED_ACTION.getStableIndex(), true), + EXECUTE_ACTIONS(37, true), + EXECUTE_ACTIONS_ERROR(0x10000 + EXECUTE_ACTIONS.getStableIndex(), true), + EXECUTE_ACTION_TOKEN(38, true), + EXECUTE_ACTION_TOKEN_ERROR(0x10000 + EXECUTE_ACTION_TOKEN.getStableIndex(), true), + + CLIENT_INFO(39, false), + CLIENT_INFO_ERROR(0x10000 + CLIENT_INFO.getStableIndex(), false), + CLIENT_REGISTER(40, true), + CLIENT_REGISTER_ERROR(0x10000 + CLIENT_REGISTER.getStableIndex(), true), + CLIENT_UPDATE(41, true), + CLIENT_UPDATE_ERROR(0x10000 + CLIENT_UPDATE.getStableIndex(), true), + CLIENT_DELETE(42, true), + CLIENT_DELETE_ERROR(0x10000 + CLIENT_DELETE.getStableIndex(), true), + + CLIENT_INITIATED_ACCOUNT_LINKING(43, true), + CLIENT_INITIATED_ACCOUNT_LINKING_ERROR(0x10000 + CLIENT_INITIATED_ACCOUNT_LINKING.getStableIndex(), true), + TOKEN_EXCHANGE(44, true), + TOKEN_EXCHANGE_ERROR(0x10000 + TOKEN_EXCHANGE.getStableIndex(), true), + + OAUTH2_DEVICE_AUTH(45, true), + OAUTH2_DEVICE_AUTH_ERROR(0x10000 + OAUTH2_DEVICE_AUTH.getStableIndex(), true), + OAUTH2_DEVICE_VERIFY_USER_CODE(46, true), + OAUTH2_DEVICE_VERIFY_USER_CODE_ERROR(0x10000 + OAUTH2_DEVICE_VERIFY_USER_CODE.getStableIndex(), true), + OAUTH2_DEVICE_CODE_TO_TOKEN(47, true), + OAUTH2_DEVICE_CODE_TO_TOKEN_ERROR(0x10000 + OAUTH2_DEVICE_CODE_TO_TOKEN.getStableIndex(), true), + + AUTHREQID_TO_TOKEN(48, true), + AUTHREQID_TO_TOKEN_ERROR(0x10000 + AUTHREQID_TO_TOKEN.getStableIndex(), true), + + PERMISSION_TOKEN(49, true), + PERMISSION_TOKEN_ERROR(0x10000 + PERMISSION_TOKEN.getStableIndex(), false), + + DELETE_ACCOUNT(50, true), + DELETE_ACCOUNT_ERROR(0x10000 + DELETE_ACCOUNT.getStableIndex(), true), + + // PAR request. + PUSHED_AUTHORIZATION_REQUEST(51, false), + PUSHED_AUTHORIZATION_REQUEST_ERROR(0x10000 + PUSHED_AUTHORIZATION_REQUEST.getStableIndex(), false), + + USER_DISABLED_BY_PERMANENT_LOCKOUT(52, true), + USER_DISABLED_BY_PERMANENT_LOCKOUT_ERROR(0x10000 + USER_DISABLED_BY_PERMANENT_LOCKOUT.getStableIndex(), false), + + USER_DISABLED_BY_TEMPORARY_LOCKOUT(53,true), + USER_DISABLED_BY_TEMPORARY_LOCKOUT_ERROR(0x10000 + USER_DISABLED_BY_TEMPORARY_LOCKOUT.getStableIndex(), false), + + OAUTH2_EXTENSION_GRANT(54, true), + OAUTH2_EXTENSION_GRANT_ERROR(0x10000 + OAUTH2_EXTENSION_GRANT.getStableIndex(), true), + + FEDERATED_IDENTITY_OVERRIDE_LINK(55, true), + FEDERATED_IDENTITY_OVERRIDE_LINK_ERROR(0x10000 + FEDERATED_IDENTITY_OVERRIDE_LINK.getStableIndex(), true), + + UPDATE_CREDENTIAL(56, true), + UPDATE_CREDENTIAL_ERROR(0x10000 + UPDATE_CREDENTIAL.getStableIndex(), true), + + REMOVE_CREDENTIAL(57, true), + REMOVE_CREDENTIAL_ERROR(0x10000 + REMOVE_CREDENTIAL.getStableIndex(), true), + + INVITE_ORG(60, true), + INVITE_ORG_ERROR(0x10000 + INVITE_ORG.getStableIndex(), true); + + private final int stableIndex; + private final boolean saveByDefault; + private static final Map BY_ID = EnumWithStableIndex.getReverseIndex(values()); + + EventType(int stableIndex, boolean saveByDefault) { + Objects.requireNonNull(stableIndex); + this.stableIndex = stableIndex; + this.saveByDefault = saveByDefault; + } + + @Override + public int getStableIndex() { + return stableIndex; + } + + /** + * Determines whether this event is stored when the admin has not set a specific set of event types to save. + * @return + */ + public boolean isSaveByDefault() { + return saveByDefault; + } + + public static EventType valueOfInteger(Integer id) { + return id == null ? null : BY_ID.get(id); + } +} diff --git a/admin-client/src/main/java/org/keycloak/models/AccountRoles.java b/admin-client/src/main/java/org/keycloak/models/AccountRoles.java new file mode 100644 index 0000000..f607664 --- /dev/null +++ b/admin-client/src/main/java/org/keycloak/models/AccountRoles.java @@ -0,0 +1,36 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * 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. + */ + +package org.keycloak.models; + +/** + * @author Stian Thorgersen + */ +public interface AccountRoles { + + String VIEW_PROFILE = "view-profile"; + String MANAGE_ACCOUNT = "manage-account"; + String MANAGE_ACCOUNT_LINKS = "manage-account-links"; + String VIEW_APPLICATIONS = "view-applications"; + String VIEW_CONSENT = "view-consent"; + String MANAGE_CONSENT = "manage-consent"; + String DELETE_ACCOUNT = "delete-account"; + String VIEW_GROUPS = "view-groups"; + + String[] DEFAULT = {VIEW_PROFILE, MANAGE_ACCOUNT}; + +} diff --git a/admin-client/src/main/java/org/keycloak/models/Constants.java b/admin-client/src/main/java/org/keycloak/models/Constants.java new file mode 100755 index 0000000..6fd1188 --- /dev/null +++ b/admin-client/src/main/java/org/keycloak/models/Constants.java @@ -0,0 +1,190 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * 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. + */ + +package org.keycloak.models; + +import org.keycloak.OAuth2Constants; +import org.keycloak.crypto.Algorithm; + +import java.util.Arrays; +import java.util.Collection; +import java.util.regex.Pattern; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public final class Constants { + public static final String ADMIN_CONSOLE_CLIENT_ID = "security-admin-console"; + public static final String ADMIN_CLI_CLIENT_ID = "admin-cli"; + + public static final String ACCOUNT_MANAGEMENT_CLIENT_ID = "account"; + public static final String ACCOUNT_CONSOLE_CLIENT_ID = "account-console"; + public static final String BROKER_SERVICE_CLIENT_ID = "broker"; + public static final String REALM_MANAGEMENT_CLIENT_ID = "realm-management"; + + public static final String AUTH_BASE_URL_PROP = "${authBaseUrl}"; + public static final String AUTH_ADMIN_URL_PROP = "${authAdminUrl}"; + + public static final Collection defaultClients = Arrays.asList(ACCOUNT_MANAGEMENT_CLIENT_ID, ADMIN_CLI_CLIENT_ID, BROKER_SERVICE_CLIENT_ID, REALM_MANAGEMENT_CLIENT_ID, ADMIN_CONSOLE_CLIENT_ID); + + public static final String INSTALLED_APP_URN = "urn:ietf:wg:oauth:2.0:oob"; + public static final String INSTALLED_APP_URL = "http://localhost"; + public static final String INSTALLED_APP_LOOPBACK = "http://127.0.0.1"; + + public static final String READ_TOKEN_ROLE = "read-token"; + public static final String[] BROKER_SERVICE_ROLES = {READ_TOKEN_ROLE}; + public static final String OFFLINE_ACCESS_ROLE = OAuth2Constants.OFFLINE_ACCESS; + public static final String DEFAULT_ROLES_ROLE_PREFIX = "default-roles"; + + public static final String AUTHZ_UMA_PROTECTION = "uma_protection"; + public static final String AUTHZ_UMA_AUTHORIZATION = "uma_authorization"; + public static final String[] AUTHZ_DEFAULT_AUTHORIZATION_ROLES = {AUTHZ_UMA_AUTHORIZATION}; + + // 15 minutes + public static final int DEFAULT_ACCESS_TOKEN_LIFESPAN_FOR_IMPLICIT_FLOW_TIMEOUT = 900; + // 30 days + public static final int DEFAULT_OFFLINE_SESSION_IDLE_TIMEOUT = 2592000; + // KEYCLOAK-7688 Offline Session Max for Offline Token + // 60 days + public static final int DEFAULT_OFFLINE_SESSION_MAX_LIFESPAN = 5184000; + public static final String DEFAULT_SIGNATURE_ALGORITHM = Algorithm.RS256; + public static final String INTERNAL_SIGNATURE_ALGORITHM = Algorithm.HS512; + + public static final int DEFAULT_SESSION_IDLE_TIMEOUT = 1800; // 30 minutes + public static final int DEFAULT_SESSION_MAX_LIFESPAN = 36000; // 10 hours + + public static final String DEFAULT_WEBAUTHN_POLICY_SIGNATURE_ALGORITHMS = Algorithm.ES256+","+Algorithm.RS256; + public static final String DEFAULT_WEBAUTHN_POLICY_RP_ENTITY_NAME = "keycloak"; + // it stands for optional parameter not specified in WebAuthn + public static final String DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED = "not specified"; + + // Prefix used for the realm attributes and other places + public static final String WEBAUTHN_PASSWORDLESS_PREFIX = "Passwordless"; + + public static final String VERIFY_EMAIL_KEY = "VERIFY_EMAIL_KEY"; + public static final String EXECUTION = "execution"; + public static final String CLIENT_ID = "client_id"; + public static final String TOKEN = "token"; + public static final String TAB_ID = "tab_id"; + public static final String CLIENT_DATA = "client_data"; + public static final String REUSE_ID = "reuse_id"; + public static final String SKIP_LOGOUT = "skip_logout"; + public static final String KEY = "key"; + + public static final String KC_ACTION = "kc_action"; + + public static final String KC_ACTION_PARAMETER = "kc_action_parameter"; + public static final String KC_ACTION_STATUS = "kc_action_status"; + public static final String KC_ACTION_EXECUTING = "kc_action_executing"; + /** + * Auth session attribute whether an AIA is enforced, which means it cannot be cancelled. + *

Example use case: the action behind the AIA is also defined on the user (for example, UPDATE_PASSWORD).

+ */ + public static final String KC_ACTION_ENFORCED = "kc_action_enforced"; + public static final int KC_ACTION_MAX_AGE = 300; + + public static final String IS_AIA_REQUEST = "IS_AIA_REQUEST"; + public static final String AIA_SILENT_CANCEL = "silent_cancel"; + public static final String AUTHENTICATION_EXECUTION = "authenticationExecution"; + public static final String CREDENTIAL_ID = "credentialId"; + + public static final String SKIP_LINK = "skipLink"; + public static final String TEMPLATE_ATTR_ACTION_URI = "actionUri"; + public static final String TEMPLATE_ATTR_REQUIRED_ACTIONS = "requiredActions"; + + // Prefix for user attributes used in various "context"data maps + public static final String USER_ATTRIBUTES_PREFIX = "user.attributes."; + + // Roles already granted by a mapper when updating brokered users. + public static final String MAPPER_GRANTED_ROLES = "MAPPER_GRANTED_ROLES"; + + // Groups already assigned by a mapper when updating brokered users. + public static final String MAPPER_GRANTED_GROUPS = "MAPPER_GRANTED_GROUPS"; + + public static final String MAPPER_SESSION_NOTES = "MAPPER_SESSION_NOTES"; + + // Indication to admin-rest-endpoint that realm keys should be re-generated + public static final String GENERATE = "GENERATE"; + + public static final int DEFAULT_MAX_RESULTS = 100; + + // Delimiter to be used in the configuration of authenticators (and some other components) in case that we need to save + // multiple values into single string + public static final String CFG_DELIMITER = "##"; + + // Better performance to use this instead of String.split + public static final Pattern CFG_DELIMITER_PATTERN = Pattern.compile("\\s*" + CFG_DELIMITER + "\\s*"); + + public static final String OFFLINE_ACCESS_SCOPE_CONSENT_TEXT = "${offlineAccessScopeConsentText}"; + + /** + * If set as an attribute in the {@link KeycloakSession}, indicates that the storage should batch write operations. + */ + public static final String STORAGE_BATCH_ENABLED = "org.keycloak.storage.batch_enabled"; + + /** + * If {@code #STORAGE_BATCH_ENABLED} is set, indicates the batch size. + */ + public static final String STORAGE_BATCH_SIZE = "org.keycloak.storage.batch_size"; + + // Client Polices Realm Attributes Keys + public static final String CLIENT_PROFILES = "client-policies.profiles"; + public static final String CLIENT_POLICIES = "client-policies.policies"; + + + // Authentication session note, which contains loa of current authentication in progress + public static final String LEVEL_OF_AUTHENTICATION = "level-of-authentication"; + + // Key in authentication execution config (AuthenticationExecutionModel), storing the configured authentication reference value + public static final String AUTHENTICATION_EXECUTION_REFERENCE_VALUE = "default.reference.value"; + public static final String AUTHENTICATION_EXECUTION_REFERENCE_MAX_AGE = "default.reference.maxAge"; + + // Authentication session note containing a serialized map of successfully completed authentication executions and their associated times + public static final String AUTHENTICATORS_COMPLETED = "authenticators-completed"; + + // Authentication session (and user session) note, which contains map with authenticated levels and the times of their authentications, + // so it is possible to check when particular level expires and needs to be re-authenticated + public static final String LOA_MAP = "loa-map"; + + public static final String REQUESTED_LEVEL_OF_AUTHENTICATION = "requested-level-of-authentication"; + public static final String FORCE_LEVEL_OF_AUTHENTICATION = "force-level-of-authentication"; + public static final String ACR_LOA_MAP = "acr.loa.map"; + public static final String DEFAULT_ACR_VALUES = "default.acr.values"; + public static final int MINIMUM_LOA = 0; + public static final int NO_LOA = -1; + + public static final String SESSION_NOTE_LIGHTWEIGHT_USER = "keycloak.userModel"; + + public static final String USE_LIGHTWEIGHT_ACCESS_TOKEN_ENABLED = "client.use.lightweight.access.token.enabled"; + + public static final String SUPPORT_JWT_CLAIM_IN_INTROSPECTION_RESPONSE_ENABLED = "client.introspection.response.allow.jwt.claim.enabled"; + + public static final String TOTP_SECRET_KEY = "TOTP_SECRET_KEY"; + + // Sent to clients when authentication session expired, but user is already logged-in in current browser + public static final String AUTHENTICATION_EXPIRED_MESSAGE = "authentication_expired"; + + // attribute name used in apps to mark that it is an admin console and its azp is allowed + public static final String SECURITY_ADMIN_CONSOLE_ATTR = "security.admin.console"; + + //attribute name used to mark a client as realm client + public static final String REALM_CLIENT = "realm_client"; + + //attribute name used to mark a temporary admin user/service account as temporary + public static final String IS_TEMP_ADMIN_ATTR_NAME = "is_temporary_admin"; +} diff --git a/admin-client/src/main/java/org/keycloak/representations/idm/IdentityProviderRepresentation.java b/admin-client/src/main/java/org/keycloak/representations/idm/IdentityProviderRepresentation.java index 9ca5a0d..5d6c658 100755 --- a/admin-client/src/main/java/org/keycloak/representations/idm/IdentityProviderRepresentation.java +++ b/admin-client/src/main/java/org/keycloak/representations/idm/IdentityProviderRepresentation.java @@ -41,7 +41,7 @@ public class IdentityProviderRepresentation { *
  • missing - update profile page is presented for users with missing some of mandatory user profile fields *
  • off - update profile page is newer shown after first login * - * + * * @see #UPFLM_ON * @see #UPFLM_MISSING * @see #UPFLM_OFF @@ -54,8 +54,10 @@ public class IdentityProviderRepresentation { protected boolean addReadTokenRoleOnCreate; protected boolean authenticateByDefault; protected boolean linkOnly; + protected boolean hideOnLogin; protected String firstBrokerLoginFlowAlias; protected String postBrokerLoginFlowAlias; + protected String organizationId; protected Map config = new HashMap<>(); public String getInternalId() { @@ -106,10 +108,18 @@ public void setLinkOnly(boolean linkOnly) { this.linkOnly = linkOnly; } + public boolean isHideOnLogin() { + return this.hideOnLogin; + } + + public void setHideOnLogin(boolean hideOnLogin) { + this.hideOnLogin = hideOnLogin; + } + /** - * + * * Deprecated because replaced by {@link #updateProfileFirstLoginMode}. Kept here to allow import of old realms. - * + * * @deprecated {@link #setUpdateProfileFirstLoginMode(String)} */ @Deprecated @@ -194,4 +204,12 @@ public void setDisplayName(String displayName) { this.displayName = displayName; } + public String getOrganizationId() { + return this.organizationId; + } + + public void setOrganizationId(String organizationId) { + this.organizationId = organizationId; + } + } diff --git a/admin-client/src/main/java/org/keycloak/representations/idm/UserRepresentation.java b/admin-client/src/main/java/org/keycloak/representations/idm/UserRepresentation.java index a4e5ed0..69c1ee0 100755 --- a/admin-client/src/main/java/org/keycloak/representations/idm/UserRepresentation.java +++ b/admin-client/src/main/java/org/keycloak/representations/idm/UserRepresentation.java @@ -67,7 +67,6 @@ public UserRepresentation(UserRepresentation rep) { this.setUserProfileMetadata(rep.getUserProfileMetadata()); this.self = rep.getSelf(); - this.origin = rep.getOrigin(); this.createdTimestamp = rep.getCreatedTimestamp(); this.enabled = rep.isEnabled(); this.totp = rep.isTotp(); @@ -220,13 +219,21 @@ public void setGroups(List groups) { * Returns id of UserStorageProvider that loaded this user * * @return NULL if user stored locally + * @deprecated Use {@link #getFederationLink()} instead */ + @Deprecated public String getOrigin() { - return origin; + return federationLink; } + /** + * + * @param origin the origin + * @deprecated Use {@link #setFederationLink(String)} instead + */ + @Deprecated public void setOrigin(String origin) { - this.origin = origin; + // deprecated } public Set getDisableableCredentialTypes() { diff --git a/admin-client/src/main/java/org/keycloak/utils/StringUtil.java b/admin-client/src/main/java/org/keycloak/utils/StringUtil.java new file mode 100644 index 0000000..c939514 --- /dev/null +++ b/admin-client/src/main/java/org/keycloak/utils/StringUtil.java @@ -0,0 +1,110 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * 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. + */ +package org.keycloak.utils; + +import java.util.Collection; + +public class StringUtil { + + /** + * Returns true if string is null or blank + */ + public static boolean isBlank(String str) { + return !(isNotBlank(str)); + } + + /** + * Returns true if string is not null and not blank + */ + public static boolean isNotBlank(String str) { + return str != null && !str.isBlank(); + } + + /** + * Returns true if string is null or empty + */ + public static boolean isNullOrEmpty(String str) { + return str == null || str.isEmpty(); + } + + /** + * Calling: + *
    joinValuesWithLogicalCondition("or", Arrays.asList("foo", "bar", "baz", "caz" ))
    + * will return "foo, bar, baz or caz" + * + * @param conditionText condition + * @param values values to be joined with the condition at the end + * @return see the example above + */ + public static String joinValuesWithLogicalCondition(String conditionText, Collection values) { + StringBuilder options = new StringBuilder(); + int i = 1; + for (String o : values) { + if (i == values.size()) { + options.append(" " + conditionText + " "); + } else if (i > 1) { + options.append(", "); + } + options.append(o); + i++; + } + return options.toString(); + } + + /** + * Utility method that substitutes any isWhitespace char to common space ' ' or character 20. + * The idea is removing any weird space character in the string like \t, \n, \r. + * If quotes character is passed the quotes char is escaped to mark is not the end + * of the value (for example escaped \" if quotes char " is found in the string). + * + * @param str The string to normalize + * @param quotes The quotes to escape (for example " or '). It can be null. + * @return The string without weird whitespaces and quotes escaped + */ + public static String sanitizeSpacesAndQuotes(String str, Character quotes) { + // idea taken from commons-lang StringUtils.normalizeSpace + if (str == null || str.isEmpty()) { + return str; + } + StringBuilder sb = null; + for (int i = 0; i < str.length(); i++) { + final char actualChar = str.charAt(i); + if ((Character.isWhitespace(actualChar) && actualChar != ' ') || actualChar == 160) { + if (sb == null) { + sb = new StringBuilder(str.length() + 10).append(str.substring(0, i)); + } + sb.append(' '); + } else if (quotes != null && actualChar == quotes) { + if (sb == null) { + sb = new StringBuilder(str.length() + 10).append(str.substring(0, i)); + } + sb.append('\\').append(actualChar); + } else if (sb != null) { + sb.append(actualChar); + } + } + return sb == null? str : sb.toString(); + } + + public static String removeSuffix(String str, String suffix) { + int index = str.lastIndexOf(suffix); + if (str.endsWith(suffix) && index > 0) { + str = str.substring(0, index); + } + return str; + } +} diff --git a/testsuite/admin-client-tests/src/test/java/org/keycloak/client/testsuite/AbstractAdminClientTest.java b/testsuite/admin-client-tests/src/test/java/org/keycloak/client/testsuite/AbstractAdminClientTest.java new file mode 100644 index 0000000..7279759 --- /dev/null +++ b/testsuite/admin-client-tests/src/test/java/org/keycloak/client/testsuite/AbstractAdminClientTest.java @@ -0,0 +1,116 @@ +/* + * Copyright 2016 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @author tags. All rights reserved. + * + * 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. + */ + +package org.keycloak.client.testsuite; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.keycloak.admin.client.Keycloak; +import org.keycloak.admin.client.resource.RealmResource; +import org.keycloak.admin.client.resource.RealmsResource; +import org.keycloak.client.testsuite.common.OAuthClient; +import org.keycloak.client.testsuite.common.RealmImporter; +import org.keycloak.client.testsuite.common.RealmRepsSupplier; +import org.keycloak.client.testsuite.framework.Inject; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.representations.idm.RoleRepresentation; +import org.keycloak.testsuite.util.RoleBuilder; +import org.keycloak.util.JsonSerialization; + +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public abstract class AbstractAdminClientTest implements RealmRepsSupplier { + + protected static final String REALM_NAME = "admin-client-test"; + + protected RealmResource realm; + protected String realmId; + + @org.keycloak.client.testsuite.framework.Inject + protected Keycloak adminClient; + + @org.keycloak.client.testsuite.framework.Inject + protected RealmImporter realmImporter; + + @Inject + protected OAuthClient oauth; + + @Override + public List getRealmsForImport() { + RealmRepresentation adminRealmRep = new RealmRepresentation(); + adminRealmRep.setId(REALM_NAME); + adminRealmRep.setRealm(REALM_NAME); + adminRealmRep.setEnabled(true); + Map config = new HashMap<>(); + config.put("from", "auto@keycloak.org"); + config.put("host", "localhost"); + config.put("port", "3025"); + adminRealmRep.setSmtpServer(config); + + List eventListeners = new ArrayList<>(); + eventListeners.add("event-queue"); + adminRealmRep.setEventsListeners(eventListeners); + + RealmRepresentation testRealm = loadJson(getClass().getResourceAsStream("/testrealm.json"), RealmRepresentation.class); + + return Arrays.asList(adminRealmRep, testRealm); + } + + @BeforeEach + public void importRealms() { + realmImporter.importRealmsIfNotImported(this); + realm = adminClient.realm(REALM_NAME); + realmId = realm.toRepresentation().getId(); + } + + @Override + public boolean removeVerifyProfileAtImport() { + // remove verify profile by default because most tests are not prepared + return true; + } + + public RealmsResource realmsResource() { + return adminClient.realms(); + } + + public static T loadJson(InputStream is, Class type) { + try { + return JsonSerialization.readValue(is, type); + } catch (IOException e) { + throw new RuntimeException("Failed to parse json", e); + } + } + + RoleRepresentation createRealmRole(String roleName) { + RoleRepresentation role = RoleBuilder.create().name(roleName).build(); + return createRealmRole(role); + } + + RoleRepresentation createRealmRole(RoleRepresentation role) { + realm.roles().create(role); + return realm.roles().get(role.getName()).toRepresentation(); + } +} \ No newline at end of file diff --git a/testsuite/admin-client-tests/src/test/java/org/keycloak/client/testsuite/AdminClientTest.java b/testsuite/admin-client-tests/src/test/java/org/keycloak/client/testsuite/AdminClientTest.java new file mode 100644 index 0000000..60b2bf3 --- /dev/null +++ b/testsuite/admin-client-tests/src/test/java/org/keycloak/client/testsuite/AdminClientTest.java @@ -0,0 +1,199 @@ +/* + * Copyright 2020 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * 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. + * + */ + +package org.keycloak.client.testsuite; + +import jakarta.ws.rs.NotAuthorizedException; +import jakarta.ws.rs.core.Response; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.keycloak.admin.client.resource.ClientResource; +import org.keycloak.admin.client.resource.RealmResource; +import org.keycloak.common.constants.ServiceAccountConstants; +import org.keycloak.models.Constants; +import org.keycloak.representations.AccessTokenResponse; +import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.ClientScopeRepresentation; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.testsuite.client.Keycloak; +import org.keycloak.testsuite.util.AdminClientUtil; +import org.keycloak.testsuite.util.ApiUtil; +import org.keycloak.testsuite.util.ClientBuilder; +import org.keycloak.testsuite.util.ClientScopeBuilder; +import org.keycloak.testsuite.util.KeycloakModelUtils; +import org.keycloak.testsuite.util.RealmBuilder; +import org.keycloak.testsuite.util.UserBuilder; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Stream; + +/** + * Test for the various "Advanced" scenarios of java admin-client + * + * @author Marek Posolda + */ +public class AdminClientTest extends AbstractAdminClientTest { + + private static String realmName; + + private static String userId; + private static String userName; + + private static String clientUUID; + private static String clientId; + private static String clientSecret; + + @Override + public List getRealmsForImport() { + List testRealms = new ArrayList<>(super.getRealmsForImport()); + realmName = "test-admin-client"; + RealmBuilder realm = RealmBuilder.create().name(realmName); + + clientId = "service-account-cl"; + clientSecret = "secret1"; + ClientRepresentation enabledAppWithSkipRefreshToken = ClientBuilder.create() + .clientId(clientId) + .secret(clientSecret) + .serviceAccountsEnabled(true) + .build(); + realm.client(enabledAppWithSkipRefreshToken); + + userId = KeycloakModelUtils.generateId(); + userName = ServiceAccountConstants.SERVICE_ACCOUNT_USER_PREFIX + enabledAppWithSkipRefreshToken.getClientId(); + UserBuilder serviceAccountUser = UserBuilder.create() + .username(userName) + .serviceAccountId(enabledAppWithSkipRefreshToken.getClientId()) + .role(Constants.REALM_MANAGEMENT_CLIENT_ID, "realm-admin"); + realm.user(serviceAccountUser); + + UserBuilder defaultUser = UserBuilder.create() + .id(KeycloakModelUtils.generateId()) + .username("test-user@localhost") + .password("password") + .role(Constants.REALM_MANAGEMENT_CLIENT_ID, "realm-admin"); + realm.user(defaultUser); + + testRealms.add(realm.build()); + return testRealms; + } + + @BeforeEach + public void before() { + clientUUID = adminClient.realm(realmName).clients().findByClientId(clientId).get(0).getId(); + } + + @Test + public void clientCredentialsAuthSuccess() throws Exception { + // Check possible to load the realm + RealmRepresentation realm = adminClient.realm(realmName).toRepresentation(); + Assert.assertEquals(realmName, realm.getRealm()); + + //setTimeOffset(1000); + + // Check still possible to load the realm after original token expired (admin client should automatically re-authenticate) + realm = adminClient.realm(realmName).toRepresentation(); + Assert.assertEquals(realmName, realm.getRealm()); + } + + @Test + public void clientCredentialsClientDisabled() throws Exception { + try (Keycloak adminClient = AdminClientUtil.createAdminClientWithClientCredentials(realmName, clientId, clientSecret, null)) { + // Check possible to load the realm + RealmRepresentation realm = adminClient.realm(realmName).toRepresentation(); + Assert.assertEquals(realmName, realm.getRealm()); + + // Disable client and check it should not be possible to load the realms anymore + setClientEnabled(clientId, false); + + // Check not possible to invoke anymore + try { + realm = adminClient.realm(realmName).toRepresentation(); + Assert.fail("Not expected to successfully get realm"); + } catch (NotAuthorizedException nae) { + // Expected + } + } finally { + setClientEnabled(clientId, true); + } + } + + @Test + public void adminAuthClientDisabled() throws Exception { + try (Keycloak adminClient = AdminClientUtil.createAdminClient(false, realmName, "test-user@localhost", "password", Constants.ADMIN_CLI_CLIENT_ID, null)) { + // Check possible to load the realm + RealmRepresentation realm = adminClient.realm(realmName).toRepresentation(); + Assert.assertEquals(realmName, realm.getRealm()); + + // Disable client and check it should not be possible to load the realms anymore + setClientEnabled(Constants.ADMIN_CLI_CLIENT_ID, false); + + // Check not possible to invoke anymore + try { + realm = adminClient.realm(realmName).toRepresentation(); + Assert.fail("Not expected to successfully get realm"); + } catch (NotAuthorizedException nae) { + // Expected + } + } finally { + setClientEnabled(Constants.ADMIN_CLI_CLIENT_ID, true); + } + } + + @Test + public void scopedClientCredentialsAuthSuccess() throws Exception { + final RealmResource testRealm = adminClient.realm(realmName); + + // we need to create custom scope after import, otherwise the default scopes are missing. + final String scopeName = "myScope"; + String scopeId = createScope(testRealm, scopeName, KeycloakModelUtils.generateId()); + testRealm.clients().get(clientUUID).addOptionalClientScope(scopeId); + + // with scope + try (Keycloak adminClient = AdminClientUtil.createAdminClientWithClientCredentials(realmName, + clientId, clientSecret, scopeName)) { + final AccessTokenResponse accessToken = adminClient.tokenManager().getAccessToken(); + Assert.assertTrue(accessToken.getScope().contains(scopeName)); + } + // without scope + try (Keycloak adminClient = AdminClientUtil.createAdminClientWithClientCredentials(realmName, + clientId, clientSecret, null)) { + final AccessTokenResponse accessToken = adminClient.tokenManager().getAccessToken(); + Assert.assertFalse(accessToken.getScope().contains(scopeName)); + } + } + + private void setClientEnabled(String clientId, boolean enabled) { + ClientResource client = ApiUtil.findClientByClientId(adminClient.realms().realm(realmName), clientId); + ClientRepresentation clientRep = client.toRepresentation(); + clientRep.setEnabled(enabled); + client.update(clientRep); + } + + private String createScope(RealmResource testRealm, String scopeName, String scopeId) { + final ClientScopeRepresentation testScope = + ClientScopeBuilder.create().name(scopeName).protocol("openid-connect").build(); + testScope.setId(scopeId); + try (Response response = testRealm.clientScopes().create(testScope)) { + Assert.assertEquals(201, response.getStatus()); + return ApiUtil.getCreatedId(response); + } + } +} diff --git a/testsuite/admin-client-tests/src/test/java/org/keycloak/client/testsuite/Assert.java b/testsuite/admin-client-tests/src/test/java/org/keycloak/client/testsuite/Assert.java new file mode 100644 index 0000000..eabe412 --- /dev/null +++ b/testsuite/admin-client-tests/src/test/java/org/keycloak/client/testsuite/Assert.java @@ -0,0 +1,96 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * 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. + */ + +package org.keycloak.client.testsuite; + + +import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.ClientScopeRepresentation; +import org.keycloak.representations.idm.ComponentRepresentation; +import org.keycloak.representations.idm.GroupRepresentation; +import org.keycloak.representations.idm.IdentityProviderRepresentation; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.representations.idm.RoleRepresentation; +import org.keycloak.representations.idm.UserFederationProviderFactoryRepresentation; +import org.keycloak.representations.idm.UserProfileAttributeMetadata; +import org.keycloak.representations.idm.UserRepresentation; +import org.keycloak.representations.info.ThemeInfoRepresentation; + +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + + +/** + * @author Stian Thorgersen + */ +public class Assert extends org.junit.jupiter.api.Assertions { + + public static final Long DEFAULT_NUMBER_DEVIATION = 20L; + + public static void assertNames(Set actual, String... expected) { + Arrays.sort(expected); + String[] actualNames = names(new LinkedList(actual)); + assertArrayEquals(expected, actualNames, "Expected: " + Arrays.toString(expected) + ", was: " + Arrays.toString(actualNames)); + } + + public static void assertNames(List actual, String... expected) { + Arrays.sort(expected); + String[] actualNames = names(actual); + assertArrayEquals(expected, actualNames, "Expected: " + Arrays.toString(expected) + ", was: " + Arrays.toString(actualNames)); + } + + private static String[] names(List list) { + String[] names = new String[list.size()]; + for (int i = 0; i < list.size(); i++) { + names[i] = name(list.get(i)); + } + Arrays.sort(names); + return names; + } + + private static String name(Object o1) { + if (o1 instanceof String) { + return (String) o1; + } else if (o1 instanceof RealmRepresentation) { + return ((RealmRepresentation) o1).getRealm(); + } else if (o1 instanceof ClientRepresentation) { + return ((ClientRepresentation) o1).getClientId(); + } else if (o1 instanceof IdentityProviderRepresentation) { + return ((IdentityProviderRepresentation) o1).getAlias(); + } else if (o1 instanceof RoleRepresentation) { + return ((RoleRepresentation) o1).getName(); + } else if (o1 instanceof UserRepresentation) { + return ((UserRepresentation) o1).getUsername(); + } else if (o1 instanceof UserFederationProviderFactoryRepresentation) { + return ((UserFederationProviderFactoryRepresentation) o1).getId(); + } else if (o1 instanceof GroupRepresentation) { + return ((GroupRepresentation) o1).getName(); + } else if (o1 instanceof ComponentRepresentation) { + return ((ComponentRepresentation) o1).getName(); + } else if (o1 instanceof ClientScopeRepresentation) { + return ((ClientScopeRepresentation) o1).getName(); + } else if (o1 instanceof ThemeInfoRepresentation) { + return ((ThemeInfoRepresentation) o1).getName(); + } else if (o1 instanceof UserProfileAttributeMetadata) { + return ((UserProfileAttributeMetadata) o1).getName(); + } + + throw new IllegalArgumentException(); + } +} diff --git a/testsuite/admin-client-tests/src/test/java/org/keycloak/client/testsuite/ClientTest.java b/testsuite/admin-client-tests/src/test/java/org/keycloak/client/testsuite/ClientTest.java new file mode 100644 index 0000000..cc3ede9 --- /dev/null +++ b/testsuite/admin-client-tests/src/test/java/org/keycloak/client/testsuite/ClientTest.java @@ -0,0 +1,870 @@ +///* +// * Copyright 2016 Red Hat, Inc. and/or its affiliates +// * and other contributors as indicated by the @author tags. +// * +// * 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. +// */ +// +package org.keycloak.client.testsuite; + +import jakarta.ws.rs.BadRequestException; +import jakarta.ws.rs.NotFoundException; +import jakarta.ws.rs.core.Response; +import org.junit.jupiter.api.Test; +import org.keycloak.OAuth2Constants; +import org.keycloak.admin.client.resource.ClientResource; +import org.keycloak.admin.client.resource.ProtocolMappersResource; +import org.keycloak.admin.client.resource.RoleMappingResource; +import org.keycloak.client.testsuite.common.OAuthClient; +import org.keycloak.client.testsuite.framework.KeycloakVersion; +import org.keycloak.common.util.Time; +import org.keycloak.models.AccountRoles; +import org.keycloak.models.Constants; +import org.keycloak.representations.adapters.action.GlobalRequestResult; + +import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.OAuth2ErrorRepresentation; +import org.keycloak.representations.idm.ProtocolMapperRepresentation; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.representations.idm.RoleRepresentation; +import org.keycloak.representations.idm.UserRepresentation; +import org.keycloak.representations.idm.UserSessionRepresentation; +import org.keycloak.testsuite.util.ApiUtil; +import org.keycloak.testsuite.util.ClientBuilder; +import org.keycloak.testsuite.util.CredentialBuilder; +import org.keycloak.testsuite.util.RoleBuilder; +import org.keycloak.testsuite.util.ServerURLs; +import org.keycloak.testsuite.util.UserBuilder; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +import static java.util.Arrays.asList; +import static io.smallrye.common.constraint.Assert.assertFalse; +import static io.smallrye.common.constraint.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.fail; +import static org.testcontainers.shaded.org.hamcrest.MatcherAssert.assertThat; +import static org.testcontainers.shaded.org.hamcrest.Matchers.lessThan; +import static org.testcontainers.shaded.org.hamcrest.Matchers.greaterThan; +import static org.testcontainers.shaded.org.hamcrest.Matchers.hasItem; +import static org.testcontainers.shaded.org.hamcrest.Matchers.containsInAnyOrder; +import static org.testcontainers.shaded.org.hamcrest.Matchers.is; +import static org.testcontainers.shaded.org.hamcrest.Matchers.not; +import static org.testcontainers.shaded.org.hamcrest.Matchers.equalTo; +import static org.testcontainers.shaded.org.hamcrest.Matchers.allOf; + +/** + * @author Stian Thorgersen + */ +public class ClientTest extends AbstractAdminClientTest { + + @Override + public List getRealmsForImport() { + return super.getRealmsForImport(); + } + + @Test + public void getRealms() { + List realms = adminClient.realms().findAll(); + Assert.assertNames(realms, "master", REALM_NAME, "test"); + } + + @Test + public void getClients() { + Assert.assertNames(realm.clients().findAll(), "account", "account-console", "realm-management", "security-admin-console", "broker", Constants.ADMIN_CLI_CLIENT_ID); + } + + @Test + @KeycloakVersion(min = "26.0.0") + public void getRealmClients() { + assertTrue(realm.clients().findAll().stream().filter(client-> client.getAttributes().get(Constants.REALM_CLIENT).equals("true")) + .map(ClientRepresentation::getClientId) + .allMatch(clientId -> clientId.equals(Constants.REALM_MANAGEMENT_CLIENT_ID) || clientId.equals(Constants.BROKER_SERVICE_CLIENT_ID) || clientId.endsWith("-realm"))); + } + + private ClientRepresentation createClient() { + return createClient(null); + } + + private ClientRepresentation createClient(String protocol) { + ClientRepresentation rep = new ClientRepresentation(); + rep.setClientId("my-app"); + rep.setDescription("my-app description"); + rep.setEnabled(true); + rep.setPublicClient(true); + if (protocol != null) { + rep.setProtocol(protocol); + } + Response response = realm.clients().create(rep); + String id = ApiUtil.getCreatedId(response); + response.close(); + ClientRepresentation found = ApiUtil.findClientResourceByClientId(realm, "my-app").toRepresentation(); + + assertEquals("my-app", found.getClientId()); + rep.setId(id); + + return rep; + } + + private ClientRepresentation createClientNonPublic() { + ClientRepresentation rep = new ClientRepresentation(); + rep.setClientId("my-app"); + rep.setDescription("my-app description"); + rep.setEnabled(true); + rep.setPublicClient(false); + Response response = realm.clients().create(rep); + response.close(); + String id = ApiUtil.getCreatedId(response); + ClientRepresentation found = ApiUtil.findClientResourceByClientId(realm, "my-app").toRepresentation(); + + assertEquals("my-app", found.getClientId()); + rep.setId(id); + + return rep; + } + + @Test + public void createClientVerifyWithSecret() { + String id = createClientNonPublic().getId(); + + ClientResource client = realm.clients().get(id); + assertNotNull(client); + assertNotNull(client.toRepresentation().getSecret()); + Assert.assertNames(realm.clients().findAll(), "account", "account-console", "realm-management", "security-admin-console", "broker", "my-app", Constants.ADMIN_CLI_CLIENT_ID); + realm.clients().get(id).remove(); + } + + @Test + public void createClientVerify() { + String id = createClient().getId(); + + ClientResource client = realm.clients().get(id); + assertNotNull(client); + assertNull(client.toRepresentation().getSecret()); + Assert.assertNames(realm.clients().findAll(), "account", "account-console", "realm-management", "security-admin-console", "broker", "my-app", Constants.ADMIN_CLI_CLIENT_ID); + realm.clients().get(id).remove(); + } + + @Test + public void testInvalidUrlClientValidation() { + testClientUriValidation("Root URL is not a valid URL", + "Base URL is not a valid URL", + "Backchannel logout URL is not a valid URL", + null, + "invalid", "myapp://some-fake-app"); + } + + @Test + public void testIllegalSchemeClientValidation() { + testClientUriValidation("Root URL uses an illegal scheme", + "Base URL uses an illegal scheme", + "Backchannel logout URL uses an illegal scheme", + "A redirect URI uses an illegal scheme", + "data:text/html;base64,PHNjcmlwdD5jb25maXJtKGRvY3VtZW50LmRvbWFpbik7PC9zY3JpcHQ+", + "javascript:confirm(document.domain)/*" + ); + } + + // KEYCLOAK-3421 + @Test + public void testFragmentProhibitedClientValidation() { + testClientUriValidation("Root URL must not contain an URL fragment", + null, + null, + "Redirect URIs must not contain an URI fragment", + "http://redhat.com/abcd#someFragment" + ); + } + + private void testClientUriValidation(String expectedRootUrlError, String expectedBaseUrlError, String expectedBackchannelLogoutUrlError, String expectedRedirectUrisError, String... testUrls) { + testClientUriValidation(false, expectedRootUrlError, expectedBaseUrlError, expectedBackchannelLogoutUrlError, expectedRedirectUrisError, testUrls); + testClientUriValidation(true, expectedRootUrlError, expectedBaseUrlError, expectedBackchannelLogoutUrlError, expectedRedirectUrisError, testUrls); + } + + private void testClientUriValidation(boolean create, String expectedRootUrlError, String expectedBaseUrlError, String expectedBackchannelLogoutUrlError, String expectedRedirectUrisError, String... testUrls) { + ClientRepresentation rep; + if (create) { + rep = new ClientRepresentation(); + rep.setClientId("my-app2"); + rep.setEnabled(true); + } + else { + rep = createClient(); + } + + for (String testUrl : testUrls) { + if (expectedRootUrlError != null) { + rep.setRootUrl(testUrl); + createOrUpdateClientExpectingValidationErrors(rep, create, expectedRootUrlError); + } + rep.setRootUrl(null); + + if (expectedBaseUrlError != null) { + rep.setBaseUrl(testUrl); + createOrUpdateClientExpectingValidationErrors(rep, create, expectedBaseUrlError); + } + rep.setBaseUrl(null); + rep.setAttributes(new HashMap<>()); + if (expectedBackchannelLogoutUrlError != null) { + rep.getAttributes().put("backchannel.logout.url", testUrl); + createOrUpdateClientExpectingValidationErrors(rep, create, expectedBackchannelLogoutUrlError); + } + rep.setAttributes(new HashMap<>()); + rep.getAttributes().put("backchannel.logout.url", null); + + if (expectedRedirectUrisError != null) { + rep.setRedirectUris(Collections.singletonList(testUrl)); + createOrUpdateClientExpectingValidationErrors(rep, create, expectedRedirectUrisError); + } + rep.setRedirectUris(null); + + if (expectedRootUrlError != null) rep.setRootUrl(testUrl); + if (expectedBaseUrlError != null) rep.setBaseUrl(testUrl); + if (expectedRedirectUrisError != null) rep.setRedirectUris(Collections.singletonList(testUrl)); + createOrUpdateClientExpectingValidationErrors(rep, create, expectedRootUrlError, expectedBaseUrlError, expectedRedirectUrisError); + + rep.setRootUrl(null); + rep.setBaseUrl(null); + rep.setRedirectUris(null); + } + if(!create) { + realm.clients().get(rep.getId()).remove(); + } + } + + private void createOrUpdateClientExpectingValidationErrors(ClientRepresentation rep, boolean create, String... expectedErrors) { + Response response = null; + if (create) { + response = realm.clients().create(rep); + } + else { + try { + realm.clients().get(rep.getId()).update(rep); + fail("Expected exception"); + } + catch (BadRequestException e) { + response = e.getResponse(); + } + } + + expectedErrors = Arrays.stream(expectedErrors).filter(Objects::nonNull).toArray(String[]::new); + + assertEquals(response.getStatus(), 400); + OAuth2ErrorRepresentation errorRep = response.readEntity(OAuth2ErrorRepresentation.class); + List actualErrors = asList(errorRep.getErrorDescription().split("; ")); + assertThat(actualErrors, containsInAnyOrder(expectedErrors)); + assertEquals("invalid_input", errorRep.getError()); + } + + @Test + public void removeClient() { + String id = createClient().getId(); + + assertNotNull(ApiUtil.findClientByClientId(realm, "my-app")); + realm.clients().get(id).remove(); + assertNull(ApiUtil.findClientResourceByClientId(realm, "my-app")); + } + + @Test + public void removeInternalClientExpectingBadRequestException() { + final String testRealmClientId = ApiUtil.findClientByClientId(adminClient.realm("master"), realm.toRepresentation().getRealm() + "-realm") + .toRepresentation().getId(); + + assertThrows(BadRequestException.class, + () -> adminClient.realm("master").clients().get(testRealmClientId).remove()); + + Constants.defaultClients.forEach(defaultClient -> { + final String defaultClientId = ApiUtil.findClientByClientId(realm, defaultClient) + .toRepresentation().getId(); + + assertThrows(BadRequestException.class, + () -> realm.clients().get(defaultClientId).remove()); + }); + } + + @Test + public void getClientRepresentation() { + String id = createClient().getId(); + + ClientRepresentation rep = realm.clients().get(id).toRepresentation(); + assertEquals(id, rep.getId()); + assertEquals("my-app", rep.getClientId()); + assertTrue(rep.isEnabled()); + realm.clients().get(id).remove(); + } + + /** + * See KEYCLOAK-1918 + */ + @Test + public void getClientDescription() { + String id = createClient().getId(); + + ClientRepresentation rep = realm.clients().get(id).toRepresentation(); + assertEquals(id, rep.getId()); + assertEquals("my-app description", rep.getDescription()); + realm.clients().get(id).remove(); + } + + @Test + public void getClientSessions() throws Exception { + + oauth.realm("test"); + oauth.clientId("direct-grant"); + oauth.redirectUri(ServerURLs.AUTH_SERVER_URL + "/realms/test/app"); + OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("password", "test-user@localhost", "password"); + assertEquals(200, response.getStatusCode()); + + ClientResource app = ApiUtil.findClientByClientId(adminClient.realm("test"), "direct-grant"); + + assertEquals(1, (long) app.getApplicationSessionCount().get("count")); + + List userSessions = app.getUserSessions(0, 100); + assertEquals(1, userSessions.size()); + assertEquals(1, userSessions.get(0).getClients().size()); + } + + @Test + public void getAllClients() { + List allClients = realm.clients().findAll(); + assertNotNull(allClients); + assertFalse(allClients.isEmpty()); + } + + @Test + public void getAllClientsSearchAndPagination() { + Set ids = new HashSet<>(); + try { + for (int i = 1; i <= 10; i++) { + ClientRepresentation c = ClientBuilder.create().clientId("ccx-" + (i < 10 ? "0" + i : i)).build(); + Response response = realm.clients().create(c); + ids.add(ApiUtil.getCreatedId(response)); + response.close(); + } + + assertPaginatedClients(1, 10, realm.clients().findAll("ccx-", null, true, 0, 100)); + assertPaginatedClients(1, 5, realm.clients().findAll("ccx-", null, true, 0, 5)); + assertPaginatedClients(6, 10, realm.clients().findAll("ccx-", null, true, 5, 5)); + } finally { + ids.stream().forEach(id -> realm.clients().get(id).remove()); + } + } + + private void assertPaginatedClients(int start, int end, List actual) { + List expected = new LinkedList<>(); + for (int i = start; i <= end; i++) { + expected.add("ccx-" + (i < 10 ? "0" + i : i)); + } + List a = actual.stream().map(rep -> rep.getClientId()).collect(Collectors.toList()); + assertThat(a, is(expected)); + + } + + @Test + public void getClientById() { + createClient(); + ClientRepresentation rep = ApiUtil.findClientResourceByClientId(realm, "my-app").toRepresentation(); + ClientRepresentation gotById = realm.clients().get(rep.getId()).toRepresentation(); + assertClient(rep, gotById); + realm.clients().get(rep.getId()).remove(); + } + + @Test + // KEYCLOAK-1110 + public void deleteDefaultRole() { + ClientRepresentation rep = createClient(); + String id = rep.getId(); + + RoleRepresentation role = new RoleRepresentation("test", "test", false); + realm.clients().get(id).roles().create(role); + + role = realm.clients().get(id).roles().get("test").toRepresentation(); + + realm.roles().get(Constants.DEFAULT_ROLES_ROLE_PREFIX + "-" + REALM_NAME).addComposites(Collections.singletonList(role)); + + assertThat(realm.roles().get(Constants.DEFAULT_ROLES_ROLE_PREFIX + "-" + REALM_NAME).getRoleComposites().stream().map(RoleRepresentation::getName).collect(Collectors.toSet()), + hasItem(role.getName())); + + realm.clients().get(id).roles().deleteRole("test"); + + assertThat(realm.roles().get(Constants.DEFAULT_ROLES_ROLE_PREFIX + "-" + REALM_NAME).getRoleComposites().stream().map(RoleRepresentation::getName).collect(Collectors.toSet()), + not(hasItem(role))); + realm.clients().get(id).remove(); + } + + @Test + public void testProtocolMappers() { + String clientDbId = createClient().getId(); + ProtocolMappersResource mappersResource = ApiUtil.findClientByClientId(realm, "my-app").getProtocolMappers(); + + protocolMappersTest(clientDbId, mappersResource); + realm.clients().get(clientDbId).remove(); + } + + @Test + public void updateClient() { + ClientRepresentation client = createClient(); + + ClientRepresentation newClient = new ClientRepresentation(); + newClient.setId(client.getId()); + newClient.setClientId(client.getClientId()); + newClient.setBaseUrl("http://baseurl"); + + ClientResource clientRes = realm.clients().get(client.getId()); + clientRes.update(newClient); + + ClientRepresentation storedClient = clientRes.toRepresentation(); + + assertNull(storedClient.getSecret()); + assertClient(client, storedClient); + + client.setPublicClient(false); + newClient.setPublicClient(client.isPublicClient()); + client.setSecret("new-secret"); + newClient.setSecret(client.getSecret()); + + clientRes.update(newClient); + + newClient.setSecret("**********"); // secrets are masked in events + + storedClient = clientRes.toRepresentation(); + assertClient(client, storedClient); + + storedClient.setSecret(null); + storedClient.getAttributes().put("backchannel.logout.url", ""); + + clientRes.update(storedClient); + storedClient = clientRes.toRepresentation(); + + assertFalse(storedClient.getAttributes().containsKey("backchannel.logout.url")); + assertClient(client, storedClient); + realm.clients().get(client.getId()).remove(); + } + + @Test + public void serviceAccount() { + Response response = realm.clients().create(ClientBuilder.create().clientId("serviceClient").serviceAccount().build()); + String id = ApiUtil.getCreatedId(response); + response.close(); + UserRepresentation userRep = realm.clients().get(id).getServiceAccountUser(); + assertThat("service-account-serviceclient", equalTo(userRep.getUsername())); + // KEYCLOAK-11197 service accounts are no longer created with a placeholder e-mail. + assertNull(userRep.getEmail()); + realm.clients().get(id).remove(); + } + + private ClientRepresentation createAppClient() { + String redirectUri = ServerURLs.AUTH_SERVER_URL + "/auth/realms/" + REALM_NAME + "/app/auth"; + + ClientRepresentation client = new ClientRepresentation(); + client.setClientId("test-app"); + client.setAdminUrl(ServerURLs.AUTH_SERVER_URL + "/auth/realms/master/app/admin"); + client.setRedirectUris(Collections.singletonList(redirectUri)); + client.setSecret("secret"); + client.setProtocol("openid-connect"); + client.setDirectAccessGrantsEnabled(true); + int notBefore = Time.currentTime() - 60; + client.setNotBefore(notBefore); + + Response response = realm.clients().create(client); + String id = ApiUtil.getCreatedId(response); + response.close(); + + client.setSecret("**********"); // secrets are masked in events + + client.setId(id); + return client; + } + + @Test + public void offlineUserSessions() throws IOException { + ClientRepresentation client = createAppClient(); + String id = client.getId(); + + Response response = realm.users().create(UserBuilder.create().username("testuser").build()); + String userId = ApiUtil.getCreatedId(response); + response.close(); + + realm.users().get(userId).resetPassword(CredentialBuilder.create().password("password").build()); + + Map offlineSessionCount = realm.clients().get(id).getOfflineSessionCount(); + assertEquals(Long.valueOf(0), offlineSessionCount.get("count")); + + List userSessions = realm.users().get(userId).getOfflineSessions(id); + assertEquals(0, userSessions.size(), "There should be no offline sessions"); + + oauth.realm(REALM_NAME); + oauth.clientId(client.getClientId()); + oauth.redirectUri(client.getRedirectUris().get(0)); + oauth.scope(OAuth2Constants.OFFLINE_ACCESS); + + OAuthClient.AccessTokenResponse accessTokenResponse = oauth.doGrantAccessTokenRequest("secret", "testuser", "password"); + assertEquals(200, accessTokenResponse.getStatusCode()); + + + offlineSessionCount = realm.clients().get(id).getOfflineSessionCount(); + assertEquals(Long.valueOf(1), offlineSessionCount.get("count")); + + List offlineUserSessions = realm.clients().get(id).getOfflineUserSessions(0, 100); + assertEquals(1, offlineUserSessions.size()); + assertEquals("testuser", offlineUserSessions.get(0).getUsername()); + assertThat(offlineUserSessions.get(0).getLastAccess(), + allOf(greaterThan(Time.currentTimeMillis() - 10000L), lessThan(Time.currentTimeMillis()))); + + userSessions = realm.users().get(userId).getOfflineSessions(id); + assertEquals(1, userSessions.size(), "There should be one offline session"); + assertOfflineSession(offlineUserSessions.get(0), userSessions.get(0)); + realm.clients().get(id).remove(); + } + + private void assertOfflineSession(UserSessionRepresentation expected, UserSessionRepresentation actual) { + assertEquals( expected.getId(), actual.getId(), "id"); + assertEquals(expected.getUserId(), actual.getUserId(), "userId"); + assertEquals(expected.getUsername(), actual.getUsername(), "userName"); + assertEquals(expected.getClients(), actual.getClients(), "clients"); + } + + @Test + public void scopes() { + Response response = realm.clients().create(ClientBuilder.create().clientId("client").fullScopeEnabled(false).build()); + String id = ApiUtil.getCreatedId(response); + response.close(); + + RoleMappingResource scopesResource = realm.clients().get(id).getScopeMappings(); + + RoleRepresentation roleRep1 = createRealmRole("realm-composite"); + RoleRepresentation roleRep2 = createRealmRole("realm-child"); + + realm.roles().get("realm-composite").addComposites(Collections.singletonList(roleRep2)); + + String accountMgmtId = realm.clients().findByClientId(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID).get(0).getId(); + RoleRepresentation viewAccountRoleRep = realm.clients().get(accountMgmtId).roles().get(AccountRoles.VIEW_PROFILE).toRepresentation(); + + scopesResource.realmLevel().add(Collections.singletonList(roleRep1)); + scopesResource.clientLevel(accountMgmtId).add(Collections.singletonList(viewAccountRoleRep)); + Assert.assertNames(scopesResource.realmLevel().listAll(), "realm-composite"); + Assert.assertNames(scopesResource.realmLevel().listEffective(), "realm-composite", "realm-child"); + Assert.assertNames(scopesResource.realmLevel().listAvailable(), "realm-child", "offline_access", + Constants.AUTHZ_UMA_AUTHORIZATION, Constants.DEFAULT_ROLES_ROLE_PREFIX + "-" + REALM_NAME); + + Assert.assertNames(scopesResource.clientLevel(accountMgmtId).listAll(), AccountRoles.VIEW_PROFILE); + Assert.assertNames(scopesResource.clientLevel(accountMgmtId).listEffective(), AccountRoles.VIEW_PROFILE); + + Assert.assertNames(scopesResource.clientLevel(accountMgmtId).listAvailable(), AccountRoles.MANAGE_ACCOUNT, AccountRoles.MANAGE_ACCOUNT_LINKS, AccountRoles.VIEW_APPLICATIONS, AccountRoles.VIEW_CONSENT, AccountRoles.MANAGE_CONSENT, AccountRoles.DELETE_ACCOUNT, AccountRoles.VIEW_GROUPS); + + Assert.assertNames(scopesResource.getAll().getRealmMappings(), "realm-composite"); + Assert.assertNames(scopesResource.getAll().getClientMappings().get(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID).getMappings(), AccountRoles.VIEW_PROFILE); + + scopesResource.realmLevel().remove(Collections.singletonList(roleRep1)); + scopesResource.clientLevel(accountMgmtId).remove(Collections.singletonList(viewAccountRoleRep)); + + Assert.assertNames(scopesResource.realmLevel().listAll()); + Assert.assertNames(scopesResource.realmLevel().listEffective()); + Assert.assertNames(scopesResource.realmLevel().listAvailable(), "offline_access", Constants.AUTHZ_UMA_AUTHORIZATION, "realm-composite", "realm-child", Constants.DEFAULT_ROLES_ROLE_PREFIX + "-" + REALM_NAME); + Assert.assertNames(scopesResource.clientLevel(accountMgmtId).listAll()); + + Assert.assertNames(scopesResource.clientLevel(accountMgmtId).listAvailable(), AccountRoles.VIEW_PROFILE, AccountRoles.MANAGE_ACCOUNT, AccountRoles.MANAGE_ACCOUNT_LINKS, AccountRoles.VIEW_APPLICATIONS, AccountRoles.VIEW_CONSENT, AccountRoles.MANAGE_CONSENT, AccountRoles.DELETE_ACCOUNT, AccountRoles.VIEW_GROUPS); + + Assert.assertNames(scopesResource.clientLevel(accountMgmtId).listEffective()); + realm.clients().get(id).remove(); + realm.rolesById().deleteRole(roleRep1.getId()); + realm.rolesById().deleteRole(roleRep2.getId()); + } + + /** + * Test for KEYCLOAK-10603. + */ + @Test + public void rolesCanBeAddedToScopeEvenWhenTheyAreAlreadyIndirectlyAssigned() { + Response response = + realm.clients().create(ClientBuilder.create().clientId("test-client").fullScopeEnabled(false).build()); + String testedClientUuid = ApiUtil.getCreatedId(response); + response.close(); + + RoleRepresentation realmComposite = createRealmRole("realm-composite"); + RoleRepresentation realmChild = createRealmRole("realm-child"); + + realm.roles().get("realm-composite") + .addComposites(Collections.singletonList(realm.roles().get("realm-child").toRepresentation())); + + response = realm.clients().create(ClientBuilder.create().clientId("role-container-client").build()); + String roleContainerClientUuid = ApiUtil.getCreatedId(response); + response.close(); + + RoleRepresentation clientCompositeRole = RoleBuilder.create().name("client-composite").build(); + realm.clients().get(roleContainerClientUuid).roles().create(clientCompositeRole); + realm.clients().get(roleContainerClientUuid).roles().create(RoleBuilder.create().name("client-child").build()); + realm.clients().get(roleContainerClientUuid).roles().get("client-composite").addComposites(Collections + .singletonList( + realm.clients().get(roleContainerClientUuid).roles().get("client-child").toRepresentation())); + + // Make indirect assignments: assign composite roles + RoleMappingResource scopesResource = realm.clients().get(testedClientUuid).getScopeMappings(); + scopesResource.realmLevel() + .add(Collections.singletonList(realm.roles().get("realm-composite").toRepresentation())); + scopesResource.clientLevel(roleContainerClientUuid).add(Collections + .singletonList(realm.clients().get(roleContainerClientUuid).roles().get("client-composite") + .toRepresentation())); + + // check state before making the direct assignments + Assert.assertNames(scopesResource.realmLevel().listAll(), "realm-composite"); + Assert.assertNames(scopesResource.realmLevel().listAvailable(), "realm-child", "offline_access", + Constants.AUTHZ_UMA_AUTHORIZATION, Constants.DEFAULT_ROLES_ROLE_PREFIX + "-" + REALM_NAME); + Assert.assertNames(scopesResource.realmLevel().listEffective(), "realm-composite", "realm-child"); + + Assert.assertNames(scopesResource.clientLevel(roleContainerClientUuid).listAll(), "client-composite"); + Assert.assertNames(scopesResource.clientLevel(roleContainerClientUuid).listAvailable(), "client-child"); + Assert.assertNames(scopesResource.clientLevel(roleContainerClientUuid).listEffective(), "client-composite", + "client-child"); + + // Make direct assignments for roles which are already indirectly assigned + scopesResource.realmLevel().add(Collections.singletonList(realm.roles().get("realm-child").toRepresentation())); + scopesResource.clientLevel(roleContainerClientUuid).add(Collections + .singletonList( + realm.clients().get(roleContainerClientUuid).roles().get("client-child").toRepresentation())); + + // List realm roles + Assert.assertNames(scopesResource.realmLevel().listAll(), "realm-composite", "realm-child"); + Assert.assertNames(scopesResource.realmLevel().listAvailable(), "offline_access", + Constants.AUTHZ_UMA_AUTHORIZATION, Constants.DEFAULT_ROLES_ROLE_PREFIX + "-" + REALM_NAME); + Assert.assertNames(scopesResource.realmLevel().listEffective(), "realm-composite", "realm-child"); + + // List client roles + Assert.assertNames(scopesResource.clientLevel(roleContainerClientUuid).listAll(), "client-composite", + "client-child"); + Assert.assertNames(scopesResource.clientLevel(roleContainerClientUuid).listAvailable()); + Assert.assertNames(scopesResource.clientLevel(roleContainerClientUuid).listEffective(), "client-composite", + "client-child"); + realm.clients().get(testedClientUuid).remove(); + realm.clients().get(roleContainerClientUuid).remove(); + realm.rolesById().deleteRole(realmComposite.getId()); + realm.rolesById().deleteRole(realmChild.getId()); + } + + @Test + public void scopesRoleRemoval() { + // clientA to test scope mappins + Response response = realm.clients().create(ClientBuilder.create().clientId("clientA").fullScopeEnabled(false).build()); + String idA = ApiUtil.getCreatedId(response); + response.close(); + + // clientB to create a client role for clientA + response = realm.clients().create(ClientBuilder.create().clientId("clientB").fullScopeEnabled(false).build()); + String idB = ApiUtil.getCreatedId(response); + response.close(); + RoleMappingResource scopesResource = realm.clients().get(idA).getScopeMappings(); + + // create a realm role and a role in clientB + RoleRepresentation realmRoleRep = createRealmRole("realm-role"); + RoleRepresentation clientBRoleRep = RoleBuilder.create().name("clientB-role").build(); + realm.clients().get(idB).roles().create(clientBRoleRep); + + // assing to clientA both roles to the scope mappings + realmRoleRep = realm.roles().get(realmRoleRep.getName()).toRepresentation(); + clientBRoleRep = realm.clients().get(idB).roles().get(clientBRoleRep.getName()).toRepresentation(); + scopesResource.realmLevel().add(Collections.singletonList(realmRoleRep)); + scopesResource.clientLevel(idB).add(Collections.singletonList(clientBRoleRep)); + + // assert the roles are there + Assert.assertNames(scopesResource.realmLevel().listAll(), realmRoleRep.getName()); + Assert.assertNames(scopesResource.clientLevel(idB).listAll(), clientBRoleRep.getName()); + + // delete realm role and check everything is refreshed ok + realm.roles().deleteRole(realmRoleRep.getName()); + Assert.assertNames(scopesResource.realmLevel().listAll()); + Assert.assertNames(scopesResource.clientLevel(idB).listAll(), clientBRoleRep.getName()); + + // delete client role and check everything is refreshed ok + realm.clients().get(idB).roles().deleteRole(clientBRoleRep.getName()); + Assert.assertNames(scopesResource.realmLevel().listAll()); + Assert.assertNames(scopesResource.clientLevel(idB).listAll()); + + realm.clients().get(idA).remove(); + realm.clients().get(idB).remove(); + } + + public void protocolMappersTest(String clientDbId, ProtocolMappersResource mappersResource) { + // assert default mappers found + List protocolMappers = mappersResource.getMappers(); + + String emailMapperId = null; + String usernameMapperId = null; + String fooMapperId = null; + for (ProtocolMapperRepresentation mapper : protocolMappers) { + if (mapper.getName().equals("email")) { + emailMapperId = mapper.getId(); + } else if (mapper.getName().equals("username")) { + usernameMapperId = mapper.getId(); + } else if (mapper.getName().equals("foo")) { + fooMapperId = mapper.getId(); + } + } + + // Builtin mappers are not here + assertNull(emailMapperId); + assertNull(usernameMapperId); + + assertNull(fooMapperId); + + // Create foo mapper + ProtocolMapperRepresentation fooMapper = new ProtocolMapperRepresentation(); + fooMapper.setName("foo"); + fooMapper.setProtocol("openid-connect"); + fooMapper.setProtocolMapper("oidc-hardcoded-claim-mapper"); + Response response = mappersResource.createMapper(fooMapper); + String location = response.getLocation().toString(); + fooMapperId = location.substring(location.lastIndexOf("/") + 1); + response.close(); + + fooMapper = mappersResource.getMapperById(fooMapperId); + assertEquals(fooMapper.getName(), "foo"); + + // Update foo mapper + mappersResource.update(fooMapperId, fooMapper); + + fooMapper = mappersResource.getMapperById(fooMapperId); + + // Remove foo mapper + mappersResource.delete(fooMapperId);try { + mappersResource.getMapperById(fooMapperId); + fail("Not expected to find deleted mapper"); + } catch (NotFoundException nfe) { + } + } + + @Test + public void updateClientWithProtocolMapper() { + ClientRepresentation rep = new ClientRepresentation(); + rep.setClientId("my-app"); + + ProtocolMapperRepresentation fooMapper = new ProtocolMapperRepresentation(); + fooMapper.setName("foo"); + fooMapper.setProtocol("openid-connect"); + fooMapper.setProtocolMapper("oidc-hardcoded-claim-mapper"); + rep.setProtocolMappers(Collections.singletonList(fooMapper)); + + Response response = realm.clients().create(rep); + String id = ApiUtil.getCreatedId(response); + response.close(); + + ClientResource clientResource = realm.clients().get(id); + assertNotNull(clientResource); + ClientRepresentation client = clientResource.toRepresentation(); + List protocolMappers = client.getProtocolMappers(); + assertEquals(1, protocolMappers.size()); + ProtocolMapperRepresentation mapper = protocolMappers.get(0); + assertEquals("foo", mapper.getName()); + + ClientRepresentation newClient = new ClientRepresentation(); + newClient.setId(client.getId()); + newClient.setClientId(client.getClientId()); + + ProtocolMapperRepresentation barMapper = new ProtocolMapperRepresentation(); + barMapper.setName("bar"); + barMapper.setProtocol("openid-connect"); + barMapper.setProtocolMapper("oidc-hardcoded-role-mapper"); + protocolMappers.add(barMapper); + newClient.setProtocolMappers(protocolMappers); + + realm.clients().get(client.getId()).update(newClient); + + ClientRepresentation storedClient = realm.clients().get(client.getId()).toRepresentation(); + assertClient(client, storedClient); + realm.clients().get(id).remove(); + } + + public static void assertClient(ClientRepresentation client, ClientRepresentation storedClient) { + if (client.getClientId() != null) Assert.assertEquals(client.getClientId(), storedClient.getClientId()); + if (client.getName() != null) Assert.assertEquals(client.getName(), storedClient.getName()); + if (client.isEnabled() != null) Assert.assertEquals(client.isEnabled(), storedClient.isEnabled()); + if (client.isAlwaysDisplayInConsole() != null) Assert.assertEquals(client.isAlwaysDisplayInConsole(), storedClient.isAlwaysDisplayInConsole()); + if (client.isBearerOnly() != null) Assert.assertEquals(client.isBearerOnly(), storedClient.isBearerOnly()); + if (client.isPublicClient() != null) Assert.assertEquals(client.isPublicClient(), storedClient.isPublicClient()); + if (client.isFullScopeAllowed() != null) Assert.assertEquals(client.isFullScopeAllowed(), storedClient.isFullScopeAllowed()); + if (client.getRootUrl() != null) Assert.assertEquals(client.getRootUrl(), storedClient.getRootUrl()); + if (client.getAdminUrl() != null) Assert.assertEquals(client.getAdminUrl(), storedClient.getAdminUrl()); + if (client.getBaseUrl() != null) Assert.assertEquals(client.getBaseUrl(), storedClient.getBaseUrl()); + if (client.isSurrogateAuthRequired() != null) Assert.assertEquals(client.isSurrogateAuthRequired(), storedClient.isSurrogateAuthRequired()); + if (client.getClientAuthenticatorType() != null) Assert.assertEquals(client.getClientAuthenticatorType(), storedClient.getClientAuthenticatorType()); + if (client.getSecret() != null) Assert.assertEquals(client.getSecret(), storedClient.getSecret()); + + if (client.getNotBefore() != null) { + Assert.assertEquals(client.getNotBefore(), storedClient.getNotBefore()); + } + if (client.getDefaultRoles() != null) { + Set set = new HashSet(); + for (String val : client.getDefaultRoles()) { + set.add(val); + } + Set storedSet = new HashSet(); + for (String val : storedClient.getDefaultRoles()) { + storedSet.add(val); + } + + Assert.assertEquals(set, storedSet); + } + + List redirectUris = client.getRedirectUris(); + if (redirectUris != null) { + Set set = new HashSet(); + for (String val : client.getRedirectUris()) { + set.add(val); + } + Set storedSet = new HashSet(); + for (String val : storedClient.getRedirectUris()) { + storedSet.add(val); + } + + Assert.assertEquals(set, storedSet); + } + + List webOrigins = client.getWebOrigins(); + if (webOrigins != null) { + Set set = new HashSet(); + for (String val : client.getWebOrigins()) { + set.add(val); + } + Set storedSet = new HashSet(); + for (String val : storedClient.getWebOrigins()) { + storedSet.add(val); + } + + Assert.assertEquals(set, storedSet); + } + + List protocolMappers = client.getProtocolMappers(); + if(protocolMappers != null){ + Set set = protocolMappers.stream() + .map(ProtocolMapperRepresentation::getName) + .collect(Collectors.toSet()); + Set storedSet = storedClient.getProtocolMappers().stream() + .map(ProtocolMapperRepresentation::getName) + .collect(Collectors.toSet()); + + Assert.assertEquals(set, storedSet); + } + } +} diff --git a/testsuite/admin-client-tests/src/test/java/org/keycloak/client/testsuite/RealmRolesTest.java b/testsuite/admin-client-tests/src/test/java/org/keycloak/client/testsuite/RealmRolesTest.java new file mode 100644 index 0000000..79127bc --- /dev/null +++ b/testsuite/admin-client-tests/src/test/java/org/keycloak/client/testsuite/RealmRolesTest.java @@ -0,0 +1,533 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * 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. + */ + +package org.keycloak.client.testsuite; + +import jakarta.ws.rs.BadRequestException; +import jakarta.ws.rs.ClientErrorException; +import jakarta.ws.rs.NotFoundException; +import jakarta.ws.rs.core.Response; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.keycloak.admin.client.resource.RoleResource; +import org.keycloak.admin.client.resource.RolesResource; +import org.keycloak.admin.client.resource.UserResource; +import org.keycloak.models.Constants; +import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.GroupRepresentation; +import org.keycloak.representations.idm.RoleRepresentation; +import org.keycloak.representations.idm.UserRepresentation; +import org.keycloak.testsuite.util.ApiUtil; +import org.keycloak.testsuite.util.ClientBuilder; +import org.keycloak.testsuite.util.RoleBuilder; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import static io.smallrye.common.constraint.Assert.assertFalse; +import static io.smallrye.common.constraint.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.fail; +import static org.testcontainers.shaded.org.hamcrest.MatcherAssert.assertThat; +import static org.testcontainers.shaded.org.hamcrest.Matchers.lessThan; +import static org.testcontainers.shaded.org.hamcrest.Matchers.greaterThan; +import static org.testcontainers.shaded.org.hamcrest.Matchers.hasItem; +import static org.testcontainers.shaded.org.hamcrest.Matchers.empty; +import static org.testcontainers.shaded.org.hamcrest.Matchers.is; +import static org.testcontainers.shaded.org.hamcrest.Matchers.not; +import static org.testcontainers.shaded.org.hamcrest.Matchers.hasSize; +import static org.testcontainers.shaded.org.hamcrest.Matchers.allOf; + + +/** + * @author Stian Thorgersen + */ +public class RealmRolesTest extends AbstractAdminClientTest { + + + RoleRepresentation roleA = RoleBuilder.create().name("role-a").description("Role A").attributes(ROLE_A_ATTRIBUTES).build(); + RoleRepresentation roleB = RoleBuilder.create().name("role-b").description("Role B").build(); + RoleRepresentation roleWithUsers = RoleBuilder.create().name("role-with-users").description("Role with users").build(); + RoleRepresentation roleWithoutUsers = RoleBuilder.create().name("role-without-users").description("role-without-users").build(); + + RoleRepresentation roleC = RoleBuilder.create().name("role-c").description("Role C").build(); + + UserRepresentation userRoleMember; + GroupRepresentation groupMember; + + private static final Map> ROLE_A_ATTRIBUTES = + Collections.singletonMap("role-a-attr-key1", Collections.singletonList("role-a-attr-val1")); + + private RolesResource resource; + + private Map ids = new HashMap<>(); + private String clientUuid; + + @BeforeEach + public void before() { + roleA = createRealmRole(roleA); + roleB = createRealmRole(roleB); + roleWithUsers = createRealmRole(roleWithUsers); + roleWithoutUsers = createRealmRole(roleWithoutUsers); + + ClientRepresentation clientRep = ClientBuilder.create().clientId("client-a").build(); + try (Response response = adminClient.realm(REALM_NAME).clients().create(clientRep)) { + clientUuid = ApiUtil.getCreatedId(response); + } + + adminClient.realm(REALM_NAME).clients().get(clientUuid).roles().create(roleC); + roleC = adminClient.realm(REALM_NAME).clients().get(clientUuid).roles().get(roleC.getName()).toRepresentation(); + + for (RoleRepresentation r : adminClient.realm(REALM_NAME).roles().list()) { + ids.put(r.getName(), r.getId()); + } + + for (RoleRepresentation r : adminClient.realm(REALM_NAME).clients().get(clientUuid).roles().list()) { + ids.put(r.getName(), r.getId()); + } + + userRoleMember = new UserRepresentation(); + userRoleMember.setUsername("test-role-member"); + userRoleMember.setEmail("test-role-member@test-role-member.com"); + userRoleMember.setRequiredActions(Collections.emptyList()); + userRoleMember.setEnabled(true); + adminClient.realm(REALM_NAME).users().create(userRoleMember); + userRoleMember = adminClient.realms().realm(REALM_NAME).users().search(userRoleMember.getUsername()).get(0); + groupMember = new GroupRepresentation(); + groupMember.setName("test-role-group"); + groupMember.setPath("/test-role-group"); + adminClient.realm(REALM_NAME).groups().add(groupMember); + groupMember = adminClient.realm(REALM_NAME).groups().groups().get(0); + resource = adminClient.realm(REALM_NAME).roles(); + } + + @AfterEach + public void after() { + try { + realm.clients().get(clientUuid).remove(); + realm.rolesById().deleteRole(roleA.getId()); + realm.rolesById().deleteRole(roleB.getId()); + realm.rolesById().deleteRole(roleWithUsers.getId()); + realm.rolesById().deleteRole(roleWithoutUsers.getId()); + + String groupId = adminClient.realm(REALM_NAME).groups().groups().get(0).getId(); + + adminClient.realm(REALM_NAME).groups().group(groupId).remove(); + adminClient.realms().realm(REALM_NAME).users().get(userRoleMember.getId()).remove(); + + } + catch (Exception e) { + + } + } + + private RoleRepresentation makeRole(String name) { + RoleRepresentation role = new RoleRepresentation(); + role.setName(name); + return role; + } + + @Test + public void getRole() { + RoleRepresentation role = resource.get("role-a").toRepresentation(); + assertNotNull(role); + assertEquals("role-a", role.getName()); + assertEquals("Role A", role.getDescription()); + assertEquals(ROLE_A_ATTRIBUTES, role.getAttributes()); + assertFalse(role.isComposite()); + } + + @Test + public void createRoleWithSameName() { + try { + resource.create(RoleBuilder.create().name("role-a").build()); + fail(); + } + catch (ClientErrorException e) { + } + } + + @Test + public void updateRole() { + RoleRepresentation roleOrig = resource.get("role-a").toRepresentation(); + RoleRepresentation role = resource.get("role-a").toRepresentation(); + + role.setName("role-a-new"); + role.setDescription("Role A New"); + Map> newAttributes = Collections.singletonMap("attrKeyNew", Collections.singletonList("attrValueNew")); + role.setAttributes(newAttributes); + + resource.get("role-a").update(role); + role = resource.get("role-a-new").toRepresentation(); + + assertNotNull(role); + assertEquals("role-a-new", role.getName()); + assertEquals("Role A New", role.getDescription()); + assertEquals(newAttributes, role.getAttributes()); + assertFalse(role.isComposite()); + + resource.get("role-a-new").update(roleOrig); + } + + @Test + public void deleteRole() { + assertNotNull(resource.get("role-a")); + resource.deleteRole("role-a"); + try { + resource.get("role-a").toRepresentation(); + fail("Expected 404"); + } catch (NotFoundException e) { + // expected + } + } + + @Test + public void composites() { + assertFalse(resource.get("role-a").toRepresentation().isComposite()); + assertEquals(0, resource.get("role-a").getRoleComposites().size()); + + List l = new LinkedList<>(); + l.add(RoleBuilder.create().id(ids.get("role-b")).build()); + l.add(RoleBuilder.create().id(ids.get("role-c")).build()); + resource.get("role-a").addComposites(l); + + Set composites = resource.get("role-a").getRoleComposites(); + + assertTrue(resource.get("role-a").toRepresentation().isComposite()); + Assert.assertNames(composites, "role-b", "role-c"); + + Set realmComposites = resource.get("role-a").getRealmRoleComposites(); + Assert.assertNames(realmComposites, "role-b"); + + Set clientComposites = resource.get("role-a").getClientRoleComposites(clientUuid); + Assert.assertNames(clientComposites, "role-c"); + + resource.get("role-a").deleteComposites(l); + assertFalse(resource.get("role-a").toRepresentation().isComposite()); + assertEquals(0, resource.get("role-a").getRoleComposites().size()); + } + + /** + * KEYCLOAK-2035 Verifies that Users assigned to Role are being properly retrieved as members in API endpoint for role membership + */ + @Test + public void testUsersInRole() { + RoleResource role = resource.get("role-with-users"); + + List users = adminClient.realm(REALM_NAME).users().search("test-role-member"); + assertEquals(1, users.size()); + UserResource user = adminClient.realm(REALM_NAME).users().get(users.get(0).getId()); + UserRepresentation userRep = user.toRepresentation(); + + RoleResource roleResource = adminClient.realm(REALM_NAME).roles().get(role.toRepresentation().getName()); + List rolesToAdd = new LinkedList<>(); + rolesToAdd.add(roleResource.toRepresentation()); + adminClient.realm(REALM_NAME).users().get(userRep.getId()).roles().realmLevel().add(rolesToAdd); + + roleResource = adminClient.realm(REALM_NAME).roles().get(role.toRepresentation().getName()); + assertEquals(Collections.singletonList("test-role-member"), extractUsernames(roleResource.getUserMembers())); + } + + private static List extractUsernames(Collection users) { + return users.stream().map(UserRepresentation::getUsername).collect(Collectors.toList()); + } + + /** + * KEYCLOAK-2035 Verifies that Role with no users assigned is being properly retrieved without members in API endpoint for role membership + */ + @Test + public void testUsersNotInRole() { + RoleResource role = resource.get("role-without-users"); + + role = adminClient.realm(REALM_NAME).roles().get(role.toRepresentation().getName()); + assertEquals(role.getUserMembers(), Collections.emptyList()); + } + + + /** + * KEYCLOAK-4978 Verifies that Groups assigned to Role are being properly retrieved as members in API endpoint for role membership + */ + @Test + public void testGroupsInRole() { + RoleResource role = resource.get("role-with-users"); + + List groups = adminClient.realm(REALM_NAME).groups().groups(); + GroupRepresentation groupRep = groups.stream().filter(g -> g.getPath().equals("/test-role-group")).findFirst().get(); + + RoleResource roleResource = adminClient.realm(REALM_NAME).roles().get(role.toRepresentation().getName()); + List rolesToAdd = new LinkedList<>(); + rolesToAdd.add(roleResource.toRepresentation()); + adminClient.realm(REALM_NAME).groups().group(groupRep.getId()).roles().realmLevel().add(rolesToAdd); + + roleResource = adminClient.realm(REALM_NAME).roles().get(role.toRepresentation().getName()); + + Set groupsInRole = roleResource.getRoleGroupMembers(); + assertTrue(groupsInRole.stream().filter(g -> g.getPath().equals("/test-role-group")).findFirst().isPresent()); + } + + /** + * KEYCLOAK-4978 Verifies that Role with no users assigned is being properly retrieved without groups in API endpoint for role membership + */ + @Test + public void testGroupsNotInRole() { + RoleResource role = resource.get("role-without-users"); + + role = adminClient.realm(REALM_NAME).roles().get(role.toRepresentation().getName()); + + Set groupsInRole = role.getRoleGroupMembers(); + assertTrue(groupsInRole.isEmpty()); + } + + /** + * KEYCLOAK-2035 Verifies that Role Membership is ok after user removal + */ + @Test + public void roleMembershipAfterUserRemoval() { + RoleResource role = resource.get("role-with-users"); + + List users = adminClient.realm(REALM_NAME).users().search("test-role-member", null, null, null, null, null); + assertEquals(1, users.size()); + UserResource user = adminClient.realm(REALM_NAME).users().get(users.get(0).getId()); + UserRepresentation userRep = user.toRepresentation(); + + RoleResource roleResource = adminClient.realm(REALM_NAME).roles().get(role.toRepresentation().getName()); + List rolesToAdd = new LinkedList<>(); + rolesToAdd.add(roleResource.toRepresentation()); + adminClient.realm(REALM_NAME).users().get(userRep.getId()).roles().realmLevel().add(rolesToAdd); + + roleResource = adminClient.realm(REALM_NAME).roles().get(role.toRepresentation().getName()); + assertEquals(Collections.singletonList("test-role-member"), extractUsernames(roleResource.getUserMembers())); + + adminClient.realm(REALM_NAME).users().delete(userRep.getId()); + assertEquals(role.getUserMembers(), Collections.emptyList()); + } + + @Test + public void testRoleMembershipWithPagination() { + RoleResource role = resource.get("role-with-users"); + + // Add a second user + UserRepresentation userRep2 = new UserRepresentation(); + userRep2.setUsername("test-role-member2"); + userRep2.setEmail("test-role-member2@test-role-member.com"); + userRep2.setRequiredActions(Collections.emptyList()); + userRep2.setEnabled(true); + adminClient.realm(REALM_NAME).users().create(userRep2); + + List users = adminClient.realm(REALM_NAME).users().search("test-role-member", null, null, null, null, null); + assertThat(users, hasSize(2)); + for (UserRepresentation userRepFromList : users) { + UserResource user = adminClient.realm(REALM_NAME).users().get(userRepFromList.getId()); + UserRepresentation userRep = user.toRepresentation(); + + RoleResource roleResource = adminClient.realm(REALM_NAME).roles().get(role.toRepresentation().getName()); + List rolesToAdd = new LinkedList<>(); + rolesToAdd.add(roleResource.toRepresentation()); + adminClient.realm(REALM_NAME).users().get(userRep.getId()).roles().realmLevel().add(rolesToAdd); + } + + RoleResource roleResource = adminClient.realm(REALM_NAME).roles().get(role.toRepresentation().getName()); + + List roleUserMembers = roleResource.getUserMembers(0, 1); + assertEquals(Collections.singletonList("test-role-member"), extractUsernames(roleUserMembers)); + Assert.assertNotNull(roleUserMembers.get(0).getNotBefore(), "Not in full representation"); + + roleUserMembers = roleResource.getUserMembers(true, 1, 1); + assertThat(roleUserMembers, hasSize(1)); + assertEquals(Collections.singletonList("test-role-member2"), extractUsernames(roleUserMembers)); + + roleUserMembers = roleResource.getUserMembers(true, 2, 1); + assertEquals(roleUserMembers, Collections.emptyList()); + } + + // issue #9587 + @Test + public void testSearchForRealmRoles() { + resource.list("role-", true).stream().forEach(role -> assertThat("There is client role '" + role.getName() + "' among realm roles.", role.getClientRole(), is(false))); + } + + @Test + public void testSearchForRoles() { + + for(int i = 0; i<15; i++) { + String roleName = "testrole"+i; + RoleRepresentation role = makeRole(roleName); + resource.create(role); + } + + String roleNameA = "abcdefg"; + RoleRepresentation roleA = makeRole(roleNameA); + resource.create(roleA); + + String roleNameB = "defghij"; + RoleRepresentation roleB = makeRole(roleNameB); + resource.create(roleB); + + List resultSearch = resource.list("defg", -1, -1); + assertEquals(2,resultSearch.size()); + + List resultSearch2 = resource.list("testrole", -1, -1); + assertEquals(15,resultSearch2.size()); + + List resultSearchPagination = resource.list("testrole", 1, 5); + assertEquals(5,resultSearchPagination.size()); + } + + @Test + public void testPaginationRoles() { + + for(int i = 0; i<15; i++) { + String roleName = "role"+i; + RoleRepresentation role = makeRole(roleName); + resource.create(role); + } + + List resultSearchPagination = resource.list(1, 5); + assertEquals(5,resultSearchPagination.size()); + + List resultSearchPagination2 = resource.list(5, 5); + assertEquals(5,resultSearchPagination2.size()); + + List resultSearchPagination3 = resource.list(1, 5); + assertEquals(5,resultSearchPagination3.size()); + + List resultSearchPaginationIncoherentParams = resource.list(1, null); + assertTrue(resultSearchPaginationIncoherentParams.size() > 15); + } + + @Test + public void testPaginationRolesCache() { + + for(int i = 0; i<5; i++) { + String roleName = "paginaterole"+i; + RoleRepresentation role = makeRole(roleName); + resource.create(role); + } + + List resultBeforeAddingRoleToTestCache = resource.list(1, 1000); + + // after a first call which init the cache, we add a new role to see if the result change + + RoleRepresentation role = makeRole("anewrole"); + resource.create(role); + + List resultafterAddingRoleToTestCache = resource.list(1, 1000); + + assertEquals(resultBeforeAddingRoleToTestCache.size()+1, resultafterAddingRoleToTestCache.size()); + } + + @Test + public void getRolesWithFullRepresentation() { + for(int i = 0; i<5; i++) { + String roleName = "attributesrole"+i; + RoleRepresentation role = makeRole(roleName); + + Map> attributes = new HashMap<>(); + attributes.put("attribute1", Arrays.asList("value1","value2")); + role.setAttributes(attributes); + + resource.create(role); + } + + List roles = resource.list("attributesrole", false); + assertTrue(roles.get(0).getAttributes().containsKey("attribute1")); + } + + @Test + public void getRolesWithBriefRepresentation() { + for(int i = 0; i<5; i++) { + String roleName = "attributesrolebrief"+i; + RoleRepresentation role = makeRole(roleName); + + Map> attributes = new HashMap<>(); + attributes.put("attribute1", Arrays.asList("value1","value2")); + role.setAttributes(attributes); + + resource.create(role); + } + + List roles = resource.list("attributesrolebrief", true); + assertNull(roles.get(0).getAttributes()); + } + + @Test + public void testDefaultRoles() { + RoleResource defaultRole = adminClient.realm(REALM_NAME).roles().get(Constants.DEFAULT_ROLES_ROLE_PREFIX + "-" + REALM_NAME); + + UserRepresentation user = adminClient.realm(REALM_NAME).users().search("test-role-member").get(0); + + UserResource userResource = adminClient.realm(REALM_NAME).users().get(user.getId()); + assertThat(convertRolesToNames(userResource.roles().realmLevel().listAll()), hasItem(Constants.DEFAULT_ROLES_ROLE_PREFIX + "-" + REALM_NAME)); + assertThat(convertRolesToNames(userResource.roles().realmLevel().listEffective()), allOf( + hasItem(Constants.DEFAULT_ROLES_ROLE_PREFIX + "-" + REALM_NAME), + hasItem(Constants.OFFLINE_ACCESS_ROLE), + hasItem(Constants.AUTHZ_UMA_AUTHORIZATION) + )); + + defaultRole.addComposites(Collections.singletonList(resource.get("role-a").toRepresentation())); + + userResource = adminClient.realm(REALM_NAME).users().get(user.getId()); + assertThat(convertRolesToNames(userResource.roles().realmLevel().listAll()), allOf( + hasItem(Constants.DEFAULT_ROLES_ROLE_PREFIX + "-" + REALM_NAME), + not(hasItem("role-a")) + )); + assertThat(convertRolesToNames(userResource.roles().realmLevel().listEffective()), allOf( + hasItem(Constants.DEFAULT_ROLES_ROLE_PREFIX + "-" + REALM_NAME), + hasItem(Constants.OFFLINE_ACCESS_ROLE), + hasItem(Constants.AUTHZ_UMA_AUTHORIZATION), + hasItem("role-a") + )); + + assertThat(userResource.roles().clientLevel(clientUuid).listAll(), empty()); + assertThat(userResource.roles().clientLevel(clientUuid).listEffective(), empty()); + + defaultRole.addComposites(Collections.singletonList(adminClient.realm(REALM_NAME).clients().get(clientUuid).roles().get("role-c").toRepresentation())); + + userResource = adminClient.realm(REALM_NAME).users().get(user.getId()); + + assertThat(userResource.roles().clientLevel(clientUuid).listAll(), empty()); + assertThat(convertRolesToNames(userResource.roles().clientLevel(clientUuid).listEffective()), + hasItem("role-c") + ); + } + + @Test + public void testDeleteDefaultRole() { + try { + adminClient.realm(REALM_NAME).roles().deleteRole(Constants.DEFAULT_ROLES_ROLE_PREFIX + "-" + REALM_NAME); + fail(); + } + catch (BadRequestException e) { + } + + } + + private List convertRolesToNames(List roles) { + return roles.stream().map(RoleRepresentation::getName).collect(Collectors.toList()); + } +} diff --git a/testsuite/admin-client-tests/src/test/java/org/keycloak/client/testsuite/RealmTest.java b/testsuite/admin-client-tests/src/test/java/org/keycloak/client/testsuite/RealmTest.java new file mode 100644 index 0000000..7393bca --- /dev/null +++ b/testsuite/admin-client-tests/src/test/java/org/keycloak/client/testsuite/RealmTest.java @@ -0,0 +1,926 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * 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. + */ + +package org.keycloak.client.testsuite; + +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Sets; +import jakarta.ws.rs.BadRequestException; +import jakarta.ws.rs.NotFoundException; +import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.Test; +import org.keycloak.admin.client.resource.RealmResource; +import org.keycloak.client.testsuite.common.Creator; +import org.keycloak.client.testsuite.framework.KeycloakVersion; +import org.keycloak.events.EventType; +import org.keycloak.client.testsuite.model.CibaConfig; +import org.keycloak.models.Constants; +import org.keycloak.client.testsuite.model.OAuth2DeviceConfig; +import org.keycloak.client.testsuite.model.ParConfig; +import org.keycloak.client.testsuite.model.RealmAttributes; +import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.ComponentRepresentation; +import org.keycloak.representations.idm.RealmEventsConfigRepresentation; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.representations.idm.RoleRepresentation; +import org.keycloak.testsuite.client.Keycloak; +import org.keycloak.testsuite.util.AdminClientUtil; +import org.keycloak.testsuite.util.RealmBuilder; +import org.keycloak.testsuite.util.UserBuilder; +import org.keycloak.util.JsonSerialization; +import org.testcontainers.shaded.org.hamcrest.CoreMatchers; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static io.smallrye.common.constraint.Assert.assertFalse; +import static io.smallrye.common.constraint.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.fail; +import static org.testcontainers.shaded.org.hamcrest.Matchers.hasItems; +import static org.testcontainers.shaded.org.hamcrest.MatcherAssert.assertThat; +import static org.testcontainers.shaded.org.hamcrest.Matchers.containsInAnyOrder; +import static org.testcontainers.shaded.org.hamcrest.Matchers.containsString; +import static org.testcontainers.shaded.org.hamcrest.Matchers.empty; +import static org.testcontainers.shaded.org.hamcrest.Matchers.everyItem; +import static org.testcontainers.shaded.org.hamcrest.Matchers.hasSize; +import static org.testcontainers.shaded.org.hamcrest.Matchers.notNullValue; +import static org.testcontainers.shaded.org.hamcrest.Matchers.not; + +/** + * @author Stian Thorgersen + */ +public class RealmTest extends AbstractAdminClientTest { + + @Override + public List getRealmsForImport() { + return super.getRealmsForImport(); + } + + @Test + public void getRealms() { + List realms = adminClient.realms().findAll(); + Assert.assertNames(realms, "master", REALM_NAME, "test"); + } + + @Test + public void renameRealm() { + String OLD = "old"; + String NEW = "new"; + + RealmRepresentation rep = new RealmRepresentation(); + rep.setId(OLD); + rep.setRealm(OLD); + + adminClient.realms().create(rep); + + Map newBaseUrls = new HashMap<>(); + Map> newRedirectUris = new HashMap<>(); + + // memorize all existing clients with their soon-to-be URIs + adminClient.realm(OLD).clients().findAll().forEach(client -> { + if (client.getBaseUrl() != null && client.getBaseUrl().contains("/" + OLD + "/")) { + newBaseUrls.put(client.getClientId(), client.getBaseUrl().replace("/" + OLD + "/", "/" + NEW + "/")); + } + if (client.getRedirectUris() != null) { + newRedirectUris.put( + client.getClientId(), + client.getRedirectUris() + .stream() + .map(redirectUri -> redirectUri.replace("/" + OLD + "/", "/" + NEW + "/")) + .collect(Collectors.toList()) + ); + } + }); + // at least those three default clients should be in the list of things to be tested + assertThat(newBaseUrls.keySet(), hasItems(Constants.ADMIN_CONSOLE_CLIENT_ID, Constants.ACCOUNT_MANAGEMENT_CLIENT_ID, Constants.ACCOUNT_CONSOLE_CLIENT_ID)); + assertThat(newRedirectUris.keySet(), hasItems(Constants.ADMIN_CONSOLE_CLIENT_ID, Constants.ACCOUNT_MANAGEMENT_CLIENT_ID, Constants.ACCOUNT_CONSOLE_CLIENT_ID)); + + rep.setRealm(NEW); + adminClient.realm(OLD).update(rep); + + // Check client in master realm renamed + assertEquals(0, adminClient.realm("master").clients().findByClientId("old-realm").size()); + assertEquals(1, adminClient.realm("master").clients().findByClientId("new-realm").size()); + + ClientRepresentation adminConsoleClient = adminClient.realm(NEW).clients().findByClientId(Constants.ADMIN_CONSOLE_CLIENT_ID).get(0); + assertEquals(Constants.AUTH_ADMIN_URL_PROP, adminConsoleClient.getRootUrl()); + + ClientRepresentation accountClient = adminClient.realm(NEW).clients().findByClientId(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID).get(0); + assertEquals(Constants.AUTH_BASE_URL_PROP, accountClient.getRootUrl()); + + ClientRepresentation accountConsoleClient = adminClient.realm(NEW).clients().findByClientId(Constants.ACCOUNT_CONSOLE_CLIENT_ID).get(0); + assertEquals(Constants.AUTH_BASE_URL_PROP, accountConsoleClient.getRootUrl()); + + newBaseUrls.forEach((clientId, baseUrl) -> { + assertEquals(baseUrl, adminClient.realm(NEW).clients().findByClientId(clientId).get(0).getBaseUrl()); + }); + newRedirectUris.forEach((clientId, redirectUris) -> { + assertEquals(redirectUris, adminClient.realm(NEW).clients().findByClientId(clientId).get(0).getRedirectUris()); + }); + + adminClient.realms().realm(NEW).remove(); + } + + @Test + public void createRealmEmpty() { + + RealmRepresentation rep = new RealmRepresentation(); + rep.setRealm("new-realm"); + + adminClient.realms().create(rep); + + Assert.assertNames(adminClient.realms().findAll(), "master", REALM_NAME, "test", "new-realm"); + + List clients = adminClient.realms().realm("new-realm").clients().findAll().stream().map(ClientRepresentation::getClientId).collect(Collectors.toList()); + assertThat(clients, containsInAnyOrder("account", "account-console", "admin-cli", "broker", "realm-management", "security-admin-console")); + + adminClient.realms().realm("new-realm").remove(); + + Assert.assertNames(adminClient.realms().findAll(), "master", REALM_NAME, "test"); + } + + @Test + @KeycloakVersion(min = "25.0.0") + public void createRealmWithValidConsoleUris() throws Exception { + var realmNameWithSpaces = "new realm"; + + + RealmRepresentation rep = new RealmRepresentation(); + rep.setRealm(realmNameWithSpaces); + rep.setEnabled(Boolean.TRUE); + rep.setUsers(Collections.singletonList(UserBuilder.create() + .username("new-realm-admin") + .firstName("new-realm-admin") + .lastName("new-realm-admin") + .email("new-realm-admin@keycloak.org") + .password("password") + .role(Constants.REALM_MANAGEMENT_CLIENT_ID, "realm-admin") + .build())); + + adminClient.realms().create(rep); + + Assert.assertNames(adminClient.realms().findAll(), "master", REALM_NAME, "test" , realmNameWithSpaces); + + final var urlPlaceHolders = ImmutableSet.of("${authBaseUrl}", "${authAdminUrl}"); + + RealmResource newRealm = adminClient.realms().realm(realmNameWithSpaces); + List clientUris = newRealm.clients() + .findAll() + .stream() + .flatMap(client -> Stream.concat(Stream.concat(Stream.concat( + client.getRedirectUris().stream(), + Stream.of(client.getBaseUrl())), + Stream.of(client.getRootUrl())), + Stream.of(client.getAdminUrl()))) + .filter(Objects::nonNull) + .filter(uri -> !urlPlaceHolders.contains(uri)) + .collect(Collectors.toList()); + + assertThat(clientUris, not(empty())); + assertThat(clientUris, everyItem(containsString("/new%20realm/"))); + + + try (Keycloak client = AdminClientUtil.createAdminClient(true, realmNameWithSpaces, + "new-realm-admin", "password", Constants.ADMIN_CLI_CLIENT_ID, null)) { + assertNotNull(client.serverInfo().getInfo()); + } + + adminClient.realms().realm(realmNameWithSpaces).remove(); + + Assert.assertNames(adminClient.realms().findAll(), "master", REALM_NAME, "test"); + } + + @Test + public void createRealmRejectReservedCharOrEmptyName() { + RealmRepresentation rep = new RealmRepresentation(); + rep.setRealm("new-re;alm"); + assertThrows(BadRequestException.class, () -> adminClient.realms().create(rep)); + rep.setRealm(""); + assertThrows(BadRequestException.class, () -> adminClient.realms().create(rep)); + rep.setRealm("new-realm"); + rep.setId("invalid;id"); + assertThrows(BadRequestException.class, () -> adminClient.realms().create(rep)); + } + + /** + * Checks attributes exposed as fields are not also included as attributes + */ + @Test + public void excludesFieldsFromAttributes() { + RealmRepresentation rep = new RealmRepresentation(); + rep.setRealm("attributes"); + + adminClient.realms().create(rep); + + RealmRepresentation rep2 = adminClient.realm("attributes").toRepresentation(); + if (rep2.getAttributes() != null) { + Arrays.asList(CibaConfig.CIBA_BACKCHANNEL_TOKEN_DELIVERY_MODE, + CibaConfig.CIBA_EXPIRES_IN, + CibaConfig.CIBA_INTERVAL, + CibaConfig.CIBA_AUTH_REQUESTED_USER_HINT).stream().forEach(i -> rep2.getAttributes().remove(i)); + } + + Set attributesKeys = rep2.getAttributes().keySet(); + + int expectedAttributesCount = 3; + final Set expectedAttributes = Sets.newHashSet( + OAuth2DeviceConfig.OAUTH2_DEVICE_CODE_LIFESPAN, + OAuth2DeviceConfig.OAUTH2_DEVICE_POLLING_INTERVAL, + ParConfig.PAR_REQUEST_URI_LIFESPAN + ); + + // This attribute is represented in Legacy store as attribute and for Map store as a field + expectedAttributes.add("realmReusableOtpCode"); + expectedAttributesCount++; + + assertThat(attributesKeys.size(), CoreMatchers.is(expectedAttributesCount)); + assertThat(attributesKeys, CoreMatchers.is(expectedAttributes)); + + adminClient.realms().realm("attributes").remove(); + } + + /** + * Checks attributes exposed as fields are not deleted on update realm + */ + @Test + public void testFieldNotErased() { + Long dummyLong = Long.valueOf(999); + Integer dummyInt = Integer.valueOf(999); + + RealmRepresentation rep = new RealmRepresentation(); + rep.setRealm("attributes"); + rep.setDisplayName("DISPLAY_NAME"); + rep.setDisplayNameHtml("DISPLAY_NAME_HTML"); + rep.setDefaultSignatureAlgorithm("RS256"); + rep.setBruteForceProtected(true); + rep.setPermanentLockout(true); + rep.setMaxFailureWaitSeconds(dummyInt); + rep.setWaitIncrementSeconds(dummyInt); + rep.setQuickLoginCheckMilliSeconds(dummyLong); + rep.setMinimumQuickLoginWaitSeconds(dummyInt); + rep.setMaxDeltaTimeSeconds(dummyInt); + rep.setFailureFactor(dummyInt); + rep.setActionTokenGeneratedByAdminLifespan(dummyInt); + rep.setActionTokenGeneratedByUserLifespan(dummyInt); + rep.setOfflineSessionMaxLifespanEnabled(true); + rep.setOfflineSessionMaxLifespan(dummyInt); + + rep.setWebAuthnPolicyRpEntityName("RP_ENTITY_NAME"); + rep.setWebAuthnPolicySignatureAlgorithms(Collections.singletonList("RS256")); + rep.setWebAuthnPolicyRpId("localhost"); + rep.setWebAuthnPolicyAttestationConveyancePreference("Direct"); + rep.setWebAuthnPolicyAuthenticatorAttachment("Platform"); + rep.setWebAuthnPolicyRequireResidentKey("Yes"); + rep.setWebAuthnPolicyUserVerificationRequirement("Required"); + rep.setWebAuthnPolicyCreateTimeout(dummyInt); + rep.setWebAuthnPolicyAvoidSameAuthenticatorRegister(true); + rep.setWebAuthnPolicyAcceptableAaguids(Collections.singletonList("00000000-0000-0000-0000-000000000000")); + + rep.setWebAuthnPolicyPasswordlessRpEntityName("RP_ENTITY_NAME"); + rep.setWebAuthnPolicyPasswordlessSignatureAlgorithms(Collections.singletonList("RS256")); + rep.setWebAuthnPolicyPasswordlessRpId("localhost"); + rep.setWebAuthnPolicyPasswordlessAttestationConveyancePreference("Direct"); + rep.setWebAuthnPolicyPasswordlessAuthenticatorAttachment("Platform"); + rep.setWebAuthnPolicyPasswordlessRequireResidentKey("Yes"); + rep.setWebAuthnPolicyPasswordlessUserVerificationRequirement("Required"); + rep.setWebAuthnPolicyPasswordlessCreateTimeout(dummyInt); + rep.setWebAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister(true); + rep.setWebAuthnPolicyPasswordlessAcceptableAaguids(Collections.singletonList("00000000-0000-0000-0000-000000000000")); + + adminClient.realms().create(rep); + + RealmRepresentation rep2 = new RealmRepresentation(); + rep2.setAttributes(Collections.singletonMap("frontendUrl", "http://localhost/frontEnd")); + adminClient.realm("attributes").update(rep2); + + rep = adminClient.realm("attributes").toRepresentation(); + assertEquals("DISPLAY_NAME", rep.getDisplayName()); + assertEquals("DISPLAY_NAME_HTML", rep.getDisplayNameHtml()); + assertEquals("RS256", rep.getDefaultSignatureAlgorithm()); + assertTrue(rep.isBruteForceProtected()); + assertTrue(rep.isPermanentLockout()); + assertEquals(dummyInt, rep.getMaxFailureWaitSeconds()); + assertEquals(dummyInt, rep.getWaitIncrementSeconds()); + assertEquals(dummyLong, rep.getQuickLoginCheckMilliSeconds()); + assertEquals(dummyInt, rep.getMinimumQuickLoginWaitSeconds()); + assertEquals(dummyInt, rep.getMaxDeltaTimeSeconds()); + assertEquals(dummyInt, rep.getFailureFactor()); + assertEquals(dummyInt, rep.getActionTokenGeneratedByAdminLifespan()); + assertEquals(dummyInt, rep.getActionTokenGeneratedByUserLifespan()); + assertTrue(rep.getOfflineSessionMaxLifespanEnabled()); + assertEquals(dummyInt, rep.getOfflineSessionMaxLifespan()); + + assertEquals("RP_ENTITY_NAME", rep.getWebAuthnPolicyRpEntityName()); + assertEquals(Collections.singletonList("RS256"), rep.getWebAuthnPolicySignatureAlgorithms()); + assertEquals("localhost", rep.getWebAuthnPolicyRpId()); + assertEquals("Direct", rep.getWebAuthnPolicyAttestationConveyancePreference()); + assertEquals("Platform", rep.getWebAuthnPolicyAuthenticatorAttachment()); + assertEquals("Yes", rep.getWebAuthnPolicyRequireResidentKey()); + assertEquals("Required", rep.getWebAuthnPolicyUserVerificationRequirement()); + assertEquals(dummyInt, rep.getWebAuthnPolicyCreateTimeout()); + assertTrue(rep.isWebAuthnPolicyAvoidSameAuthenticatorRegister()); + assertEquals(Collections.singletonList("00000000-0000-0000-0000-000000000000"), rep.getWebAuthnPolicyAcceptableAaguids()); + + assertEquals("RP_ENTITY_NAME", rep.getWebAuthnPolicyPasswordlessRpEntityName()); + assertEquals(Collections.singletonList("RS256"), rep.getWebAuthnPolicyPasswordlessSignatureAlgorithms()); + assertEquals("localhost", rep.getWebAuthnPolicyPasswordlessRpId()); + assertEquals("Direct", rep.getWebAuthnPolicyPasswordlessAttestationConveyancePreference()); + assertEquals("Platform", rep.getWebAuthnPolicyPasswordlessAuthenticatorAttachment()); + assertEquals("Yes", rep.getWebAuthnPolicyPasswordlessRequireResidentKey()); + assertEquals("Required", rep.getWebAuthnPolicyPasswordlessUserVerificationRequirement()); + assertEquals(dummyInt, rep.getWebAuthnPolicyPasswordlessCreateTimeout()); + assertTrue(rep.isWebAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister()); + assertEquals(Collections.singletonList("00000000-0000-0000-0000-000000000000"), rep.getWebAuthnPolicyPasswordlessAcceptableAaguids()); + + adminClient.realms().realm("attributes").remove(); + } + + @Test + public void smtpPasswordSecret() { + RealmRepresentation rep = RealmBuilder.create().testMail().build(); + rep.setRealm("realm-with-smtp"); + rep.getSmtpServer().put("user", "user"); + rep.getSmtpServer().put("password", "secret"); + + adminClient.realms().create(rep); + + RealmRepresentation returned = adminClient.realm("realm-with-smtp").toRepresentation(); + assertEquals(ComponentRepresentation.SECRET_VALUE, returned.getSmtpServer().get("password")); + + adminClient.realm("realm-with-smtp").update(rep); + + + RealmRepresentation realm = adminClient.realms().findAll().stream().filter(r -> r.getRealm().equals("realm-with-smtp")).findFirst().get(); + assertEquals(ComponentRepresentation.SECRET_VALUE, realm.getSmtpServer().get("password")); + + adminClient.realms().realm("realm-with-smtp").remove(); + } + + @Test + public void createRealmCheckDefaultPasswordPolicy() { + RealmRepresentation rep = new RealmRepresentation(); + rep.setRealm("new-realm"); + + adminClient.realms().create(rep); + + assertEquals(null, adminClient.realm("new-realm").toRepresentation().getPasswordPolicy()); + + adminClient.realms().realm("new-realm").remove(); + + rep.setPasswordPolicy("length(8)"); + + adminClient.realms().create(rep); + + assertEquals("length(8)", adminClient.realm("new-realm").toRepresentation().getPasswordPolicy()); + + adminClient.realms().realm("new-realm").remove(); + } + + @Test + public void createRealmFromJson() { + RealmRepresentation rep = loadJson(getClass().getResourceAsStream("/admin-test/testrealm.json"), RealmRepresentation.class); + adminClient.realms().create(rep); + + RealmRepresentation created = adminClient.realms().realm("admin-test-1").toRepresentation(); + assertRealm(rep, created); + + adminClient.realms().realm("admin-test-1").remove(); + } + + //KEYCLOAK-6146 + @Test + public void createRealmWithPasswordPolicyFromJsonWithInvalidPasswords() { + + //try to create realm with password policies and users with plain-text passwords what doesn't met the policies + RealmRepresentation rep = loadJson(getClass().getResourceAsStream("/import/testrealm-keycloak-6146-error.json"), RealmRepresentation.class); + try { + adminClient.realms().create(rep); + fail(); + } + catch (BadRequestException ex) { + } + + try { + //ensure the realm was not created + adminClient.realms().realm("secure-app").toRepresentation(); + fail(); + } + catch (NotFoundException ex) { + } + //test will fail on AssertionError when both BadRequestException and NotFoundException is not thrown + } + + //KEYCLOAK-6146 + @Test + public void createRealmWithPasswordPolicyFromJsonWithValidPasswords() { + RealmRepresentation rep = loadJson(getClass().getResourceAsStream("/import/testrealm-keycloak-6146.json"), RealmRepresentation.class); + try (Creator c = Creator.create(adminClient, rep)) { + RealmRepresentation created = c.resource().toRepresentation(); + assertRealm(rep, created); + } + } + + @Test + public void removeRealm() { + RealmRepresentation realmRepresentation = realm.toRepresentation(); + realm.remove(); + + Assert.assertNames(adminClient.realms().findAll(), "master", "test"); + + // Re-create realm + adminClient.realms().create(realmRepresentation); + } + + @Test + @KeycloakVersion(min = "25.0.0") + public void removeMasterRealm() { + // any attempt to remove the master realm should fail. + try { + adminClient.realm("master").remove(); + fail("It should not be possible to remove the master realm"); + } catch(BadRequestException ignored) { + } + } + + @Test + public void loginAfterRemoveRealm() { + RealmRepresentation realmRepresentation = realm.toRepresentation(); + realm.remove(); + + try (Keycloak client = AdminClientUtil.createAdminClient(true, "master", "admin", "admin", Constants.ADMIN_CLI_CLIENT_ID, null)) { + client.serverInfo().getInfo(); + } + + // Re-create realm + adminClient.realms().create(realmRepresentation); + } + + /** + * KEYCLOAK-1990 1991 + * @throws Exception + */ + @Test + public void renameRealmTest() throws Exception { + RealmRepresentation realm1 = new RealmRepresentation(); + realm1.setRealm("test-immutable"); + adminClient.realms().create(realm1); + realm1 = adminClient.realms().realm("test-immutable").toRepresentation(); + realm1.setRealm("test-immutable-old"); + adminClient.realms().realm("test-immutable").update(realm1); + assertThat(adminClient.realms().realm("test-immutable-old").toRepresentation(), notNullValue()); + + RealmRepresentation realm2 = new RealmRepresentation(); + realm2.setRealm("test-immutable"); + adminClient.realms().create(realm2); + assertThat(adminClient.realms().realm("test-immutable").toRepresentation(), notNullValue()); + + adminClient.realms().realm("test-immutable").remove(); + adminClient.realms().realm("test-immutable-old").remove(); + } + + private RealmEventsConfigRepresentation copyRealmEventsConfigRepresentation(RealmEventsConfigRepresentation rep) { + RealmEventsConfigRepresentation recr = new RealmEventsConfigRepresentation(); + recr.setEnabledEventTypes(rep.getEnabledEventTypes()); + recr.setEventsListeners(rep.getEventsListeners()); + recr.setEventsExpiration(rep.getEventsExpiration()); + recr.setEventsEnabled(rep.isEventsEnabled()); + recr.setAdminEventsEnabled(rep.isAdminEventsEnabled()); + recr.setAdminEventsDetailsEnabled(rep.isAdminEventsDetailsEnabled()); + return recr; + } + + private void checkRealmEventsConfigRepresentation(RealmEventsConfigRepresentation expected, + RealmEventsConfigRepresentation actual) { + assertEquals(expected.getEnabledEventTypes().size(), actual.getEnabledEventTypes().size()); + assertTrue(actual.getEnabledEventTypes().containsAll(expected.getEnabledEventTypes())); + assertEquals(expected.getEventsListeners().size(), actual.getEventsListeners().size()); + assertTrue(actual.getEventsListeners().containsAll(expected.getEventsListeners())); + assertEquals(expected.getEventsExpiration(), actual.getEventsExpiration()); + assertEquals(expected.isEventsEnabled(), actual.isEventsEnabled()); + assertEquals(expected.isAdminEventsEnabled(), actual.isAdminEventsEnabled()); + assertEquals(expected.isAdminEventsDetailsEnabled(), actual.isAdminEventsDetailsEnabled()); + } + + @Test + public void updateRealmEventsConfig() { + RealmEventsConfigRepresentation rep = realm.getRealmEventsConfig(); + RealmEventsConfigRepresentation repOrig = copyRealmEventsConfigRepresentation(rep); + + // the "event-queue" listener should be enabled by default + assertTrue(rep.getEventsListeners().contains("event-queue")); + + // first modification => remove "event-queue", should be sent to the queue + rep.setEnabledEventTypes(Arrays.asList(EventType.LOGIN.name(), EventType.LOGIN_ERROR.name())); + rep.setEventsListeners(Arrays.asList("jboss-logging")); + rep.setEventsExpiration(36000L); + rep.setEventsEnabled(true); + rep.setAdminEventsEnabled(true); + rep.setAdminEventsDetailsEnabled(true); + adminClient.realms().realm(REALM_NAME).updateRealmEventsConfig(rep); + RealmEventsConfigRepresentation actual = realm.getRealmEventsConfig(); + checkRealmEventsConfigRepresentation(rep, actual); + + // second modification => should not be sent cos event-queue was removed in the first mod + rep.setEnabledEventTypes(Arrays.asList(EventType.LOGIN.name(), + EventType.LOGIN_ERROR.name(), EventType.CLIENT_LOGIN.name())); + adminClient.realms().realm(REALM_NAME).updateRealmEventsConfig(rep); + actual = realm.getRealmEventsConfig(); + checkRealmEventsConfigRepresentation(rep, actual); + + // third modification => restore queue => should be sent and recovered + adminClient.realms().realm(REALM_NAME).updateRealmEventsConfig(repOrig); + actual = realm.getRealmEventsConfig(); + checkRealmEventsConfigRepresentation(repOrig, actual); + } + + @Test + public void updateRealmWithReservedCharInNameOrEmptyName() { + RealmRepresentation rep = realm.toRepresentation(); + rep.setRealm("fo#o"); + assertThrows(BadRequestException.class, () -> realm.update(rep)); + rep.setRealm(""); + assertThrows(BadRequestException.class, () -> realm.update(rep)); + } + + @Test + @KeycloakVersion(min = "26.0.0") + public void updateRealm() { + // first change + RealmRepresentation rep = realm.toRepresentation(); + rep.setSsoSessionIdleTimeout(123); + rep.setSsoSessionMaxLifespan(12); + rep.setSsoSessionIdleTimeoutRememberMe(33); + rep.setSsoSessionMaxLifespanRememberMe(34); + rep.setAccessCodeLifespanLogin(1234); + rep.setActionTokenGeneratedByAdminLifespan(2345); + rep.setActionTokenGeneratedByUserLifespan(3456); + rep.setRegistrationAllowed(true); + rep.setRegistrationEmailAsUsername(true); + rep.setEditUsernameAllowed(true); + rep.setUserManagedAccessAllowed(true); + + realm.update(rep); + + rep = realm.toRepresentation(); + + assertEquals(123, rep.getSsoSessionIdleTimeout().intValue()); + assertEquals(12, rep.getSsoSessionMaxLifespan().intValue()); + assertEquals(33, rep.getSsoSessionIdleTimeoutRememberMe().intValue()); + assertEquals(34, rep.getSsoSessionMaxLifespanRememberMe().intValue()); + assertEquals(1234, rep.getAccessCodeLifespanLogin().intValue()); + assertEquals(2345, rep.getActionTokenGeneratedByAdminLifespan().intValue()); + assertEquals(3456, rep.getActionTokenGeneratedByUserLifespan().intValue()); + assertEquals(Boolean.TRUE, rep.isRegistrationAllowed()); + assertEquals(Boolean.TRUE, rep.isRegistrationEmailAsUsername()); + assertEquals(Boolean.TRUE, rep.isEditUsernameAllowed()); +// if (ProfileAssume.isFeatureEnabled(Profile.Feature.AUTHORIZATION)) { +// assertEquals(Boolean.TRUE, rep.isUserManagedAccessAllowed()); +// } else { +// assertEquals(Boolean.FALSE, rep.isUserManagedAccessAllowed()); +// } + + // second change + rep.setRegistrationAllowed(false); + rep.setRegistrationEmailAsUsername(false); + rep.setEditUsernameAllowed(false); + rep.setUserManagedAccessAllowed(false); + + realm.update(rep); + + rep = realm.toRepresentation(); + assertEquals(Boolean.FALSE, rep.isRegistrationAllowed()); + assertEquals(Boolean.FALSE, rep.isRegistrationEmailAsUsername()); + assertEquals(Boolean.FALSE, rep.isEditUsernameAllowed()); + assertEquals(Boolean.FALSE, rep.isUserManagedAccessAllowed()); + + rep.setAccessCodeLifespanLogin(0); + rep.setAccessCodeLifespanUserAction(0); + + try { + realm.update(rep); + fail("Not expected to successfully update the realm"); + } catch (Exception expected) { + // Expected exception + assertEquals("HTTP 400 Bad Request", expected.getMessage()); + } + } + @Test + public void updateRealmWithNewRepresentation() { + // first change + RealmRepresentation rep = new RealmRepresentation(); + rep.setEditUsernameAllowed(true); + rep.setSupportedLocales(new HashSet<>(Arrays.asList("en", "de"))); + + realm.update(rep); + + rep = realm.toRepresentation(); + + assertEquals(Boolean.TRUE, rep.isEditUsernameAllowed()); + assertEquals(2, rep.getSupportedLocales().size()); + + // second change + rep = new RealmRepresentation(); + rep.setEditUsernameAllowed(false); + + realm.update(rep); + + rep = realm.toRepresentation(); + assertEquals(Boolean.FALSE, rep.isEditUsernameAllowed()); + assertEquals(2, rep.getSupportedLocales().size()); + } + + @Test + public void updateRealmAttributes() { + // first change + RealmRepresentation rep = new RealmRepresentation(); + List webAuthnPolicyAcceptableAaguids = new ArrayList<>(); + webAuthnPolicyAcceptableAaguids.add("aaguid1"); + webAuthnPolicyAcceptableAaguids.add("aaguid2"); + + rep.setAttributes(new HashMap<>()); + rep.getAttributes().put("foo1", "bar1"); + rep.getAttributes().put("foo2", "bar2"); + + rep.setWebAuthnPolicyRpEntityName("keycloak"); + rep.setWebAuthnPolicyAcceptableAaguids(webAuthnPolicyAcceptableAaguids); + rep.setBruteForceProtected(true); + rep.setDisplayName("dn1"); + + realm.update(rep); + + rep = realm.toRepresentation(); + assertEquals("bar1", rep.getAttributes().get("foo1")); + assertEquals("bar2", rep.getAttributes().get("foo2")); + assertTrue(rep.isBruteForceProtected()); + assertEquals("dn1", rep.getDisplayName()); + assertEquals(webAuthnPolicyAcceptableAaguids, rep.getWebAuthnPolicyAcceptableAaguids()); + + // second change + webAuthnPolicyAcceptableAaguids.clear(); + rep.setBruteForceProtected(false); + rep.setDisplayName("dn2"); + rep.getAttributes().put("foo1", "bar11"); + rep.getAttributes().remove("foo2"); + rep.setWebAuthnPolicyAcceptableAaguids(webAuthnPolicyAcceptableAaguids); + + realm.update(rep); + + rep = realm.toRepresentation(); + + assertFalse(rep.isBruteForceProtected()); + assertEquals("dn2", rep.getDisplayName()); + + assertEquals("bar11", rep.getAttributes().get("foo1")); + assertFalse(rep.getAttributes().containsKey("foo2")); + assertTrue(rep.getWebAuthnPolicyAcceptableAaguids().isEmpty()); + } + + @Test + public void getRealmRepresentation() { + RealmRepresentation rep = realm.toRepresentation(); + assertEquals(REALM_NAME, rep.getRealm()); + assertTrue(rep.isEnabled()); + } + + @Test + // KEYCLOAK-1110 + public void deleteDefaultRole() { + RoleRepresentation role = new RoleRepresentation("test", "test", false); + realm.roles().create(role); + + role = realm.roles().get("test").toRepresentation(); + assertNotNull(role); + + realm.roles().get(Constants.DEFAULT_ROLES_ROLE_PREFIX + "-" + REALM_NAME).addComposites(Collections.singletonList(role)); + + + realm.roles().deleteRole("test"); + + try { + realm.roles().get("testsadfsadf").toRepresentation(); + fail("Expected NotFoundException"); + } catch (NotFoundException e) { + // Expected + } + } + + @Test + public void convertKeycloakClientDescription() throws IOException { + ClientRepresentation description = new ClientRepresentation(); + description.setClientId("client-id"); + description.setRedirectUris(Collections.singletonList("http://localhost")); + + ClientRepresentation converted = realm.convertClientDescription(JsonSerialization.writeValueAsString(description)); + assertEquals("client-id", converted.getClientId()); + assertEquals("http://localhost", converted.getRedirectUris().get(0)); + } + + @Test + public void convertOIDCClientDescription() throws IOException { + String description = IOUtils.toString(getClass().getResourceAsStream("/client-descriptions/client-oidc.json"), Charset.defaultCharset()); + + ClientRepresentation converted = realm.convertClientDescription(description); + assertEquals(1, converted.getRedirectUris().size()); + assertEquals("http://localhost", converted.getRedirectUris().get(0)); + } + + @Test + public void convertSAMLClientDescription() throws IOException { + String description = IOUtils.toString(getClass().getResourceAsStream("/client-descriptions/saml-entity-descriptor.xml"), Charset.defaultCharset()); + + ClientRepresentation converted = realm.convertClientDescription(description); + assertEquals("loadbalancer-9.siroe.com", converted.getClientId()); + assertEquals(2, converted.getRedirectUris().size()); + assertEquals("https://LoadBalancer-9.siroe.com:3443/federation/Consumer/metaAlias/sp", converted.getRedirectUris().get(0)); + assertEquals("https://LoadBalancer-9.siroe.com:3443/federation/Consumer/metaAlias/sp", converted.getRedirectUris().get(1)); + } + + public static void assertRealm(RealmRepresentation realm, RealmRepresentation storedRealm) { + if (realm.getRealm() != null) { + assertEquals(realm.getRealm(), storedRealm.getRealm()); + } + if (realm.isEnabled() != null) assertEquals(realm.isEnabled(), storedRealm.isEnabled()); + if (realm.isBruteForceProtected() != null) assertEquals(realm.isBruteForceProtected(), storedRealm.isBruteForceProtected()); + if (realm.getMaxFailureWaitSeconds() != null) assertEquals(realm.getMaxFailureWaitSeconds(), storedRealm.getMaxFailureWaitSeconds()); + if (realm.getMinimumQuickLoginWaitSeconds() != null) assertEquals(realm.getMinimumQuickLoginWaitSeconds(), storedRealm.getMinimumQuickLoginWaitSeconds()); + if (realm.getWaitIncrementSeconds() != null) assertEquals(realm.getWaitIncrementSeconds(), storedRealm.getWaitIncrementSeconds()); + if (realm.getQuickLoginCheckMilliSeconds() != null) assertEquals(realm.getQuickLoginCheckMilliSeconds(), storedRealm.getQuickLoginCheckMilliSeconds()); + if (realm.getMaxDeltaTimeSeconds() != null) assertEquals(realm.getMaxDeltaTimeSeconds(), storedRealm.getMaxDeltaTimeSeconds()); + if (realm.getFailureFactor() != null) assertEquals(realm.getFailureFactor(), storedRealm.getFailureFactor()); + if (realm.isRegistrationAllowed() != null) assertEquals(realm.isRegistrationAllowed(), storedRealm.isRegistrationAllowed()); + if (realm.isRegistrationEmailAsUsername() != null) assertEquals(realm.isRegistrationEmailAsUsername(), storedRealm.isRegistrationEmailAsUsername()); + if (realm.isRememberMe() != null) assertEquals(realm.isRememberMe(), storedRealm.isRememberMe()); + if (realm.isVerifyEmail() != null) assertEquals(realm.isVerifyEmail(), storedRealm.isVerifyEmail()); + if (realm.isLoginWithEmailAllowed() != null) assertEquals(realm.isLoginWithEmailAllowed(), storedRealm.isLoginWithEmailAllowed()); + if (realm.isDuplicateEmailsAllowed() != null) assertEquals(realm.isDuplicateEmailsAllowed(), storedRealm.isDuplicateEmailsAllowed()); + if (realm.isResetPasswordAllowed() != null) assertEquals(realm.isResetPasswordAllowed(), storedRealm.isResetPasswordAllowed()); + if (realm.isEditUsernameAllowed() != null) assertEquals(realm.isEditUsernameAllowed(), storedRealm.isEditUsernameAllowed()); + if (realm.getSslRequired() != null) assertEquals(realm.getSslRequired(), storedRealm.getSslRequired()); + if (realm.getAccessCodeLifespan() != null) assertEquals(realm.getAccessCodeLifespan(), storedRealm.getAccessCodeLifespan()); + if (realm.getAccessCodeLifespanUserAction() != null) + assertEquals(realm.getAccessCodeLifespanUserAction(), storedRealm.getAccessCodeLifespanUserAction()); + if (realm.getActionTokenGeneratedByAdminLifespan() != null) + assertEquals(realm.getActionTokenGeneratedByAdminLifespan(), storedRealm.getActionTokenGeneratedByAdminLifespan()); + if (realm.getActionTokenGeneratedByUserLifespan() != null) + assertEquals(realm.getActionTokenGeneratedByUserLifespan(), storedRealm.getActionTokenGeneratedByUserLifespan()); + else + assertEquals(realm.getAccessCodeLifespanUserAction(), storedRealm.getActionTokenGeneratedByUserLifespan()); + if (realm.getNotBefore() != null) assertEquals(realm.getNotBefore(), storedRealm.getNotBefore()); + if (realm.getAccessTokenLifespan() != null) assertEquals(realm.getAccessTokenLifespan(), storedRealm.getAccessTokenLifespan()); + if (realm.getAccessTokenLifespanForImplicitFlow() != null) assertEquals(realm.getAccessTokenLifespanForImplicitFlow(), storedRealm.getAccessTokenLifespanForImplicitFlow()); + if (realm.getSsoSessionIdleTimeout() != null) assertEquals(realm.getSsoSessionIdleTimeout(), storedRealm.getSsoSessionIdleTimeout()); + if (realm.getSsoSessionMaxLifespan() != null) assertEquals(realm.getSsoSessionMaxLifespan(), storedRealm.getSsoSessionMaxLifespan()); + if (realm.getSsoSessionIdleTimeoutRememberMe() != null) assertEquals(realm.getSsoSessionIdleTimeoutRememberMe(), storedRealm.getSsoSessionIdleTimeoutRememberMe()); + if (realm.getSsoSessionMaxLifespanRememberMe() != null) assertEquals(realm.getSsoSessionMaxLifespanRememberMe(), storedRealm.getSsoSessionMaxLifespanRememberMe()); + if (realm.getClientSessionIdleTimeout() != null) + assertEquals(realm.getClientSessionIdleTimeout(), storedRealm.getClientSessionIdleTimeout()); + if (realm.getClientSessionMaxLifespan() != null) + assertEquals(realm.getClientSessionMaxLifespan(), storedRealm.getClientSessionMaxLifespan()); + if (realm.getClientOfflineSessionIdleTimeout() != null) + assertEquals(realm.getClientOfflineSessionIdleTimeout(), storedRealm.getClientOfflineSessionIdleTimeout()); + if (realm.getClientOfflineSessionMaxLifespan() != null) + assertEquals(realm.getClientOfflineSessionMaxLifespan(), storedRealm.getClientOfflineSessionMaxLifespan()); + if (realm.getRequiredCredentials() != null) { + assertNotNull(storedRealm.getRequiredCredentials()); + for (String cred : realm.getRequiredCredentials()) { + assertTrue(storedRealm.getRequiredCredentials().contains(cred)); + } + } + if (realm.getLoginTheme() != null) assertEquals(realm.getLoginTheme(), storedRealm.getLoginTheme()); + if (realm.getAccountTheme() != null) assertEquals(realm.getAccountTheme(), storedRealm.getAccountTheme()); + if (realm.getAdminTheme() != null) assertEquals(realm.getAdminTheme(), storedRealm.getAdminTheme()); + if (realm.getEmailTheme() != null) assertEquals(realm.getEmailTheme(), storedRealm.getEmailTheme()); + + if (realm.getPasswordPolicy() != null) assertEquals(realm.getPasswordPolicy(), storedRealm.getPasswordPolicy()); + + if (realm.getSmtpServer() != null) { + assertEquals(realm.getSmtpServer(), storedRealm.getSmtpServer()); + } + + if (realm.getBrowserSecurityHeaders() != null) { + assertEquals(realm.getBrowserSecurityHeaders(), storedRealm.getBrowserSecurityHeaders()); + } + + if (realm.getAttributes() != null) { + HashMap attributes = new HashMap<>(); + attributes.putAll(storedRealm.getAttributes()); + attributes.entrySet().retainAll(realm.getAttributes().entrySet()); + assertEquals(realm.getAttributes(), attributes); + } + + if (realm.isUserManagedAccessAllowed() != null) assertEquals(realm.isUserManagedAccessAllowed(), storedRealm.isUserManagedAccessAllowed()); + } + + // NOTE: clearKeysCache tested in KcOIDCBrokerWithSignatureTest + @Test + // KEYCLOAK-17342 + public void testDefaultSignatureAlgorithm() { + RealmRepresentation rep = new RealmRepresentation(); + rep.setRealm("new-realm"); + + adminClient.realms().create(rep); + + assertEquals(Constants.DEFAULT_SIGNATURE_ALGORITHM, adminClient.realm("master").toRepresentation().getDefaultSignatureAlgorithm()); + assertEquals(Constants.DEFAULT_SIGNATURE_ALGORITHM, adminClient.realm("new-realm").toRepresentation().getDefaultSignatureAlgorithm()); + adminClient.realms().realm("new-realm").remove(); + } + + @KeycloakVersion(min = "25.0.0") + @Test + public void testSupportedOTPApplications() { + RealmRepresentation rep = new RealmRepresentation(); + rep.setRealm("new-realm"); + + adminClient.realms().create(rep); + + RealmResource realm = adminClient.realms().realm("new-realm"); + + rep = realm.toRepresentation(); + + List supportedApplications = rep.getOtpSupportedApplications(); + assertThat(supportedApplications, hasSize(3)); + assertThat(supportedApplications, containsInAnyOrder("totpAppGoogleName", "totpAppFreeOTPName", "totpAppMicrosoftAuthenticatorName")); + + rep.setOtpPolicyDigits(8); + realm.update(rep); + + rep = realm.toRepresentation(); + + supportedApplications = rep.getOtpSupportedApplications(); + assertThat(supportedApplications, hasSize(2)); + assertThat(supportedApplications, containsInAnyOrder("totpAppFreeOTPName", "totpAppGoogleName")); + + rep.setOtpPolicyType("hotp"); + realm.update(rep); + + rep = realm.toRepresentation(); + + supportedApplications = rep.getOtpSupportedApplications(); + assertThat(supportedApplications, hasSize(2)); + assertThat(supportedApplications, containsInAnyOrder("totpAppFreeOTPName", "totpAppGoogleName")); + adminClient.realms().realm("new-realm").remove(); + } + + @Test + public void testNoUserProfileProviderComponentUponRealmChange() { + String realmName = "new-realm"; + RealmRepresentation rep = new RealmRepresentation(); + rep.setRealm(realmName); + + adminClient.realms().create(rep); + + assertThat(adminClient.realm(realmName).components().query(null, "UserProfileProvider"), empty()); + + rep.setDisplayName("displayName"); + adminClient.realm(realmName).update(rep); + + // this used to return non-empty collection + assertThat(adminClient.realm(realmName).components().query(null, "UserProfileProvider"), empty()); + + adminClient.realms().realm(realmName).remove(); + } + + @Test + public void testSetEmptyAttributeValues() { + String realmName = "testSetEmptyAttributeValues"; + RealmRepresentation rep = new RealmRepresentation(); + rep.setRealm(realmName); + rep.setAttributes(new HashMap<>()); + rep.getAttributes().put("myboolean", ""); + rep.getAttributes().put(RealmAttributes.ACTION_TOKEN_GENERATED_BY_USER_LIFESPAN + ".something", ""); + + adminClient.realms().create(rep); + + RealmRepresentation realmRepresentation = adminClient.realms().realm(realmName).toRepresentation(); + Assert.assertNotNull(realmRepresentation.getAttributes().get("myboolean")); + adminClient.realm(realmName).remove(); + } +} diff --git a/testsuite/admin-client-tests/src/test/resources/admin-test/testrealm.json b/testsuite/admin-client-tests/src/test/resources/admin-test/testrealm.json new file mode 100644 index 0000000..cd58f83 --- /dev/null +++ b/testsuite/admin-client-tests/src/test/resources/admin-test/testrealm.json @@ -0,0 +1,105 @@ +{ + "realm": "admin-test-1", + "enabled": true, + "sslRequired": "external", + "registrationAllowed": true, + "registrationEmailAsUsername": true, + "resetPasswordAllowed": true, + "requiredCredentials": [ "password" ], + "defaultRoles": [ "user" ], + "actionTokenGeneratedByAdminLifespan": "147", + "actionTokenGeneratedByUserLifespan": "258", + "smtpServer": { + "from": "auto@keycloak.org", + "host": "localhost", + "port":"3025" + }, + "users" : [ + { + "username" : "test-user@localhost", + "enabled": true, + "email" : "test-user@localhost", + "credentials" : [ + { "type" : "password", + "value" : "password" } + ], + "realmRoles": ["user"], + "applicationRoles": { + "test-app": [ "customer-user" ], + "account": [ "view-profile", "manage-account" ] + } + } + ], + "oauthClients" : [ + { + "name" : "third-party", + "enabled": true, + "redirectUris": [ + "http://localhost:8081/app/*" + ], + "secret": "password" + } + ], + "scopeMappings": [ + { + "client": "third-party", + "roles": ["user"] + }, + { + "client": "test-app", + "roles": ["user"] + } + ], + "applications": [ + { + "name": "test-app", + "enabled": true, + "baseUrl": "http://localhost:8081/app", + "redirectUris": [ + "http://localhost:8081/app/*" + ], + "adminUrl": "http://localhost:8081/app/logout", + "secret": "password" + } + ], + "roles" : { + "realm" : [ + { + "name": "user", + "description": "Have User privileges" + }, + { + "name": "admin", + "description": "Have Administrator privileges" + } + ], + "application" : { + "test-app" : [ + { + "name": "customer-user", + "description": "Have Customer User privileges" + }, + { + "name": "customer-admin", + "description": "Have Customer Admin privileges" + } + ] + } + + }, + + "applicationScopeMappings": { + "test-app": [ + { + "client": "third-party", + "roles": ["customer-user"] + } + ] + }, + "attributes": { + "string-attr": "foo", + "int-attr": "123", + "long-attr": "1234567890123456", + "bool-attr": "true" + } +} diff --git a/testsuite/admin-client-tests/src/test/resources/client-descriptions/client-oidc.json b/testsuite/admin-client-tests/src/test/resources/client-descriptions/client-oidc.json new file mode 100644 index 0000000..51562fd --- /dev/null +++ b/testsuite/admin-client-tests/src/test/resources/client-descriptions/client-oidc.json @@ -0,0 +1,6 @@ +{ + "client_name": "Name", + "redirect_uris": [ + "http://localhost" + ] +} \ No newline at end of file diff --git a/testsuite/admin-client-tests/src/test/resources/client-descriptions/saml-entity-descriptor.xml b/testsuite/admin-client-tests/src/test/resources/client-descriptions/saml-entity-descriptor.xml new file mode 100644 index 0000000..0bfce4b --- /dev/null +++ b/testsuite/admin-client-tests/src/test/resources/client-descriptions/saml-entity-descriptor.xml @@ -0,0 +1,99 @@ + + + + + + + + +MIICYDCCAgqgAwIBAgICBoowDQYJKoZIhvcNAQEEBQAwgZIxCzAJBgNVBAYTAlVTMRMwEQYDVQQI +EwpDYWxpZm9ybmlhMRQwEgYDVQQHEwtTYW50YSBDbGFyYTEeMBwGA1UEChMVU3VuIE1pY3Jvc3lz +dGVtcyBJbmMuMRowGAYDVQQLExFJZGVudGl0eSBTZXJ2aWNlczEcMBoGA1UEAxMTQ2VydGlmaWNh +dGUgTWFuYWdlcjAeFw0wNjExMDIxOTExMzRaFw0xMDA3MjkxOTExMzRaMDcxEjAQBgNVBAoTCXNp +cm9lLmNvbTEhMB8GA1UEAxMYbG9hZGJhbGFuY2VyLTkuc2lyb2UuY29tMIGfMA0GCSqGSIb3DQEB +AQUAA4GNADCBiQKBgQCjOwa5qoaUuVnknqf5pdgAJSEoWlvx/jnUYbkSDpXLzraEiy2UhvwpoBgB +EeTSUaPPBvboCItchakPI6Z/aFdH3Wmjuij9XD8r1C+q//7sUO0IGn0ORycddHhoo0aSdnnxGf9V +tREaqKm9dJ7Yn7kQHjo2eryMgYxtr/Z5Il5F+wIDAQABo2AwXjARBglghkgBhvhCAQEEBAMCBkAw +DgYDVR0PAQH/BAQDAgTwMB8GA1UdIwQYMBaAFDugITflTCfsWyNLTXDl7cMDUKuuMBgGA1UdEQQR +MA+BDW1hbGxhQHN1bi5jb20wDQYJKoZIhvcNAQEEBQADQQB/6DOB6sRqCZu2OenM9eQR0gube85e +nTTxU4a7x1naFxzYXK1iQ1vMARKMjDb19QEJIEJKZlDK4uS7yMlf1nFS + + + + + + + + +MIICTDCCAfagAwIBAgICBo8wDQYJKoZIhvcNAQEEBQAwgZIxCzAJBgNVBAYTAlVTMRMwEQYDVQQI +EwpDYWxpZm9ybmlhMRQwEgYDVQQHEwtTYW50YSBDbGFyYTEeMBwGA1UEChMVU3VuIE1pY3Jvc3lz +dGVtcyBJbmMuMRowGAYDVQQLExFJZGVudGl0eSBTZXJ2aWNlczEcMBoGA1UEAxMTQ2VydGlmaWNh +dGUgTWFuYWdlcjAeFw0wNjExMDcyMzU2MTdaFw0xMDA4MDMyMzU2MTdaMCMxITAfBgNVBAMTGGxv +YWRiYWxhbmNlci05LnNpcm9lLmNvbTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAw574iRU6 +HsSO4LXW/OGTXyfsbGv6XRVOoy3v+J1pZ51KKejcDjDJXNkKGn3/356AwIaqbcymWd59T0zSqYfR +Hn+45uyjYxRBmVJseLpVnOXLub9jsjULfGx0yjH4w+KsZSZCXatoCHbj/RJtkzuZY6V9to/hkH3S +InQB4a3UAgMCAwEAAaNgMF4wEQYJYIZIAYb4QgEBBAQDAgZAMA4GA1UdDwEB/wQEAwIE8DAfBgNV +HSMEGDAWgBQ7oCE35Uwn7FsjS01w5e3DA1CrrjAYBgNVHREEETAPgQ1tYWxsYUBzdW4uY29tMA0G +CSqGSIb3DQEBBAUAA0EAMlbfBg/ff0Xkv4DOR5LEqmfTZKqgdlD81cXynfzlF7XfnOqI6hPIA90I +x5Ql0ejivIJAYcMGUyA+/YwJg2FGoA== + + + + + 128 + + + + + + + + urn:oasis:names:tc:SAML:2.0:nameid-format:persistent + + + urn:oasis:names:tc:SAML:2.0:nameid-format:transient + + + + + diff --git a/testsuite/admin-client-tests/src/test/resources/import/import-without-clients.json b/testsuite/admin-client-tests/src/test/resources/import/import-without-clients.json new file mode 100644 index 0000000..1c18e34 --- /dev/null +++ b/testsuite/admin-client-tests/src/test/resources/import/import-without-clients.json @@ -0,0 +1,660 @@ +{ + "id": "import-without-clients", + "realm": "import-without-clients", + "notBefore": 0, + "revokeRefreshToken": false, + "refreshTokenMaxReuse": 0, + "accessTokenLifespan": 300, + "accessTokenLifespanForImplicitFlow": 900, + "ssoSessionIdleTimeout": 1800, + "ssoSessionMaxLifespan": 36000, + "offlineSessionIdleTimeout": 2592000, + "accessCodeLifespan": 60, + "accessCodeLifespanUserAction": 300, + "accessCodeLifespanLogin": 1800, + "actionTokenGeneratedByAdminLifespan": 43200, + "actionTokenGeneratedByUserLifespan": 300, + "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": "045e743f-43b1-4e60-9d51-be3cbe71d6aa", + "name": "uma_authorization", + "description": "${role_uma_authorization}", + "scopeParamRequired": false, + "composite": false, + "clientRole": false, + "containerId": "import-without-clients" + }, + { + "id": "d2d8ee95-b0a2-4e7c-b379-9f840be85935", + "name": "offline_access", + "description": "${role_offline-access}", + "scopeParamRequired": true, + "composite": false, + "clientRole": false, + "containerId": "import-without-clients" + } + ] + }, + "groups": [], + "defaultRoles": [ + "uma_authorization", + "offline_access" + ], + "requiredCredentials": [ + "password" + ], + "otpPolicyType": "totp", + "otpPolicyAlgorithm": "HmacSHA1", + "otpPolicyInitialCounter": 0, + "otpPolicyDigits": 6, + "otpPolicyLookAheadWindow": 1, + "otpPolicyPeriod": 30, + "otpSupportedApplications": [ + "FreeOTP", + "Google Authenticator" + ], + "browserSecurityHeaders": { + "xContentTypeOptions": "nosniff", + "xRobotsTag": "none", + "xFrameOptions": "SAMEORIGIN", + "xXSSProtection": "1; mode=block", + "contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", + "strictTransportSecurity": "max-age=31536000; includeSubDomains" + }, + "smtpServer": {}, + "eventsEnabled": false, + "eventsListeners": [ + "jboss-logging" + ], + "enabledEventTypes": [], + "adminEventsEnabled": false, + "adminEventsDetailsEnabled": false, + "components": { + "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy": [ + { + "id": "a2a9471e-0f18-4847-b64a-7ad3cd1f0597", + "name": "Allowed Client Templates", + "providerId": "allowed-client-templates", + "subType": "anonymous", + "subComponents": {}, + "config": {} + }, + { + "id": "efba5b6b-dd97-4e2c-907d-c1c957e20ec4", + "name": "Full Scope Disabled", + "providerId": "scope", + "subType": "anonymous", + "subComponents": {}, + "config": {} + }, + { + "id": "12d8a070-da0e-4825-9fb5-6cb637e998e5", + "name": "Allowed Client Templates", + "providerId": "allowed-client-templates", + "subType": "authenticated", + "subComponents": {}, + "config": {} + }, + { + "id": "956db004-8833-4b95-9266-fedbf564c902", + "name": "Consent Required", + "providerId": "consent-required", + "subType": "anonymous", + "subComponents": {}, + "config": {} + }, + { + "id": "c3c67c56-3c88-46bd-8b23-d8f92dcd8231", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "oidc-usermodel-attribute-mapper", + "oidc-address-mapper", + "saml-user-property-mapper", + "oidc-sha256-pairwise-sub-mapper", + "oidc-full-name-mapper", + "saml-user-attribute-mapper", + "oidc-usermodel-property-mapper", + "saml-role-list-mapper" + ], + "consent-required-for-all-mappers": [ + "true" + ] + } + }, + { + "id": "fd4d4055-f3fa-491a-8dd4-ee8ee9354b6d", + "name": "Max Clients Limit", + "providerId": "max-clients", + "subType": "anonymous", + "subComponents": {}, + "config": { + "max-clients": [ + "200" + ] + } + }, + { + "id": "1982560c-6689-434f-9a3a-6c6c5513b014", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "oidc-address-mapper", + "saml-user-attribute-mapper", + "saml-user-property-mapper", + "saml-role-list-mapper", + "oidc-usermodel-attribute-mapper", + "oidc-usermodel-property-mapper", + "oidc-sha256-pairwise-sub-mapper", + "oidc-full-name-mapper" + ], + "consent-required-for-all-mappers": [ + "true" + ] + } + }, + { + "id": "c5ccbffe-a3fe-4c77-a6a7-3679e7f281f5", + "name": "Trusted Hosts", + "providerId": "trusted-hosts", + "subType": "anonymous", + "subComponents": {}, + "config": { + "host-sending-registration-request-must-match": [ + "true" + ], + "client-uris-must-match": [ + "true" + ] + } + } + ], + "org.keycloak.keys.KeyProvider": [ + { + "id": "bb5ce31b-932b-402b-8cc5-14e84e81e639", + "name": "hmac-generated", + "providerId": "hmac-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ] + } + }, + { + "id": "a8ddc80d-b0e3-4365-bd1f-12a47b2eb868", + "name": "rsa-generated", + "providerId": "rsa-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ] + } + }, + { + "id": "e3a30dae-8267-4673-b0f4-454491200cb9", + "name": "aes-generated", + "providerId": "aes-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ] + } + } + ] + }, + "internationalizationEnabled": false, + "supportedLocales": [], + "authenticationFlows": [ + { + "id": "662c0102-45d2-4719-b685-61a999d12f2d", + "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 + }, + { + "authenticator": "idp-email-verification", + "requirement": "ALTERNATIVE", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "requirement": "ALTERNATIVE", + "priority": 30, + "flowAlias": "Verify Existing Account by Re-authentication", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "a16d6bf5-3c20-41c1-b546-8a18fd64009d", + "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 + }, + { + "authenticator": "auth-otp-form", + "requirement": "OPTIONAL", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "5fc097f3-747a-478b-b01c-e0ca7cafdbb0", + "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": "f5368d26-e65c-459e-ae80-cb298f895872", + "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 + } + ] + }, + { + "id": "e02ce186-73d8-4ee7-a442-e1fe6bc66649", + "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 + }, + { + "authenticator": "direct-grant-validate-otp", + "requirement": "OPTIONAL", + "priority": 30, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "417fa197-dc0e-4e32-925b-197065587773", + "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 + } + ] + }, + { + "id": "a5745cc1-2413-41b3-bb72-a80c33c2e346", + "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 + }, + { + "authenticatorConfig": "create unique user config", + "authenticator": "idp-create-user-if-unique", + "requirement": "ALTERNATIVE", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "requirement": "ALTERNATIVE", + "priority": 30, + "flowAlias": "Handle Existing Account", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "17877629-e5a1-46c7-ac08-f8e8c765090f", + "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 + }, + { + "authenticator": "auth-otp-form", + "requirement": "OPTIONAL", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "3dd5e424-da91-4401-b56f-b54da15c73cb", + "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 + } + ] + }, + { + "id": "f0889639-f982-4354-bbec-1d7d80a1d24f", + "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 + } + ] + }, + { + "id": "07606ef3-c2ef-4b77-9660-2cc7fbe60be1", + "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 + }, + { + "authenticator": "reset-otp", + "requirement": "OPTIONAL", + "priority": 40, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "5bd8314b-75db-4677-9699-e3f5d814cc24", + "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": "19402384-99b2-4088-96ca-e50c2ba56b30", + "alias": "create unique user config", + "config": { + "require.password.update.after.registration": "false" + } + }, + { + "id": "388ce6bb-09c8-43a8-a429-5dbd5516510f", + "alias": "review profile config", + "config": { + "update.profile.on.first.login": "missing" + } + } + ], + "requiredActions": [ + { + "alias": "CONFIGURE_TOTP", + "name": "Configure OTP", + "providerId": "CONFIGURE_TOTP", + "enabled": true, + "defaultAction": false, + "config": {} + }, + { + "alias": "UPDATE_PASSWORD", + "name": "Update Password", + "providerId": "UPDATE_PASSWORD", + "enabled": true, + "defaultAction": false, + "config": {} + }, + { + "alias": "UPDATE_PROFILE", + "name": "Update Profile", + "providerId": "UPDATE_PROFILE", + "enabled": true, + "defaultAction": false, + "config": {} + }, + { + "alias": "VERIFY_EMAIL", + "name": "Verify Email", + "providerId": "VERIFY_EMAIL", + "enabled": true, + "defaultAction": false, + "config": {} + }, + { + "alias": "terms_and_conditions", + "name": "Terms and Conditions", + "providerId": "terms_and_conditions", + "enabled": false, + "defaultAction": false, + "config": {} + } + ], + "browserFlow": "browser", + "registrationFlow": "registration", + "directGrantFlow": "direct grant", + "resetCredentialsFlow": "reset credentials", + "clientAuthenticationFlow": "clients", + "dockerAuthenticationFlow": "docker auth", + "attributes": { + "_browser_header.xXSSProtection": "1; mode=block", + "_browser_header.xFrameOptions": "SAMEORIGIN", + "_browser_header.strictTransportSecurity": "max-age=31536000; includeSubDomains", + "permanentLockout": "false", + "quickLoginCheckMilliSeconds": "1000", + "_browser_header.xRobotsTag": "none", + "maxFailureWaitSeconds": "900", + "minimumQuickLoginWaitSeconds": "60", + "failureFactor": "30", + "actionTokenGeneratedByUserLifespan": "300", + "maxDeltaTimeSeconds": "43200", + "_browser_header.xContentTypeOptions": "nosniff", + "actionTokenGeneratedByAdminLifespan": "43200", + "bruteForceProtected": "false", + "_browser_header.contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", + "waitIncrementSeconds": "60" + }, + "userManagedAccessAllowed": false +} diff --git a/testsuite/admin-client-tests/src/test/resources/import/import-without-roles.json b/testsuite/admin-client-tests/src/test/resources/import/import-without-roles.json new file mode 100644 index 0000000..5f0c800 --- /dev/null +++ b/testsuite/admin-client-tests/src/test/resources/import/import-without-roles.json @@ -0,0 +1,1257 @@ +{ + "id": "import-without-roles", + "realm": "import-without-roles", + "notBefore": 0, + "revokeRefreshToken": false, + "refreshTokenMaxReuse": 0, + "accessTokenLifespan": 300, + "accessTokenLifespanForImplicitFlow": 900, + "ssoSessionIdleTimeout": 1800, + "ssoSessionMaxLifespan": 36000, + "offlineSessionIdleTimeout": 2592000, + "accessCodeLifespan": 60, + "accessCodeLifespanUserAction": 300, + "accessCodeLifespanLogin": 1800, + "actionTokenGeneratedByAdminLifespan": 43200, + "actionTokenGeneratedByUserLifespan": 300, + "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, + "defaultRoles": [ + "uma_authorization", + "offline_access" + ], + "requiredCredentials": [ + "password" + ], + "otpPolicyType": "totp", + "otpPolicyAlgorithm": "HmacSHA1", + "otpPolicyInitialCounter": 0, + "otpPolicyDigits": 6, + "otpPolicyLookAheadWindow": 1, + "otpPolicyPeriod": 30, + "otpSupportedApplications": [ + "FreeOTP", + "Google Authenticator" + ], + "clients": [ + { + "id": "938ba3e1-eabc-4bf2-bf87-43a5824b2824", + "clientId": "account", + "name": "${client_account}", + "baseUrl": "/auth/realms/import-without-roles/account", + "surrogateAuthRequired": false, + "enabled": true, + "clientAuthenticatorType": "client-secret", + "secret": "**********", + "defaultRoles": [ + "view-profile", + "manage-account" + ], + "redirectUris": [ + "/auth/realms/import-without-roles/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, + "protocolMappers": [ + { + "id": "d5c720ba-dcbd-4730-a15a-2d2fba1512c9", + "name": "role list", + "protocol": "saml", + "protocolMapper": "saml-role-list-mapper", + "consentRequired": false, + "config": { + "single": "false", + "attribute.nameformat": "Basic", + "attribute.name": "Role" + } + }, + { + "id": "d45c591d-ef75-4816-bc42-487ba44498cc", + "name": "family name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": true, + "consentText": "${familyName}", + "config": { + "userinfo.token.claim": "true", + "user.attribute": "lastName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "family_name", + "jsonType.label": "String" + } + }, + { + "id": "97285777-1f27-4222-87a0-f78dbc49829d", + "name": "full name", + "protocol": "openid-connect", + "protocolMapper": "oidc-full-name-mapper", + "consentRequired": true, + "consentText": "${fullName}", + "config": { + "id.token.claim": "true", + "access.token.claim": "true" + } + }, + { + "id": "8e341cf4-c7dc-40b2-b14e-634a4aacea44", + "name": "given name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": true, + "consentText": "${givenName}", + "config": { + "userinfo.token.claim": "true", + "user.attribute": "firstName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "given_name", + "jsonType.label": "String" + } + }, + { + "id": "cc510e62-52ea-4035-81d0-eb7dd2fb3722", + "name": "username", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": true, + "consentText": "${username}", + "config": { + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "preferred_username", + "jsonType.label": "String" + } + }, + { + "id": "3b7ce41b-66e1-4e6b-9ba8-4eafc4fb8b79", + "name": "email", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": true, + "consentText": "${email}", + "config": { + "userinfo.token.claim": "true", + "user.attribute": "email", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email", + "jsonType.label": "String" + } + } + ], + "useTemplateConfig": false, + "useTemplateScope": false, + "useTemplateMappers": false + }, + { + "id": "aceec783-0b6f-4eae-a4b9-4d97217d8c9b", + "clientId": "admin-cli", + "name": "${client_admin-cli}", + "surrogateAuthRequired": false, + "enabled": true, + "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, + "protocolMappers": [ + { + "id": "0437942b-0e5f-4b08-98e2-2c3a27ad1808", + "name": "role list", + "protocol": "saml", + "protocolMapper": "saml-role-list-mapper", + "consentRequired": false, + "config": { + "single": "false", + "attribute.nameformat": "Basic", + "attribute.name": "Role" + } + }, + { + "id": "52fa0e17-9634-45d5-9e6b-cd6f9803e7e7", + "name": "email", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": true, + "consentText": "${email}", + "config": { + "userinfo.token.claim": "true", + "user.attribute": "email", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email", + "jsonType.label": "String" + } + }, + { + "id": "c9301fee-8e33-47c3-856a-1fdb532c828f", + "name": "given name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": true, + "consentText": "${givenName}", + "config": { + "userinfo.token.claim": "true", + "user.attribute": "firstName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "given_name", + "jsonType.label": "String" + } + }, + { + "id": "be22824c-00b7-4803-9cde-f155b7410aa9", + "name": "full name", + "protocol": "openid-connect", + "protocolMapper": "oidc-full-name-mapper", + "consentRequired": true, + "consentText": "${fullName}", + "config": { + "id.token.claim": "true", + "access.token.claim": "true" + } + }, + { + "id": "149859e9-066a-426e-85bd-5476c0a67c79", + "name": "family name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": true, + "consentText": "${familyName}", + "config": { + "userinfo.token.claim": "true", + "user.attribute": "lastName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "family_name", + "jsonType.label": "String" + } + }, + { + "id": "e668c6e0-72e3-4394-a6fe-19047a9e29c7", + "name": "username", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": true, + "consentText": "${username}", + "config": { + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "preferred_username", + "jsonType.label": "String" + } + } + ], + "useTemplateConfig": false, + "useTemplateScope": false, + "useTemplateMappers": false + }, + { + "id": "e156734b-7039-4bf9-acdc-9c2f92b3981e", + "clientId": "broker", + "name": "${client_broker}", + "surrogateAuthRequired": false, + "enabled": true, + "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, + "protocolMappers": [ + { + "id": "bd70aec6-2696-4936-91f5-18389a7c925c", + "name": "full name", + "protocol": "openid-connect", + "protocolMapper": "oidc-full-name-mapper", + "consentRequired": true, + "consentText": "${fullName}", + "config": { + "id.token.claim": "true", + "access.token.claim": "true" + } + }, + { + "id": "0e8e108b-fa89-411b-a08c-b794e552825e", + "name": "family name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": true, + "consentText": "${familyName}", + "config": { + "userinfo.token.claim": "true", + "user.attribute": "lastName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "family_name", + "jsonType.label": "String" + } + }, + { + "id": "7e45f4b1-8323-4b5a-9009-54ec1ba8bc91", + "name": "given name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": true, + "consentText": "${givenName}", + "config": { + "userinfo.token.claim": "true", + "user.attribute": "firstName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "given_name", + "jsonType.label": "String" + } + }, + { + "id": "ce0da762-de49-4914-b217-cf289ebfd44a", + "name": "role list", + "protocol": "saml", + "protocolMapper": "saml-role-list-mapper", + "consentRequired": false, + "config": { + "single": "false", + "attribute.nameformat": "Basic", + "attribute.name": "Role" + } + }, + { + "id": "9250e31c-fadb-4e7a-9d1b-341626fbe6a3", + "name": "email", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": true, + "consentText": "${email}", + "config": { + "userinfo.token.claim": "true", + "user.attribute": "email", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email", + "jsonType.label": "String" + } + }, + { + "id": "5579d355-06dc-4e9c-b1ab-d968e44005ac", + "name": "username", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": true, + "consentText": "${username}", + "config": { + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "preferred_username", + "jsonType.label": "String" + } + } + ], + "useTemplateConfig": false, + "useTemplateScope": false, + "useTemplateMappers": false + }, + { + "id": "57219158-fed9-407c-97ca-aec699ab8389", + "clientId": "realm-management", + "name": "${client_realm-management}", + "surrogateAuthRequired": false, + "enabled": true, + "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, + "protocolMappers": [ + { + "id": "85fdc6bf-47fb-4eec-b277-aa1794b9a5f9", + "name": "role list", + "protocol": "saml", + "protocolMapper": "saml-role-list-mapper", + "consentRequired": false, + "config": { + "single": "false", + "attribute.nameformat": "Basic", + "attribute.name": "Role" + } + }, + { + "id": "e7ddaa35-1627-49e8-987a-53772fbeacbe", + "name": "username", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": true, + "consentText": "${username}", + "config": { + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "preferred_username", + "jsonType.label": "String" + } + }, + { + "id": "618c7891-57a0-45dc-80b5-363f6919c550", + "name": "email", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": true, + "consentText": "${email}", + "config": { + "userinfo.token.claim": "true", + "user.attribute": "email", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email", + "jsonType.label": "String" + } + }, + { + "id": "4919911a-f927-4c0d-8cc9-2944abc2dd4a", + "name": "full name", + "protocol": "openid-connect", + "protocolMapper": "oidc-full-name-mapper", + "consentRequired": true, + "consentText": "${fullName}", + "config": { + "id.token.claim": "true", + "access.token.claim": "true" + } + }, + { + "id": "24754ffa-4a34-44b2-8df3-87dc1e4d7698", + "name": "given name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": true, + "consentText": "${givenName}", + "config": { + "userinfo.token.claim": "true", + "user.attribute": "firstName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "given_name", + "jsonType.label": "String" + } + }, + { + "id": "ee2ab491-0b0c-4ac2-89eb-0a3f8e75b61d", + "name": "family name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": true, + "consentText": "${familyName}", + "config": { + "userinfo.token.claim": "true", + "user.attribute": "lastName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "family_name", + "jsonType.label": "String" + } + } + ], + "useTemplateConfig": false, + "useTemplateScope": false, + "useTemplateMappers": false + }, + { + "id": "b97fc203-0297-4eac-a1f7-35a7b6722f86", + "clientId": "security-admin-console", + "name": "${client_security-admin-console}", + "baseUrl": "/auth/admin/import-without-roles/console/index.html", + "surrogateAuthRequired": false, + "enabled": true, + "clientAuthenticatorType": "client-secret", + "secret": "**********", + "redirectUris": [ + "/auth/admin/import-without-roles/console/*" + ], + "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, + "protocolMappers": [ + { + "id": "abd17ea8-4fb5-4e66-9594-3c22ce38f515", + "name": "given name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": true, + "consentText": "${givenName}", + "config": { + "userinfo.token.claim": "true", + "user.attribute": "firstName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "given_name", + "jsonType.label": "String" + } + }, + { + "id": "26e30623-33ea-435f-9876-fe8469edc01f", + "name": "full name", + "protocol": "openid-connect", + "protocolMapper": "oidc-full-name-mapper", + "consentRequired": true, + "consentText": "${fullName}", + "config": { + "id.token.claim": "true", + "access.token.claim": "true" + } + }, + { + "id": "8a6e66f6-9901-40b3-9469-65396597a6d2", + "name": "family name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": true, + "consentText": "${familyName}", + "config": { + "userinfo.token.claim": "true", + "user.attribute": "lastName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "family_name", + "jsonType.label": "String" + } + }, + { + "id": "5a50bd32-00c0-4d8b-b64c-8bc0c0a7b1a1", + "name": "username", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": true, + "consentText": "${username}", + "config": { + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "preferred_username", + "jsonType.label": "String" + } + }, + { + "id": "0cead636-2f98-4f6b-b261-22a258d795e0", + "name": "email", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": true, + "consentText": "${email}", + "config": { + "userinfo.token.claim": "true", + "user.attribute": "email", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email", + "jsonType.label": "String" + } + }, + { + "id": "8698187e-6b0a-4c03-814f-4668de90948d", + "name": "role list", + "protocol": "saml", + "protocolMapper": "saml-role-list-mapper", + "consentRequired": false, + "config": { + "single": "false", + "attribute.nameformat": "Basic", + "attribute.name": "Role" + } + }, + { + "id": "a7c95686-3b2c-4bda-a07b-cda77dfbad6b", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "consentText": "${locale}", + "config": { + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" + } + } + ], + "useTemplateConfig": false, + "useTemplateScope": false, + "useTemplateMappers": false + } + ], + "clientTemplates": [], + "browserSecurityHeaders": { + "xContentTypeOptions": "nosniff", + "xRobotsTag": "none", + "xFrameOptions": "SAMEORIGIN", + "xXSSProtection": "1; mode=block", + "contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", + "strictTransportSecurity": "max-age=31536000; includeSubDomains" + }, + "smtpServer": {}, + "eventsEnabled": false, + "eventsListeners": [ + "jboss-logging" + ], + "enabledEventTypes": [], + "adminEventsEnabled": false, + "adminEventsDetailsEnabled": false, + "components": { + "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy": [ + { + "id": "5b942906-8a41-4ccd-9ad9-ea3ae24e6d56", + "name": "Trusted Hosts", + "providerId": "trusted-hosts", + "subType": "anonymous", + "subComponents": {}, + "config": { + "host-sending-registration-request-must-match": [ + "true" + ], + "client-uris-must-match": [ + "true" + ] + } + }, + { + "id": "211d788e-3df1-4c01-9ee2-1085945544f2", + "name": "Full Scope Disabled", + "providerId": "scope", + "subType": "anonymous", + "subComponents": {}, + "config": {} + }, + { + "id": "16c3becf-16d8-4fa1-af72-9ca8f9c2ce31", + "name": "Allowed Client Templates", + "providerId": "allowed-client-templates", + "subType": "authenticated", + "subComponents": {}, + "config": {} + }, + { + "id": "769242e8-bb32-4704-af9f-290944a8420a", + "name": "Allowed Client Templates", + "providerId": "allowed-client-templates", + "subType": "anonymous", + "subComponents": {}, + "config": {} + }, + { + "id": "93ddca2c-e4df-4a00-a0f4-76c6838d56f4", + "name": "Consent Required", + "providerId": "consent-required", + "subType": "anonymous", + "subComponents": {}, + "config": {} + }, + { + "id": "95fd31fb-f513-4bea-bca0-61d7d030b1b2", + "name": "Max Clients Limit", + "providerId": "max-clients", + "subType": "anonymous", + "subComponents": {}, + "config": { + "max-clients": [ + "200" + ] + } + }, + { + "id": "bdd962d7-b76b-4f52-9aef-d19f08d7137b", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "saml-role-list-mapper", + "saml-user-attribute-mapper", + "oidc-sha256-pairwise-sub-mapper", + "oidc-usermodel-attribute-mapper", + "oidc-full-name-mapper", + "oidc-usermodel-property-mapper", + "saml-user-property-mapper", + "oidc-address-mapper" + ], + "consent-required-for-all-mappers": [ + "true" + ] + } + }, + { + "id": "f96fa5be-8af3-425e-9a40-a14a6b56bdb3", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "oidc-full-name-mapper", + "saml-user-attribute-mapper", + "saml-role-list-mapper", + "oidc-usermodel-attribute-mapper", + "oidc-address-mapper", + "oidc-sha256-pairwise-sub-mapper", + "oidc-usermodel-property-mapper", + "saml-user-property-mapper" + ], + "consent-required-for-all-mappers": [ + "true" + ] + } + } + ], + "org.keycloak.keys.KeyProvider": [ + { + "id": "950cd5f7-9e73-434b-926a-463c5d7e8985", + "name": "hmac-generated", + "providerId": "hmac-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ] + } + }, + { + "id": "66a95a1b-788b-4942-805f-5c9552755c76", + "name": "aes-generated", + "providerId": "aes-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ] + } + }, + { + "id": "41640c93-40e7-4dc2-beae-2887f8bbebc2", + "name": "rsa-generated", + "providerId": "rsa-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ] + } + } + ] + }, + "internationalizationEnabled": false, + "supportedLocales": [], + "authenticationFlows": [ + { + "id": "bed29d4f-aba7-4992-a600-18c0a28c1fc3", + "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 + }, + { + "authenticator": "idp-email-verification", + "requirement": "ALTERNATIVE", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "requirement": "ALTERNATIVE", + "priority": 30, + "flowAlias": "Verify Existing Account by Re-authentication", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "e8b8f564-6d56-4171-ba36-a8922c6eae49", + "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 + }, + { + "authenticator": "auth-otp-form", + "requirement": "OPTIONAL", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "83dcb1e4-2c7c-4494-825d-f2677cbc114c", + "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": "b0a80dc3-d473-468e-b6e8-f1d306c21360", + "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 + } + ] + }, + { + "id": "a1882f46-54be-4738-847a-32e849d53240", + "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 + }, + { + "authenticator": "direct-grant-validate-otp", + "requirement": "OPTIONAL", + "priority": 30, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "c727a208-587c-4f27-8f48-ba2a0d4effdd", + "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 + } + ] + }, + { + "id": "6a6ac775-4000-4ccf-9271-6cb599297d4b", + "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 + }, + { + "authenticatorConfig": "create unique user config", + "authenticator": "idp-create-user-if-unique", + "requirement": "ALTERNATIVE", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "requirement": "ALTERNATIVE", + "priority": 30, + "flowAlias": "Handle Existing Account", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "2a84808d-e0c7-4759-aee8-cf9229542429", + "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 + }, + { + "authenticator": "auth-otp-form", + "requirement": "OPTIONAL", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "817f990a-1c46-464c-9051-5e0ae39d63db", + "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 + } + ] + }, + { + "id": "266fca50-7b69-4cd4-80eb-a569e87ff8a2", + "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 + } + ] + }, + { + "id": "b516cb39-8f6d-4d08-ac82-236377be6500", + "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 + }, + { + "authenticator": "reset-otp", + "requirement": "OPTIONAL", + "priority": 40, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "9b9ae730-11e0-451f-b693-e32f09415e42", + "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": "b6d38dcd-7b53-4991-b4eb-c866ce3c5e70", + "alias": "create unique user config", + "config": { + "require.password.update.after.registration": "false" + } + }, + { + "id": "8408f503-b929-422f-b52b-277cebda44ba", + "alias": "review profile config", + "config": { + "update.profile.on.first.login": "missing" + } + } + ], + "requiredActions": [ + { + "alias": "CONFIGURE_TOTP", + "name": "Configure OTP", + "providerId": "CONFIGURE_TOTP", + "enabled": true, + "defaultAction": false, + "config": {} + }, + { + "alias": "UPDATE_PASSWORD", + "name": "Update Password", + "providerId": "UPDATE_PASSWORD", + "enabled": true, + "defaultAction": false, + "config": {} + }, + { + "alias": "UPDATE_PROFILE", + "name": "Update Profile", + "providerId": "UPDATE_PROFILE", + "enabled": true, + "defaultAction": false, + "config": {} + }, + { + "alias": "VERIFY_EMAIL", + "name": "Verify Email", + "providerId": "VERIFY_EMAIL", + "enabled": true, + "defaultAction": false, + "config": {} + }, + { + "alias": "terms_and_conditions", + "name": "Terms and Conditions", + "providerId": "terms_and_conditions", + "enabled": false, + "defaultAction": false, + "config": {} + } + ], + "browserFlow": "browser", + "registrationFlow": "registration", + "directGrantFlow": "direct grant", + "resetCredentialsFlow": "reset credentials", + "clientAuthenticationFlow": "clients", + "dockerAuthenticationFlow": "docker auth", + "attributes": { + "_browser_header.xXSSProtection": "1; mode=block", + "_browser_header.xFrameOptions": "SAMEORIGIN", + "_browser_header.strictTransportSecurity": "max-age=31536000; includeSubDomains", + "permanentLockout": "false", + "quickLoginCheckMilliSeconds": "1000", + "_browser_header.xRobotsTag": "none", + "maxFailureWaitSeconds": "900", + "minimumQuickLoginWaitSeconds": "60", + "failureFactor": "30", + "actionTokenGeneratedByUserLifespan": "300", + "maxDeltaTimeSeconds": "43200", + "_browser_header.xContentTypeOptions": "nosniff", + "actionTokenGeneratedByAdminLifespan": "43200", + "bruteForceProtected": "false", + "_browser_header.contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", + "waitIncrementSeconds": "60" + }, + "keycloakVersion": "4.0.0.Beta2-SNAPSHOT", + "userManagedAccessAllowed": false +} \ No newline at end of file diff --git a/testsuite/admin-client-tests/src/test/resources/import/partial-authentication-flows-import.json b/testsuite/admin-client-tests/src/test/resources/import/partial-authentication-flows-import.json new file mode 100644 index 0000000..d1da7db --- /dev/null +++ b/testsuite/admin-client-tests/src/test/resources/import/partial-authentication-flows-import.json @@ -0,0 +1,23 @@ +{ + "enabled": true, + "realm": "partial-authentication-flows-import", + "authenticationFlows": [ + { + "alias": "X.509 browser", + "description": "Browser-based authentication", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": false, + "authenticationExecutions": [ + { + "requirement": "ALTERNATIVE", + "priority": 10, + "authenticator": "auth-cookie", + "authenticatorFlow": false, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + } + ] +} \ No newline at end of file diff --git a/testsuite/admin-client-tests/src/test/resources/import/partial-import.json b/testsuite/admin-client-tests/src/test/resources/import/partial-import.json new file mode 100644 index 0000000..06e30ba --- /dev/null +++ b/testsuite/admin-client-tests/src/test/resources/import/partial-import.json @@ -0,0 +1,637 @@ +{ + "id": "partial-import", + "realm": "partial-import", + "notBefore": 0, + "revokeRefreshToken": false, + "refreshTokenMaxReuse": 0, + "accessTokenLifespan": 300, + "accessTokenLifespanForImplicitFlow": 900, + "ssoSessionIdleTimeout": 1800, + "ssoSessionMaxLifespan": 36000, + "offlineSessionIdleTimeout": 2592000, + "accessCodeLifespan": 60, + "accessCodeLifespanUserAction": 300, + "accessCodeLifespanLogin": 1800, + "actionTokenGeneratedByAdminLifespan": 43200, + "actionTokenGeneratedByUserLifespan": 300, + "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, + "defaultRoles": [ + "offline_access", + "uma_authorization" + ], + "requiredCredentials": [ + "password" + ], + "otpPolicyType": "totp", + "otpPolicyAlgorithm": "HmacSHA1", + "otpPolicyInitialCounter": 0, + "otpPolicyDigits": 6, + "otpPolicyLookAheadWindow": 1, + "otpPolicyPeriod": 30, + "otpSupportedApplications": [ + "FreeOTP", + "Google Authenticator" + ], + "browserSecurityHeaders": { + "xContentTypeOptions": "nosniff", + "xRobotsTag": "none", + "xFrameOptions": "SAMEORIGIN", + "xXSSProtection": "1; mode=block", + "contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", + "strictTransportSecurity": "max-age=31536000; includeSubDomains" + }, + "smtpServer": {}, + "eventsEnabled": false, + "eventsListeners": [ + "jboss-logging" + ], + "enabledEventTypes": [], + "adminEventsEnabled": false, + "adminEventsDetailsEnabled": false, + "components": { + "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy": [ + { + "id": "81655ead-ccb8-4ccc-8824-fd0edda7ea67", + "name": "Max Clients Limit", + "providerId": "max-clients", + "subType": "anonymous", + "subComponents": {}, + "config": { + "max-clients": [ + "200" + ] + } + }, + { + "id": "3e48a4ae-c5a1-4fb7-a685-4adce3b1157b", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "saml-user-attribute-mapper", + "saml-user-property-mapper", + "oidc-sha256-pairwise-sub-mapper", + "saml-role-list-mapper", + "oidc-usermodel-attribute-mapper", + "oidc-usermodel-property-mapper", + "oidc-address-mapper", + "oidc-full-name-mapper" + ], + "consent-required-for-all-mappers": [ + "true" + ] + } + }, + { + "id": "a46ff320-c841-45b0-800e-9219ed627ffb", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "saml-user-attribute-mapper", + "oidc-address-mapper", + "oidc-usermodel-attribute-mapper", + "oidc-usermodel-property-mapper", + "oidc-sha256-pairwise-sub-mapper", + "saml-role-list-mapper", + "saml-user-property-mapper", + "oidc-full-name-mapper" + ], + "consent-required-for-all-mappers": [ + "true" + ] + } + }, + { + "id": "fe0b7fac-e039-4b43-997d-28e41685c099", + "name": "Allowed Client Templates", + "providerId": "allowed-client-templates", + "subType": "anonymous", + "subComponents": {}, + "config": {} + }, + { + "id": "5813bff1-3f03-48b0-9bba-1a08f2b76dff", + "name": "Trusted Hosts", + "providerId": "trusted-hosts", + "subType": "anonymous", + "subComponents": {}, + "config": { + "host-sending-registration-request-must-match": [ + "true" + ], + "client-uris-must-match": [ + "true" + ] + } + }, + { + "id": "8febb3aa-3aac-4b76-a950-c96a81b88507", + "name": "Full Scope Disabled", + "providerId": "scope", + "subType": "anonymous", + "subComponents": {}, + "config": {} + }, + { + "id": "bc470e1b-6ec9-4cdd-ae2c-0de565d94f75", + "name": "Allowed Client Templates", + "providerId": "allowed-client-templates", + "subType": "authenticated", + "subComponents": {}, + "config": {} + }, + { + "id": "15685bc6-6696-4dd5-bedc-32148de67997", + "name": "Consent Required", + "providerId": "consent-required", + "subType": "anonymous", + "subComponents": {}, + "config": {} + } + ], + "org.keycloak.keys.KeyProvider": [ + { + "id": "50ad939b-d517-499a-abdb-6a07d7c44189", + "name": "aes-generated", + "providerId": "aes-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ] + } + }, + { + "id": "be86234c-d6c4-49d2-908c-84ddadd318b7", + "name": "hmac-generated", + "providerId": "hmac-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ] + } + }, + { + "id": "f0fa644d-6f10-49f2-aec5-d2f9bc810146", + "name": "rsa-generated", + "providerId": "rsa-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ] + } + } + ] + }, + "internationalizationEnabled": false, + "supportedLocales": [], + "authenticationFlows": [ + { + "id": "94d8fa06-b1cd-4a62-8919-04eea95e09e4", + "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 + }, + { + "authenticator": "idp-email-verification", + "requirement": "ALTERNATIVE", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "requirement": "ALTERNATIVE", + "priority": 30, + "flowAlias": "Verify Existing Account by Re-authentication", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "6d6ee1c8-20d5-464a-8547-fef968daf7f2", + "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 + }, + { + "authenticator": "auth-otp-form", + "requirement": "OPTIONAL", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "9da83947-e030-4097-b8d2-a17990c50da7", + "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": "133ae33d-5cba-4829-9f2d-a50320e43faf", + "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 + } + ] + }, + { + "id": "c8d171b8-efa5-40d1-8c6f-67c89eb08a54", + "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 + }, + { + "authenticator": "direct-grant-validate-otp", + "requirement": "OPTIONAL", + "priority": 30, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "49e99ea3-dc57-47dd-93f4-25662ab408e3", + "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 + } + ] + }, + { + "id": "50432c1d-18d9-469c-9e08-2579288442a2", + "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 + }, + { + "authenticatorConfig": "create unique user config", + "authenticator": "idp-create-user-if-unique", + "requirement": "ALTERNATIVE", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "requirement": "ALTERNATIVE", + "priority": 30, + "flowAlias": "Handle Existing Account", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "6eb36200-bf93-4a1f-821a-59d683460d35", + "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 + }, + { + "authenticator": "auth-otp-form", + "requirement": "OPTIONAL", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "3f4d6db4-de10-49f4-9897-a13327167566", + "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 + } + ] + }, + { + "id": "3c1935ce-ba99-4395-af59-f0a50454e567", + "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 + } + ] + }, + { + "id": "ffab670c-90a5-4067-bbb8-61aa605c1481", + "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 + }, + { + "authenticator": "reset-otp", + "requirement": "OPTIONAL", + "priority": 40, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "7af61e40-1647-4411-9bde-4cc89d4e5c3b", + "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": "1c5b8534-ee70-4506-996b-369bbb902b83", + "alias": "create unique user config", + "config": { + "require.password.update.after.registration": "false" + } + }, + { + "id": "6afe4d4e-cd76-4bcf-8f1e-6971d72474c7", + "alias": "review profile config", + "config": { + "update.profile.on.first.login": "missing" + } + } + ], + "requiredActions": [ + { + "alias": "CONFIGURE_TOTP", + "name": "Configure OTP", + "providerId": "CONFIGURE_TOTP", + "enabled": true, + "defaultAction": false, + "config": {} + }, + { + "alias": "UPDATE_PASSWORD", + "name": "Update Password", + "providerId": "UPDATE_PASSWORD", + "enabled": true, + "defaultAction": false, + "config": {} + }, + { + "alias": "UPDATE_PROFILE", + "name": "Update Profile", + "providerId": "UPDATE_PROFILE", + "enabled": true, + "defaultAction": false, + "config": {} + }, + { + "alias": "VERIFY_EMAIL", + "name": "Verify Email", + "providerId": "VERIFY_EMAIL", + "enabled": true, + "defaultAction": false, + "config": {} + }, + { + "alias": "terms_and_conditions", + "name": "Terms and Conditions", + "providerId": "terms_and_conditions", + "enabled": false, + "defaultAction": false, + "config": {} + } + ], + "browserFlow": "browser", + "registrationFlow": "registration", + "directGrantFlow": "direct grant", + "resetCredentialsFlow": "reset credentials", + "clientAuthenticationFlow": "clients", + "dockerAuthenticationFlow": "docker auth", + "attributes": { + "_browser_header.xXSSProtection": "1; mode=block", + "_browser_header.xFrameOptions": "SAMEORIGIN", + "_browser_header.strictTransportSecurity": "max-age=31536000; includeSubDomains", + "permanentLockout": "false", + "quickLoginCheckMilliSeconds": "1000", + "_browser_header.xRobotsTag": "none", + "maxFailureWaitSeconds": "900", + "minimumQuickLoginWaitSeconds": "60", + "failureFactor": "30", + "actionTokenGeneratedByUserLifespan": "300", + "maxDeltaTimeSeconds": "43200", + "_browser_header.xContentTypeOptions": "nosniff", + "actionTokenGeneratedByAdminLifespan": "43200", + "bruteForceProtected": "false", + "_browser_header.contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", + "waitIncrementSeconds": "60" + }, + "userManagedAccessAllowed": false +} diff --git a/testsuite/admin-client-tests/src/test/resources/import/sample-authz-partial-import.json b/testsuite/admin-client-tests/src/test/resources/import/sample-authz-partial-import.json new file mode 100644 index 0000000..4c9b9ab --- /dev/null +++ b/testsuite/admin-client-tests/src/test/resources/import/sample-authz-partial-import.json @@ -0,0 +1,58 @@ +{ + "allowRemoteResourceManagement": true, + "policyEnforcementMode": "ENFORCING", + "resources": [ + { + "name": "Default Resource", + "type": "urn:test:resources:default", + "ownerManagedAccess": false, + "attributes": {}, + "uris": [ + "/*" + ] + }, + { + "name": "test", + "type": "test", + "ownerManagedAccess": true, + "displayName": "test", + "attributes": {}, + "uris": [ + "/test" + ] + } + ], + "policies": [ + { + "name": "Default Policy", + "description": "A policy that grants access only for users within this realm", + "type": "script-scripts/default-policy.js", + "logic": "POSITIVE", + "decisionStrategy": "AFFIRMATIVE" + }, + { + "name": "Default Permission", + "description": "A permission that applies to the default resource type", + "type": "resource", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "defaultResourceType": "urn:test:resources:default", + "applyPolicies": "[\"Default Policy\"]" + } + }, + { + "name": "test-permission", + "description": "test-permission", + "type": "resource", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"test\"]", + "applyPolicies": "[\"Default Policy\"]" + } + } + ], + "scopes": [], + "decisionStrategy": "UNANIMOUS" +} diff --git a/testsuite/admin-client-tests/src/test/resources/import/testrealm-authenticator-config-null.json b/testsuite/admin-client-tests/src/test/resources/import/testrealm-authenticator-config-null.json new file mode 100644 index 0000000..916ac2a --- /dev/null +++ b/testsuite/admin-client-tests/src/test/resources/import/testrealm-authenticator-config-null.json @@ -0,0 +1,25 @@ +{ + "authenticationFlows": [ + { + "alias": "browser", + "authenticationExecutions": [ + { + "authenticator": "auth-spnego", + "authenticatorFlow": false, + "priority": 20, + "requirement": "ALTERNATIVE", + "userSetupAllowed": false + } + ], + "builtIn": true, + "description": "browser based authentification2", + "id": "3e6ccf87-5473-4eb0-8cbb-28f6b9e6f4d6", + "providerId": "basic-flow", + "topLevel": true + } + ], + "displayName": "CEZ", + "enabled": true, + "id": "cez", + "realm": "cez" +} diff --git a/testsuite/admin-client-tests/src/test/resources/import/testrealm-keycloak-6146-error.json b/testsuite/admin-client-tests/src/test/resources/import/testrealm-keycloak-6146-error.json new file mode 100644 index 0000000..db16e08 --- /dev/null +++ b/testsuite/admin-client-tests/src/test/resources/import/testrealm-keycloak-6146-error.json @@ -0,0 +1,265 @@ +{ + "id":"secure-app", + "realm":"secure-app", + "enabled":true, + "sslRequired":"external", + "bruteForceProtected" : true, + "permanentLockout": true, + "maxFailureWaitSeconds" : 900, + "minimumQuickLoginWaitSeconds" : 60, + "waitIncrementSeconds" : 60, + "quickLoginCheckMilliSeconds" : 1000, + "maxDeltaTimeSeconds" : 43200, + "failureFactor" : 3, + "eventsEnabled" : true, + "eventsListeners" : [ "jboss-logging" ], + "enabledEventTypes" : [ "SEND_RESET_PASSWORD", "UPDATE_TOTP", "REMOVE_TOTP", "REVOKE_GRANT", "LOGIN_ERROR", "CLIENT_LOGIN", "RESET_PASSWORD_ERROR", "IMPERSONATE_ERROR", "CODE_TO_TOKEN_ERROR", "CUSTOM_REQUIRED_ACTION", "RESTART_AUTHENTICATION", "UPDATE_PROFILE_ERROR", "IMPERSONATE", "LOGIN", "UPDATE_PASSWORD_ERROR", "CLIENT_INITIATED_ACCOUNT_LINKING", "REGISTER", "LOGOUT", "CLIENT_REGISTER", "IDENTITY_PROVIDER_LINK_ACCOUNT", "UPDATE_PASSWORD", "FEDERATED_IDENTITY_LINK_ERROR", "CLIENT_DELETE", "IDENTITY_PROVIDER_FIRST_LOGIN", "VERIFY_EMAIL", "CLIENT_DELETE_ERROR", "CLIENT_LOGIN_ERROR", "RESTART_AUTHENTICATION_ERROR", "REMOVE_FEDERATED_IDENTITY_ERROR", "EXECUTE_ACTIONS", "SEND_IDENTITY_PROVIDER_LINK_ERROR", "EXECUTE_ACTION_TOKEN_ERROR", "SEND_VERIFY_EMAIL", "EXECUTE_ACTIONS_ERROR", "REMOVE_FEDERATED_IDENTITY", "IDENTITY_PROVIDER_POST_LOGIN", "IDENTITY_PROVIDER_LINK_ACCOUNT_ERROR", "UPDATE_EMAIL", "REGISTER_ERROR", "REVOKE_GRANT_ERROR", "LOGOUT_ERROR", "UPDATE_EMAIL_ERROR", "EXECUTE_ACTION_TOKEN", "CLIENT_UPDATE_ERROR", "UPDATE_PROFILE", "FEDERATED_IDENTITY_LINK", "CLIENT_REGISTER_ERROR", "SEND_VERIFY_EMAIL_ERROR", "SEND_IDENTITY_PROVIDER_LINK", "RESET_PASSWORD", "CLIENT_INITIATED_ACCOUNT_LINKING_ERROR", "REMOVE_TOTP_ERROR", "VERIFY_EMAIL_ERROR", "SEND_RESET_PASSWORD_ERROR", "CLIENT_UPDATE", "IDENTITY_PROVIDER_POST_LOGIN_ERROR", "CUSTOM_REQUIRED_ACTION_ERROR", "UPDATE_TOTP_ERROR", "CODE_TO_TOKEN", "IDENTITY_PROVIDER_FIRST_LOGIN_ERROR" ], + "adminEventsEnabled" : true, + "adminEventsDetailsEnabled" : true, + "requiredCredentials":[ + "password" + ], + "passwordPolicy": "length(10) and lowerCase(1) and upperCase(1) and passwordHistory(3) and digits(1) and specialChars(1) and hashAlgorithm(pbkdf2-sha256)", + "users":[ + { + "username":"user1", + "firstName" : "User", + "lastName" : "1", + "email" : "user1@feedhenry.org", + "enabled":true, + "credentials":[ + { + "type":"password", + "value":"123" + } + ], + "clientRoles":{ + "account":[ + "view-profile", + "manage-account" + ] + }, + "attributes":{ + + }, + "groups" : [ "/Mobile Users" ] + }, + { + "username":"user2", + "firstName" : "User", + "lastName" : "2", + "email" : "user2@feedhenry.org", + "enabled":true, + "credentials":[ + { + "type":"password", + "value":"123" + } + ], + "clientRoles":{ + "account":[ + "view-profile", + "manage-account" + ] + }, + "attributes":{ + + }, + "groups" : [ "/Mobile Users" ] + }, + { + "username":"secure-user", + "enabled" : true, + "emailVerified" : true, + "firstName" : "Secure", + "lastName" : "User", + "email" : "secure-user@feedhenry.org", + "credentials":[ + { + "type":"password", + "value":"123" + } + ], + "requiredActions" : [ "CONFIGURE_TOTP" ], + "realmRoles" : [ ], + "clientRoles":{ + "account":[ + "view-profile", + "manage-account" + ] + }, + "attributes":{ + + }, + "groups" : [ "/Mobile Users", "/API Access" ] + }, + { + "username":"secure-user2", + "enabled" : true, + "emailVerified" : true, + "firstName" : "Secure", + "lastName" : "User 2", + "email" : "secure-user2@feedhenry.org", + "credentials":[ + { + "type":"password", + "value":"123" + } + ], + "requiredActions" : [ "CONFIGURE_TOTP" ], + "realmRoles" : [ ], + "clientRoles":{ + "account":[ + "view-profile", + "manage-account" + ] + }, + "attributes":{ + + }, + "groups" : [ "/Mobile Users", "/API Access" ] + }, + { + "username":"superuser", + "enabled" : true, + "emailVerified" : true, + "firstName" : "Super", + "lastName" : "User", + "email" : "superuser@feedhenry.org", + "credentials":[ + { + "type":"password", + "value":"123" + } + ], + "realmRoles" : [ ], + "clientRoles":{ + "account":[ + "view-profile", + "manage-account" + ] + }, + "attributes":{ + + }, + "groups" : [ "/Mobile Users", "/API Access", "Superuser" ] + } + ], + "clients":[ + { + "clientId":"api-server", + "enabled":true, + "bearerOnly":true, + "adminUrl":"http://localhost:8080" + }, + { + "clientId":"client-app", + "enabled":true, + "publicClient":true, + "redirectUris":[ + "com.feedhenry.securenativeandroidtemplate:/callback" + ], + "webOrigins":[ + "*" + ] + } + ], + "clientScopeMappings":{ + "account":[ + { + "client":"api-server", + "roles":[ + "view-profile" + ] + }, + { + "client":"client-app", + "roles":[ + "view-profile" + ] + } + ] + }, + "groups" : [ { + "name" : "Mobile Users", + "path" : "/Mobile Users", + "attributes" : { }, + "realmRoles" : [ "mobile-user" ], + "clientRoles" : { }, + "subGroups" : [ ] + }, + { + "name" : "API Access", + "path" : "/API Access", + "attributes" : { }, + "realmRoles" : [ "mobile-user", "api-access" ], + "clientRoles" : { }, + "subGroups" : [ ] + }, + { + "name" : "Superuser", + "path" : "/Superusers", + "attributes" : { }, + "realmRoles" : [ "mobile-user", "api-access", "superuser" ], + "clientRoles" : { }, + "subGroups" : [ ] + } ], + "roles":{ + "client":{ + "api-server":[ + + ], + "client-app":[ + + ] + } + }, + "authenticatorConfig" : [ { + "alias" : "authenticator config", + "config" : { + "defaultProvider" : "" + } + }, { + "alias" : "create unique user config", + "config" : { + "require.password.update.after.registration" : "false" + } + }, { + "alias" : "review profile config", + "config" : { + "update.profile.on.first.login" : "missing" + } + }, { + "alias" : "x509 browser auth config", + "config" : { + "x509-cert-auth.extendedkeyusage" : "", + "x509-cert-auth.mapper-selection.user-attribute-name" : "usercertificate", + "x509-cert-auth.ocsp-responder-uri" : "", + "x509-cert-auth.regular-expression" : "(.*?)(?:$)", + "x509-cert-auth.crl-checking-enabled" : "", + "x509-cert-auth.confirmation-page-disallowed" : "", + "x509-cert-auth.keyusage" : "", + "x509-cert-auth.mapper-selection" : "Username or Email", + "x509-cert-auth.crl-relative-path" : "crl.pem", + "x509-cert-auth.crldp-checking-enabled" : "false", + "x509-cert-auth.mapping-source-selection" : "Subject's Common Name", + "x509-cert-auth.ocsp-checking-enabled" : "" + } + }, { + "alias" : "x509 direct grant config", + "config" : { + "x509-cert-auth.extendedkeyusage" : "", + "x509-cert-auth.mapper-selection.user-attribute-name" : "usercertificate", + "x509-cert-auth.ocsp-responder-uri" : "", + "x509-cert-auth.regular-expression" : "(.*?)(?:$)", + "x509-cert-auth.crl-checking-enabled" : "", + "x509-cert-auth.confirmation-page-disallowed" : "", + "x509-cert-auth.keyusage" : "", + "x509-cert-auth.mapper-selection" : "Username or Email", + "x509-cert-auth.crl-relative-path" : "crl.pem", + "x509-cert-auth.crldp-checking-enabled" : "false", + "x509-cert-auth.mapping-source-selection" : "Subject's Common Name", + "x509-cert-auth.ocsp-checking-enabled" : "" + } + } ] +} diff --git a/testsuite/admin-client-tests/src/test/resources/import/testrealm-keycloak-6146.json b/testsuite/admin-client-tests/src/test/resources/import/testrealm-keycloak-6146.json new file mode 100644 index 0000000..30b87fa --- /dev/null +++ b/testsuite/admin-client-tests/src/test/resources/import/testrealm-keycloak-6146.json @@ -0,0 +1,266 @@ +{ + "id":"secure-app", + "realm":"secure-app", + "accessCodeLifespanUserAction" : 300, + "enabled":true, + "sslRequired":"external", + "bruteForceProtected" : true, + "permanentLockout": true, + "maxFailureWaitSeconds" : 900, + "minimumQuickLoginWaitSeconds" : 60, + "waitIncrementSeconds" : 60, + "quickLoginCheckMilliSeconds" : 1000, + "maxDeltaTimeSeconds" : 43200, + "failureFactor" : 3, + "eventsEnabled" : true, + "eventsListeners" : [ "jboss-logging" ], + "enabledEventTypes" : [ "SEND_RESET_PASSWORD", "UPDATE_TOTP", "REMOVE_TOTP", "REVOKE_GRANT", "LOGIN_ERROR", "CLIENT_LOGIN", "RESET_PASSWORD_ERROR", "IMPERSONATE_ERROR", "CODE_TO_TOKEN_ERROR", "CUSTOM_REQUIRED_ACTION", "RESTART_AUTHENTICATION", "UPDATE_PROFILE_ERROR", "IMPERSONATE", "LOGIN", "UPDATE_PASSWORD_ERROR", "CLIENT_INITIATED_ACCOUNT_LINKING", "REGISTER", "LOGOUT", "CLIENT_REGISTER", "IDENTITY_PROVIDER_LINK_ACCOUNT", "UPDATE_PASSWORD", "FEDERATED_IDENTITY_LINK_ERROR", "CLIENT_DELETE", "IDENTITY_PROVIDER_FIRST_LOGIN", "VERIFY_EMAIL", "CLIENT_DELETE_ERROR", "CLIENT_LOGIN_ERROR", "RESTART_AUTHENTICATION_ERROR", "REMOVE_FEDERATED_IDENTITY_ERROR", "EXECUTE_ACTIONS", "SEND_IDENTITY_PROVIDER_LINK_ERROR", "EXECUTE_ACTION_TOKEN_ERROR", "SEND_VERIFY_EMAIL", "EXECUTE_ACTIONS_ERROR", "REMOVE_FEDERATED_IDENTITY", "IDENTITY_PROVIDER_POST_LOGIN", "IDENTITY_PROVIDER_LINK_ACCOUNT_ERROR", "UPDATE_EMAIL", "REGISTER_ERROR", "REVOKE_GRANT_ERROR", "LOGOUT_ERROR", "UPDATE_EMAIL_ERROR", "EXECUTE_ACTION_TOKEN", "CLIENT_UPDATE_ERROR", "UPDATE_PROFILE", "FEDERATED_IDENTITY_LINK", "CLIENT_REGISTER_ERROR", "SEND_VERIFY_EMAIL_ERROR", "SEND_IDENTITY_PROVIDER_LINK", "RESET_PASSWORD", "CLIENT_INITIATED_ACCOUNT_LINKING_ERROR", "REMOVE_TOTP_ERROR", "VERIFY_EMAIL_ERROR", "SEND_RESET_PASSWORD_ERROR", "CLIENT_UPDATE", "IDENTITY_PROVIDER_POST_LOGIN_ERROR", "CUSTOM_REQUIRED_ACTION_ERROR", "UPDATE_TOTP_ERROR", "CODE_TO_TOKEN", "IDENTITY_PROVIDER_FIRST_LOGIN_ERROR" ], + "adminEventsEnabled" : true, + "adminEventsDetailsEnabled" : true, + "requiredCredentials":[ + "password" + ], + "passwordPolicy": "length(10) and lowerCase(1) and upperCase(1) and passwordHistory(3) and digits(1) and specialChars(1) and hashAlgorithm(pbkdf2-sha256)", + "users":[ + { + "username":"user1", + "firstName" : "User", + "lastName" : "1", + "email" : "user1@feedhenry.org", + "enabled":true, + "credentials":[ + { + "type":"password", + "value":"Password123!" + } + ], + "clientRoles":{ + "account":[ + "view-profile", + "manage-account" + ] + }, + "attributes":{ + + }, + "groups" : [ "/Mobile Users" ] + }, + { + "username":"user2", + "firstName" : "User", + "lastName" : "2", + "email" : "user2@feedhenry.org", + "enabled":true, + "credentials":[ + { + "type":"password", + "value":"Password123!" + } + ], + "clientRoles":{ + "account":[ + "view-profile", + "manage-account" + ] + }, + "attributes":{ + + }, + "groups" : [ "/Mobile Users" ] + }, + { + "username":"secure-user", + "enabled" : true, + "emailVerified" : true, + "firstName" : "Secure", + "lastName" : "User", + "email" : "secure-user@feedhenry.org", + "credentials":[ + { + "type":"password", + "value":"Password123!" + } + ], + "requiredActions" : [ "CONFIGURE_TOTP" ], + "realmRoles" : [ ], + "clientRoles":{ + "account":[ + "view-profile", + "manage-account" + ] + }, + "attributes":{ + + }, + "groups" : [ "/Mobile Users", "/API Access" ] + }, + { + "username":"secure-user2", + "enabled" : true, + "emailVerified" : true, + "firstName" : "Secure", + "lastName" : "User 2", + "email" : "secure-user2@feedhenry.org", + "credentials":[ + { + "type":"password", + "value":"Password123!" + } + ], + "requiredActions" : [ "CONFIGURE_TOTP" ], + "realmRoles" : [ ], + "clientRoles":{ + "account":[ + "view-profile", + "manage-account" + ] + }, + "attributes":{ + + }, + "groups" : [ "/Mobile Users", "/API Access" ] + }, + { + "username":"superuser", + "enabled" : true, + "emailVerified" : true, + "firstName" : "Super", + "lastName" : "User", + "email" : "superuser@feedhenry.org", + "credentials":[ + { + "type":"password", + "value":"Password123!" + } + ], + "realmRoles" : [ ], + "clientRoles":{ + "account":[ + "view-profile", + "manage-account" + ] + }, + "attributes":{ + + }, + "groups" : [ "/Mobile Users", "/API Access", "Superuser" ] + } + ], + "clients":[ + { + "clientId":"api-server", + "enabled":true, + "bearerOnly":true, + "adminUrl":"http://localhost:8080" + }, + { + "clientId":"client-app", + "enabled":true, + "publicClient":true, + "redirectUris":[ + "com.feedhenry.securenativeandroidtemplate:/callback" + ], + "webOrigins":[ + "*" + ] + } + ], + "clientScopeMappings":{ + "account":[ + { + "client":"api-server", + "roles":[ + "view-profile" + ] + }, + { + "client":"client-app", + "roles":[ + "view-profile" + ] + } + ] + }, + "groups" : [ { + "name" : "Mobile Users", + "path" : "/Mobile Users", + "attributes" : { }, + "realmRoles" : [ "mobile-user" ], + "clientRoles" : { }, + "subGroups" : [ ] + }, + { + "name" : "API Access", + "path" : "/API Access", + "attributes" : { }, + "realmRoles" : [ "mobile-user", "api-access" ], + "clientRoles" : { }, + "subGroups" : [ ] + }, + { + "name" : "Superuser", + "path" : "/Superusers", + "attributes" : { }, + "realmRoles" : [ "mobile-user", "api-access", "superuser" ], + "clientRoles" : { }, + "subGroups" : [ ] + } ], + "roles":{ + "client":{ + "api-server":[ + + ], + "client-app":[ + + ] + } + }, + "authenticatorConfig" : [ { + "alias" : "authenticator config", + "config" : { + "defaultProvider" : "" + } + }, { + "alias" : "create unique user config", + "config" : { + "require.password.update.after.registration" : "false" + } + }, { + "alias" : "review profile config", + "config" : { + "update.profile.on.first.login" : "missing" + } + }, { + "alias" : "x509 browser auth config", + "config" : { + "x509-cert-auth.extendedkeyusage" : "", + "x509-cert-auth.mapper-selection.user-attribute-name" : "usercertificate", + "x509-cert-auth.ocsp-responder-uri" : "", + "x509-cert-auth.regular-expression" : "(.*?)(?:$)", + "x509-cert-auth.crl-checking-enabled" : "", + "x509-cert-auth.confirmation-page-disallowed" : "", + "x509-cert-auth.keyusage" : "", + "x509-cert-auth.mapper-selection" : "Username or Email", + "x509-cert-auth.crl-relative-path" : "crl.pem", + "x509-cert-auth.crldp-checking-enabled" : "false", + "x509-cert-auth.mapping-source-selection" : "Subject's Common Name", + "x509-cert-auth.ocsp-checking-enabled" : "" + } + }, { + "alias" : "x509 direct grant config", + "config" : { + "x509-cert-auth.extendedkeyusage" : "", + "x509-cert-auth.mapper-selection.user-attribute-name" : "usercertificate", + "x509-cert-auth.ocsp-responder-uri" : "", + "x509-cert-auth.regular-expression" : "(.*?)(?:$)", + "x509-cert-auth.crl-checking-enabled" : "", + "x509-cert-auth.confirmation-page-disallowed" : "", + "x509-cert-auth.keyusage" : "", + "x509-cert-auth.mapper-selection" : "Username or Email", + "x509-cert-auth.crl-relative-path" : "crl.pem", + "x509-cert-auth.crldp-checking-enabled" : "false", + "x509-cert-auth.mapping-source-selection" : "Subject's Common Name", + "x509-cert-auth.ocsp-checking-enabled" : "" + } + } ] +} diff --git a/testsuite/admin-client-tests/src/test/resources/import/testrealm-user-null-attr.json b/testsuite/admin-client-tests/src/test/resources/import/testrealm-user-null-attr.json new file mode 100644 index 0000000..ba1a0ba --- /dev/null +++ b/testsuite/admin-client-tests/src/test/resources/import/testrealm-user-null-attr.json @@ -0,0 +1,1617 @@ +{ + "id" : "test-user-null-attr", + "realm" : "test-user-null-attr", + "notBefore" : 0, + "revokeRefreshToken" : false, + "refreshTokenMaxReuse" : 0, + "accessTokenLifespan" : 300, + "accessTokenLifespanForImplicitFlow" : 900, + "ssoSessionIdleTimeout" : 1800, + "ssoSessionMaxLifespan" : 36000, + "ssoSessionIdleTimeoutRememberMe" : 0, + "ssoSessionMaxLifespanRememberMe" : 0, + "offlineSessionIdleTimeout" : 2592000, + "offlineSessionMaxLifespanEnabled" : false, + "offlineSessionMaxLifespan" : 5184000, + "accessCodeLifespan" : 60, + "accessCodeLifespanUserAction" : 300, + "accessCodeLifespanLogin" : 1800, + "actionTokenGeneratedByAdminLifespan" : 43200, + "actionTokenGeneratedByUserLifespan" : 300, + "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" : "e161bb37-bffb-44a8-8d39-c0de5e378cd5", + "name" : "offline_access", + "description" : "${role_offline-access}", + "composite" : false, + "clientRole" : false, + "containerId" : "test-user-null-attr", + "attributes" : { } + }, { + "id" : "6aec0f95-55b4-4fe7-87b1-73c8c1e28556", + "name" : "uma_authorization", + "description" : "${role_uma_authorization}", + "composite" : false, + "clientRole" : false, + "containerId" : "test-user-null-attr", + "attributes" : { } + } ], + "client" : { + "realm-management" : [ { + "id" : "47ad2786-2ed4-4614-bfdb-7cb3d0d6d3af", + "name" : "view-identity-providers", + "description" : "${role_view-identity-providers}", + "composite" : false, + "clientRole" : true, + "containerId" : "759d0356-3c2b-4192-90b2-63b9ebcf46cb", + "attributes" : { } + }, { + "id" : "77637fb4-2159-47d9-bd02-bbea227f8182", + "name" : "view-clients", + "description" : "${role_view-clients}", + "composite" : true, + "composites" : { + "client" : { + "realm-management" : [ "query-clients" ] + } + }, + "clientRole" : true, + "containerId" : "759d0356-3c2b-4192-90b2-63b9ebcf46cb", + "attributes" : { } + }, { + "id" : "fcc75dc3-c08f-4ea9-af33-a94c215bf28e", + "name" : "manage-clients", + "description" : "${role_manage-clients}", + "composite" : false, + "clientRole" : true, + "containerId" : "759d0356-3c2b-4192-90b2-63b9ebcf46cb", + "attributes" : { } + }, { + "id" : "6cb02fc0-d0d6-4b83-bbf3-9fe48844507d", + "name" : "manage-identity-providers", + "description" : "${role_manage-identity-providers}", + "composite" : false, + "clientRole" : true, + "containerId" : "759d0356-3c2b-4192-90b2-63b9ebcf46cb", + "attributes" : { } + }, { + "id" : "d251255e-99ab-4c6d-a53a-f16905ce88b5", + "name" : "create-client", + "description" : "${role_create-client}", + "composite" : false, + "clientRole" : true, + "containerId" : "759d0356-3c2b-4192-90b2-63b9ebcf46cb", + "attributes" : { } + }, { + "id" : "3e08c460-ac98-4b6d-a895-516583a9daaf", + "name" : "manage-users", + "description" : "${role_manage-users}", + "composite" : false, + "clientRole" : true, + "containerId" : "759d0356-3c2b-4192-90b2-63b9ebcf46cb", + "attributes" : { } + }, { + "id" : "7a8b6fe6-7ac6-4683-a964-c58ea38991f0", + "name" : "query-groups", + "description" : "${role_query-groups}", + "composite" : false, + "clientRole" : true, + "containerId" : "759d0356-3c2b-4192-90b2-63b9ebcf46cb", + "attributes" : { } + }, { + "id" : "ad54ac19-edf8-4955-9b92-d667b0b7fe36", + "name" : "view-authorization", + "description" : "${role_view-authorization}", + "composite" : false, + "clientRole" : true, + "containerId" : "759d0356-3c2b-4192-90b2-63b9ebcf46cb", + "attributes" : { } + }, { + "id" : "4ebe00a1-327c-47c6-ab57-dd63f2d2029e", + "name" : "manage-authorization", + "description" : "${role_manage-authorization}", + "composite" : false, + "clientRole" : true, + "containerId" : "759d0356-3c2b-4192-90b2-63b9ebcf46cb", + "attributes" : { } + }, { + "id" : "ee4c2b4e-4322-4d3c-a402-8825fcb84fab", + "name" : "impersonation", + "description" : "${role_impersonation}", + "composite" : false, + "clientRole" : true, + "containerId" : "759d0356-3c2b-4192-90b2-63b9ebcf46cb", + "attributes" : { } + }, { + "id" : "77bc5018-fdda-4cae-b2c5-86e78d957f92", + "name" : "view-realm", + "description" : "${role_view-realm}", + "composite" : false, + "clientRole" : true, + "containerId" : "759d0356-3c2b-4192-90b2-63b9ebcf46cb", + "attributes" : { } + }, { + "id" : "bee6e333-08c1-478f-be83-a2f5d6b1f4d9", + "name" : "realm-admin", + "description" : "${role_realm-admin}", + "composite" : true, + "composites" : { + "client" : { + "realm-management" : [ "view-identity-providers", "view-clients", "manage-clients", "manage-identity-providers", "manage-users", "create-client", "query-groups", "manage-authorization", "view-authorization", "impersonation", "view-realm", "manage-events", "view-events", "view-users", "manage-realm", "query-clients", "query-users", "query-realms" ] + } + }, + "clientRole" : true, + "containerId" : "759d0356-3c2b-4192-90b2-63b9ebcf46cb", + "attributes" : { } + }, { + "id" : "b931dcee-095e-4092-b3da-207efd2af07c", + "name" : "view-users", + "description" : "${role_view-users}", + "composite" : true, + "composites" : { + "client" : { + "realm-management" : [ "query-groups", "query-users" ] + } + }, + "clientRole" : true, + "containerId" : "759d0356-3c2b-4192-90b2-63b9ebcf46cb", + "attributes" : { } + }, { + "id" : "865df2a3-9908-4562-89a2-7abf97bacc58", + "name" : "view-events", + "description" : "${role_view-events}", + "composite" : false, + "clientRole" : true, + "containerId" : "759d0356-3c2b-4192-90b2-63b9ebcf46cb", + "attributes" : { } + }, { + "id" : "24ad1a73-4ac4-47aa-a773-9433c212ab29", + "name" : "manage-events", + "description" : "${role_manage-events}", + "composite" : false, + "clientRole" : true, + "containerId" : "759d0356-3c2b-4192-90b2-63b9ebcf46cb", + "attributes" : { } + }, { + "id" : "414dbb00-16e9-4ed4-a8f1-041370e3e069", + "name" : "manage-realm", + "description" : "${role_manage-realm}", + "composite" : false, + "clientRole" : true, + "containerId" : "759d0356-3c2b-4192-90b2-63b9ebcf46cb", + "attributes" : { } + }, { + "id" : "af0d82f1-f438-4a37-a37a-e866597a2126", + "name" : "query-clients", + "description" : "${role_query-clients}", + "composite" : false, + "clientRole" : true, + "containerId" : "759d0356-3c2b-4192-90b2-63b9ebcf46cb", + "attributes" : { } + }, { + "id" : "a64c5a34-71ad-4f93-aab3-246c93b7c038", + "name" : "query-users", + "description" : "${role_query-users}", + "composite" : false, + "clientRole" : true, + "containerId" : "759d0356-3c2b-4192-90b2-63b9ebcf46cb", + "attributes" : { } + }, { + "id" : "b3a25a3a-63e4-4d2c-8902-8cbe1de3e30e", + "name" : "query-realms", + "description" : "${role_query-realms}", + "composite" : false, + "clientRole" : true, + "containerId" : "759d0356-3c2b-4192-90b2-63b9ebcf46cb", + "attributes" : { } + } ], + "security-admin-console" : [ ], + "admin-cli" : [ ], + "account-console" : [ ], + "broker" : [ { + "id" : "17611769-2d4d-4b67-8158-afc00a300b3c", + "name" : "read-token", + "description" : "${role_read-token}", + "composite" : false, + "clientRole" : true, + "containerId" : "83744367-aeb8-4ec3-8371-fcc847e15d20", + "attributes" : { } + } ], + "account" : [ { + "id" : "8e2532f1-c892-47f9-9125-d122756d850b", + "name" : "manage-account-links", + "description" : "${role_manage-account-links}", + "composite" : false, + "clientRole" : true, + "containerId" : "618d62a1-1d11-48e5-b14b-41de66877629", + "attributes" : { } + }, { + "id" : "8ab03779-c159-4ae0-aa26-2ef93e0d939c", + "name" : "view-profile", + "description" : "${role_view-profile}", + "composite" : false, + "clientRole" : true, + "containerId" : "618d62a1-1d11-48e5-b14b-41de66877629", + "attributes" : { } + }, { + "id" : "a1e8df41-0da8-475e-b96e-165b364979ec", + "name" : "manage-account", + "description" : "${role_manage-account}", + "composite" : true, + "composites" : { + "client" : { + "account" : [ "manage-account-links" ] + } + }, + "clientRole" : true, + "containerId" : "618d62a1-1d11-48e5-b14b-41de66877629", + "attributes" : { } + } ] + } + }, + "groups" : [ ], + "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" : [ ], + "users" : [ { + "id" : "269d52af-f3c3-48f6-841e-75dc7b808831", + "createdTimestamp" : 1578311988958, + "username" : "testuser", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "attributes" : { + "key1" : [ "value1" ], + "key2" : [ "value2" ], + "key3" : [ null ] + }, + "credentials" : [ ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "offline_access", "uma_authorization" ], + "clientRoles" : { + "account" : [ "view-profile", "manage-account" ] + }, + "notBefore" : 0, + "groups" : [ ] + } ], + "scopeMappings" : [ { + "clientScope" : "offline_access", + "roles" : [ "offline_access" ] + } ], + "clientScopeMappings" : { + "account" : [ { + "client" : "account-console", + "roles" : [ "manage-account" ] + } ] + }, + "clients" : [ { + "id" : "618d62a1-1d11-48e5-b14b-41de66877629", + "clientId" : "account", + "name" : "${client_account}", + "rootUrl" : "${authBaseUrl}", + "baseUrl" : "/realms/test-user-null-attr/account/", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "secret" : "8766f356-181b-4365-966e-759b37d19ba0", + "defaultRoles" : [ "view-profile", "manage-account" ], + "redirectUris" : [ "/realms/test-user-null-attr/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", "roles", "profile", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "c49a7747-3f56-4709-a0cc-248ceb67e199", + "clientId" : "account-console", + "name" : "${client_account-console}", + "rootUrl" : "${authBaseUrl}", + "baseUrl" : "/realms/test-user-null-attr/account/", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "secret" : "315f51c2-bbb6-426a-8b74-777cf7423248", + "redirectUris" : [ "/realms/test-user-null-attr/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, + "protocolMappers" : [ { + "id" : "598ed852-1b50-4083-be76-55be502977c5", + "name" : "audience resolve", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-audience-resolve-mapper", + "consentRequired" : false, + "config" : { } + } ], + "defaultClientScopes" : [ "web-origins", "role_list", "roles", "profile", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "be3a4e6b-523c-46fe-a75a-885191aee1cb", + "clientId" : "admin-cli", + "name" : "${client_admin-cli}", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "secret" : "804b52c0-5b35-4066-9617-e97b5d801052", + "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", "roles", "profile", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "83744367-aeb8-4ec3-8371-fcc847e15d20", + "clientId" : "broker", + "name" : "${client_broker}", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "secret" : "c24b44a6-cdfc-43e2-8416-9fb2178dda93", + "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", "roles", "profile", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "759d0356-3c2b-4192-90b2-63b9ebcf46cb", + "clientId" : "realm-management", + "name" : "${client_realm-management}", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "secret" : "f60a8b90-1d0d-4ec1-b95e-c97067d16182", + "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", "roles", "profile", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "03235ba2-ad66-4f53-97f9-0bf8069619aa", + "clientId" : "security-admin-console", + "name" : "${client_security-admin-console}", + "rootUrl" : "${authAdminUrl}", + "baseUrl" : "/admin/test-user-null-attr/console/", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "secret" : "89ce17a6-2fbf-4b94-b09d-0e172a68776d", + "redirectUris" : [ "/admin/test-user-null-attr/console/*" ], + "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, + "protocolMappers" : [ { + "id" : "9e277beb-7ab1-40e1-8537-7efe9206a737", + "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", "roles", "profile", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + } ], + "clientScopes" : [ { + "id" : "1ac2e110-441c-45c4-8e2e-c50d7b2b8994", + "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" : "809e4a60-387c-4417-b50d-7ddb03eb0449", + "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" : "4cecd387-2fe4-4b0b-baf2-f77ec8810099", + "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" : "238514d4-859c-44c8-90ca-a1497473435f", + "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" : "76f40b9a-af6c-46a8-8fc6-0687da2d3553", + "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" : "5e619211-3659-4121-bebc-42ab0078c428", + "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" : "6dd05ee2-ea89-48b4-877e-d5578dcf37a3", + "name" : "groups", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-realm-role-mapper", + "consentRequired" : false, + "config" : { + "multivalued" : "true", + "user.attribute" : "foo", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "groups", + "jsonType.label" : "String" + } + }, { + "id" : "93e6f241-846c-48af-ba78-7c8266c7e558", + "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" : "f436fe9e-485c-4b74-a1db-19108a78de34", + "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" : "5d41409d-0253-4fac-a4c9-7f76a377f44f", + "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" : "1a978dec-da66-4bf1-b773-b009e23e4725", + "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" : "7248b788-835f-489d-a14b-ab4498db7007", + "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" : "fd4aac27-cc9d-49e2-aa48-41695aa8eb40", + "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" : "4a176ee7-dab6-49bd-b644-ee359f7966ff", + "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" : "85ec37c1-8430-4984-ad49-cfb01e0780b5", + "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" : "032520c0-4356-4c4e-bab6-7b10696b2dd6", + "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" : "72a91f75-7a09-4e46-a204-bb11c6093474", + "name" : "profile", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "profile", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "profile", + "jsonType.label" : "String" + } + }, { + "id" : "67dc078b-09fc-405f-8905-70aa32d3532b", + "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" : "a7ffb672-9bc7-408d-9301-a872fbe6d670", + "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" : "d4a1feb7-1727-40f9-a732-676463d54889", + "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" : "2ed6d2e2-0c65-4508-aecb-80b32bb5ced6", + "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" : "4e68813a-84be-4e9e-ba83-dcb8d773e4d4", + "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" + } + }, { + "id" : "755b29e5-1df9-4c5c-8796-788c4a39d164", + "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" : "6a4872d3-b9d2-4932-827f-11c04bae18d3", + "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" : "649cc53c-6ae1-44e1-850a-ecf9afb7eaa3", + "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" : "4bff897e-c5ff-46a4-b010-a81f3fa3152c", + "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" : "00d048d6-8c89-4a29-843a-020edada37fc", + "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" : "4de348f5-b362-448d-809e-edc2a70261fa", + "name" : "role_list", + "description" : "SAML role list", + "protocol" : "saml", + "attributes" : { + "consent.screen.text" : "${samlRoleListScopeConsentText}", + "display.on.consent.screen" : "true" + }, + "protocolMappers" : [ { + "id" : "62cb2960-6885-4a98-b4d0-8987c44e7bec", + "name" : "role list", + "protocol" : "saml", + "protocolMapper" : "saml-role-list-mapper", + "consentRequired" : false, + "config" : { + "single" : "false", + "attribute.nameformat" : "Basic", + "attribute.name" : "Role" + } + } ] + }, { + "id" : "54de406a-cfac-42ae-8d97-f149ad1043b8", + "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" : "7bdd6fb0-1055-4c66-8409-34d6d11b6be4", + "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" : "f6181634-4eb6-436f-ad95-6d287f260380", + "name" : "audience resolve", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-audience-resolve-mapper", + "consentRequired" : false, + "config" : { } + }, { + "id" : "80ca5dfb-d1f9-4c2f-8f1f-4056b9248259", + "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" : "0875318f-0ebd-4e91-9a44-f12a16d6d966", + "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" : "01139d93-185a-4635-a81b-1b6a2e66f1a0", + "name" : "allowed web origins", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-allowed-origins-mapper", + "consentRequired" : false, + "config" : { } + } ] + } ], + "defaultDefaultClientScopes" : [ "role_list", "profile", "email", "roles", "web-origins" ], + "defaultOptionalClientScopes" : [ "offline_access", "address", "phone", "microprofile-jwt" ], + "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, + "components" : { + "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy" : [ { + "id" : "a5172cf0-6b05-41ee-870a-fd25e6c2e498", + "name" : "Max Clients Limit", + "providerId" : "max-clients", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "max-clients" : [ "200" ] + } + }, { + "id" : "5bf6b838-1d41-4370-b61f-c51faab98eb7", + "name" : "Trusted Hosts", + "providerId" : "trusted-hosts", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "host-sending-registration-request-must-match" : [ "true" ], + "client-uris-must-match" : [ "true" ] + } + }, { + "id" : "f0de671e-3389-4533-acc9-ca3e3ff7716d", + "name" : "Allowed Protocol Mapper Types", + "providerId" : "allowed-protocol-mappers", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "allowed-protocol-mapper-types" : [ "oidc-sha256-pairwise-sub-mapper", "saml-user-attribute-mapper", "oidc-address-mapper", "oidc-usermodel-attribute-mapper", "oidc-full-name-mapper", "saml-user-property-mapper", "oidc-usermodel-property-mapper", "saml-role-list-mapper" ] + } + }, { + "id" : "927fb360-0adc-4c91-b480-d003768df18a", + "name" : "Allowed Client Scopes", + "providerId" : "allowed-client-templates", + "subType" : "authenticated", + "subComponents" : { }, + "config" : { + "allow-default-scopes" : [ "true" ] + } + }, { + "id" : "f429d8bf-d042-4b13-aa70-e1b09ef6a3a4", + "name" : "Full Scope Disabled", + "providerId" : "scope", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { } + }, { + "id" : "2e5317ec-a8c7-47f5-aa3c-de12a99c13ff", + "name" : "Allowed Protocol Mapper Types", + "providerId" : "allowed-protocol-mappers", + "subType" : "authenticated", + "subComponents" : { }, + "config" : { + "allowed-protocol-mapper-types" : [ "oidc-usermodel-attribute-mapper", "oidc-sha256-pairwise-sub-mapper", "saml-user-property-mapper", "saml-role-list-mapper", "saml-user-attribute-mapper", "oidc-usermodel-property-mapper", "oidc-full-name-mapper", "oidc-address-mapper" ] + } + }, { + "id" : "9c089afc-4d32-47e7-bf2d-62ce27d08cb7", + "name" : "Consent Required", + "providerId" : "consent-required", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { } + }, { + "id" : "41f6398e-d9db-4756-b376-0ca589e95bdb", + "name" : "Allowed Client Scopes", + "providerId" : "allowed-client-templates", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "allow-default-scopes" : [ "true" ] + } + } ], + "org.keycloak.keys.KeyProvider" : [ { + "id" : "6106a56b-47e9-47d0-a3f6-59952d5e597b", + "name" : "rsa-generated", + "providerId" : "rsa-generated", + "subComponents" : { }, + "config" : { + "privateKey" : [ "MIIEowIBAAKCAQEAhSjJajn/bcNGK49W9tCFVIFGL+MYsAotNGE389X0xnGsWzRI0+4pOF8R+6HlXP3wKOlUdNckCgu7HvzJ/N+mTl6CTVWdnQKQVhRY0NcqccK8iMWkRtL3sqJKbNHZ0iSug6Wccx71AcS8ieEUy7zRB5fdylRR30NUeDhMKlFAVyxMGfWYwISr1Mqx2cq1BPByzyi1+AAkrka9cs58JdcFDzUGxy2yXmxJB5Uuqi5wheFOTVY6mGeZIA1r8ADIrwD3mByYzGVRNI/UB8dN3X7iZgKss2TtDhLKSURwohwRJiXvBZg7VpwVoz4M0oitEHfvb8LTIWCpN5OY2d8+goS19QIDAQABAoIBAHdSExozGmXbA7fo2/6S38bXiHNExkyI8fTr4N2IraxdFBsuAMyXrywqVMztR8BKdLfUTa/dURgHZwffcYg9SKMVISH3RCL7tybLWMLa69fArnzIzeoBBaB2uMZGTS93W0HwVv75kIajKmdK3/2pFo39UesKH8s2ZCzOFcIdyM/TQQHkzQgz9d6xQt9i3XjjZiixSDtWoi6WUixur6LM3rFIdbGke1VzaCFFnzM9maBqtjYLYXDEuiJXsp4Gr5CvwU0doyKyizuZlfUqEPKtlRwpLRQorDRP0P5j0el6TUBYPolW+oxlI/aCy8Iva73GyouGi92rz21MTcYNqX772EECgYEAuNKB2A/4Lk0Hg9SPm0WnsVJlhhtN6qTlVmQE5u9UTGzcqHXt6YPi5COINZmnCbZ3yDsmk0R5iTObIjT0/ydAy7MitlBuIrYjU8PKiABRcbqmk9xjXSPUsOFxR+9I1rfQIQKMRnPFv0YMMOGwGIOTyFfPa7dL9Uw+9cuVCKACvs0CgYEAuHDdm68kCv49xZi/jmssaFUAALxzmqhwEomrmYxZr4gLmSGyrBDMEOKzTTGjW5/ViK7raqEa3wIn97waoLjP+CfDnHk88m6JBWJn7lK1bDuYvLk70Tq4+LFp7QhRPdrClMA7Byvhv43A1PhxgJauAnu/BrDeDzkKFKxU76eig8kCgYEAt0BhZa1P0fimPtv/F2FVB8g+yV1BQCoHCkVZZvBdkPlPP+jN3/7YdIOWhi63JDY7RdkAQnxeVN9KLfx7/pEY+d+/xyywRtJ47JDwuzA1kKIUj/6wtqTUOh0NiNmESwEt58zy8NfRdfkqFT1wsJ2lZbtK+e5f7fOPaX5VYpvknPECgYAqroO6CVev65Hj7is2C/sk0bbEdNfTzHLS92TsjZwbkMIOV8v/IYv3xF512KzTATPrA3+bF1kejmMtYyxOUTZfWORdi3jdKVMwGcuvTRiKyWfZFIyRKKOxeWzn22rhg4RP5ARE7pS5PVaIck3h0fzGulhEdh2NLEf27MJjC0oCcQKBgDOPxaI45A+aQR4NskZvlnuT86+f8JOI950xsO8ZmFmqFngy518a6TnykLl1WA4E8TS+inlCFEiduj1btsccvCBp9qNVRX6I93mbemQFlbg6oQQdj7GrVJVl/uZLY7WaZsGVfy1xpGXKRQHBstWRfKvO6vD88pz+co5UK9Ab9K+e" ], + "certificate" : [ "MIICtTCCAZ0CBgFverl9mDANBgkqhkiG9w0BAQsFADAeMRwwGgYDVQQDDBN0ZXN0LXVzZXItbnVsbC1hdHRyMB4XDTIwMDEwNjExNTc1NFoXDTMwMDEwNjExNTkzNFowHjEcMBoGA1UEAwwTdGVzdC11c2VyLW51bGwtYXR0cjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAIUoyWo5/23DRiuPVvbQhVSBRi/jGLAKLTRhN/PV9MZxrFs0SNPuKThfEfuh5Vz98CjpVHTXJAoLux78yfzfpk5egk1VnZ0CkFYUWNDXKnHCvIjFpEbS97KiSmzR2dIkroOlnHMe9QHEvInhFMu80QeX3cpUUd9DVHg4TCpRQFcsTBn1mMCEq9TKsdnKtQTwcs8otfgAJK5GvXLOfCXXBQ81Bsctsl5sSQeVLqoucIXhTk1WOphnmSANa/AAyK8A95gcmMxlUTSP1AfHTd1+4mYCrLNk7Q4SyklEcKIcESYl7wWYO1acFaM+DNKIrRB372/C0yFgqTeTmNnfPoKEtfUCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAFrTjRzrYbbkC+G7ASxkmHmvM2mdD92bUS1jwTvdJMrF0tFcZTtRD//Wbg+6GvRtkgimI5EM0Q4LflL4BfGKn9bA6wgXOlnXWaQzRnpmfcoa7/BHvOgvZITcuHUNrbgQhuBtNh/jaDrRr8u4fMwqwvGPe5JcesfZkC9siztl7RO8IAHCgai0NUCKcr9/eFNffGSlU23dLrBME7ZVpMBZxPvVH4zMBzK0FLOCRJAG5yInLxzoOBAhgKmsfnLnwSKOS6AudnsIjmulyO/rLEt8FOtKoKx7GQmtzoJX8qqt2gElQojlff3/LJ+oOgYnrdA2pDVmHKSWZ6pf/pEy6S7TiZg==" ], + "priority" : [ "100" ] + } + }, { + "id" : "2b06c73a-fe38-413d-9fee-626980685c3d", + "name" : "aes-generated", + "providerId" : "aes-generated", + "subComponents" : { }, + "config" : { + "kid" : [ "694a4045-7b63-402b-8a9f-84a0d8c0b6b5" ], + "secret" : [ "p_QQsM00mP7tJCEW7rTCew" ], + "priority" : [ "100" ] + } + }, { + "id" : "9010959e-17bf-4bc9-9a16-9e6a7c230e4b", + "name" : "hmac-generated", + "providerId" : "hmac-generated", + "subComponents" : { }, + "config" : { + "kid" : [ "9d547d80-029d-44c5-a60b-cf3b4b311375" ], + "secret" : [ "4JFv7rRBmdmBofkkILKx69GpWvckjkya2KBf_V8FfuhuvO_BEXSCRHp9n76bFyexJ6DmK6pKdmgZX__oIHEJ7w" ], + "priority" : [ "100" ], + "algorithm" : [ "HS256" ] + } + } ] + }, + "internationalizationEnabled" : false, + "supportedLocales" : [ ], + "authenticationFlows" : [ { + "id" : "4924886a-22ef-4b6d-992e-6d269cb57081", + "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" : "d3ea3a35-074b-4827-b3b5-e064ce740df6", + "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" : "1dc9bf38-729b-4ee7-9a44-5d9f4cd73f35", + "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" : "fac6529b-4550-45ec-9094-099b6e99a64e", + "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" : "76ef3b04-bb13-4271-922e-cda0762c0d3b", + "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" : "77bfe71a-4f2f-4acd-ad61-e6ca0df64d89", + "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" : "18c36a31-72fc-461f-b0c8-524e5167de17", + "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" : "5c613aa7-a5e6-42d6-8ce8-6fd122a15e5e", + "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" : "87d8d3b1-016d-49e2-831b-d311a7040bcb", + "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" : "782f6021-4cc7-4704-8a42-91fcbfdf5508", + "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" : "f21c1bce-dcb7-4049-bf7d-c9ab144d917a", + "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 + } ] + }, { + "id" : "c2ca35fa-6c53-41ce-8000-346300a0b381", + "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 + } ] + }, { + "id" : "cca40f39-c6a8-4c98-b8c7-5b416f05c093", + "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 + } ] + }, { + "id" : "10f6ff01-24c3-4aff-9040-fa4e12f79ae9", + "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 + } ] + }, { + "id" : "0f6bbf42-7926-4a7c-8167-46331f3cfde4", + "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 + } ] + }, { + "id" : "0dd2f1c3-a3ce-4380-8ef0-4dc7bdb4297e", + "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 + } ] + }, { + "id" : "5645a178-2e60-42ba-8e55-eb52d0577fa3", + "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 + } ] + }, { + "id" : "9c8992a2-422a-4624-bb3b-86af4cc99915", + "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 + } ] + }, { + "id" : "d6844f7e-5593-4af4-896a-db5784ca3643", + "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 + } ] + }, { + "id" : "b278f188-d1d5-406f-bb45-0cef0f00c459", + "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" : "7ab6e1d9-dda7-4d5b-866b-bb638c965b9b", + "alias" : "create unique user config", + "config" : { + "require.password.update.after.registration" : "false" + } + }, { + "id" : "324810cf-c770-4e41-9338-cfae24fff093", + "alias" : "review profile config", + "config" : { + "update.profile.on.first.login" : "missing" + } + } ], + "requiredActions" : [ { + "alias" : "CONFIGURE_TOTP", + "name" : "Configure OTP", + "providerId" : "CONFIGURE_TOTP", + "enabled" : true, + "defaultAction" : false, + "priority" : 10, + "config" : { } + }, { + "alias" : "terms_and_conditions", + "name" : "Terms and Conditions", + "providerId" : "terms_and_conditions", + "enabled" : false, + "defaultAction" : false, + "priority" : 20, + "config" : { } + }, { + "alias" : "UPDATE_PASSWORD", + "name" : "Update Password", + "providerId" : "UPDATE_PASSWORD", + "enabled" : true, + "defaultAction" : false, + "priority" : 30, + "config" : { } + }, { + "alias" : "UPDATE_PROFILE", + "name" : "Update Profile", + "providerId" : "UPDATE_PROFILE", + "enabled" : true, + "defaultAction" : false, + "priority" : 40, + "config" : { } + }, { + "alias" : "VERIFY_EMAIL", + "name" : "Verify Email", + "providerId" : "VERIFY_EMAIL", + "enabled" : true, + "defaultAction" : false, + "priority" : 50, + "config" : { } + } ], + "browserFlow" : "browser", + "registrationFlow" : "registration", + "directGrantFlow" : "direct grant", + "resetCredentialsFlow" : "reset credentials", + "clientAuthenticationFlow" : "clients", + "dockerAuthenticationFlow" : "docker auth", + "attributes" : { + "webAuthnPolicyAuthenticatorAttachment" : "not specified", + "_browser_header.xRobotsTag" : "none", + "webAuthnPolicyRpEntityName" : "keycloak", + "failureFactor" : "30", + "actionTokenGeneratedByUserLifespan" : "300", + "maxDeltaTimeSeconds" : "43200", + "webAuthnPolicySignatureAlgorithms" : "ES256", + "offlineSessionMaxLifespan" : "5184000", + "_browser_header.contentSecurityPolicyReportOnly" : "", + "bruteForceProtected" : "false", + "_browser_header.contentSecurityPolicy" : "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", + "_browser_header.xXSSProtection" : "1; mode=block", + "_browser_header.xFrameOptions" : "SAMEORIGIN", + "_browser_header.strictTransportSecurity" : "max-age=31536000; includeSubDomains", + "webAuthnPolicyUserVerificationRequirement" : "not specified", + "permanentLockout" : "false", + "quickLoginCheckMilliSeconds" : "1000", + "webAuthnPolicyCreateTimeout" : "0", + "webAuthnPolicyRequireResidentKey" : "not specified", + "webAuthnPolicyRpId" : "", + "webAuthnPolicyAttestationConveyancePreference" : "not specified", + "maxFailureWaitSeconds" : "900", + "minimumQuickLoginWaitSeconds" : "60", + "webAuthnPolicyAvoidSameAuthenticatorRegister" : "false", + "_browser_header.xContentTypeOptions" : "nosniff", + "actionTokenGeneratedByAdminLifespan" : "43200", + "waitIncrementSeconds" : "60", + "offlineSessionMaxLifespanEnabled" : "false" + }, + "keycloakVersion" : "9.0.0-SNAPSHOT", + "userManagedAccessAllowed" : false +} diff --git a/testsuite/admin-client-tests/src/test/resources/testrealm.json b/testsuite/admin-client-tests/src/test/resources/testrealm.json new file mode 100644 index 0000000..fcbc806 --- /dev/null +++ b/testsuite/admin-client-tests/src/test/resources/testrealm.json @@ -0,0 +1,694 @@ +{ + "id": "test", + "realm": "test", + "enabled": true, + "sslRequired": "external", + "registrationAllowed": true, + "resetPasswordAllowed": true, + "editUsernameAllowed" : true, + "ssoSessionIdleTimeout": 1800, + "ssoSessionMaxLifespan": 36000, + "offlineSessionIdleTimeout": 2592000, + "offlineSessionMaxLifespan": 5184000, + "requiredCredentials": [ "password" ], + "defaultRoles": [ "user" ], + "smtpServer": { + "from": "auto@keycloak.org", + "host": "localhost", + "port":"3025", + "fromDisplayName": "Keycloak SSO", + "replyTo":"reply-to@keycloak.org", + "replyToDisplayName": "Keycloak no-reply", + "envelopeFrom": "auto+bounces@keycloak.org" + }, + "users" : [ + { + "username" : "test-user@localhost", + "enabled": true, + "email" : "test-user@localhost", + "firstName": "Tom", + "lastName": "Brady", + "credentials" : [ + { "type" : "password", + "value" : "password" } + ], + "realmRoles": ["user", "offline_access"], + "clientRoles": { + "test-app": [ "customer-user" ], + "account": [ "view-profile", "manage-account" ] + } + }, + { + "username" : "john-doh@localhost", + "enabled": true, + "email" : "john-doh@localhost", + "firstName": "John", + "lastName": "Doh", + "credentials" : [ + { "type" : "password", + "value" : "password" } + ], + "realmRoles": ["user"], + "clientRoles": { + "test-app": [ "customer-user" ], + "account": [ "view-profile", "manage-account" ] + } + }, + { + "username" : "keycloak-user@localhost", + "enabled": true, + "email" : "keycloak-user@localhost", + "credentials" : [ + { "type" : "password", + "value" : "password" } + ], + "realmRoles": ["user"], + "clientRoles": { + "test-app": [ "customer-user" ], + "account": [ "view-profile", "manage-account" ] + } + }, + { + "username" : "topGroupUser", + "enabled": true, + "email" : "top@redhat.com", + "credentials" : [ + { "type" : "password", + "value" : "password" } + ], + "groups": [ + "/topGroup" + ] + }, + { + "username" : "level2GroupUser", + "enabled": true, + "email" : "level2@redhat.com", + "credentials" : [ + { "type" : "password", + "value" : "password" } + ], + "groups": [ + "/topGroup/level2group" + ] + }, + { + "username" : "roleRichUser", + "enabled": true, + "email" : "rich.roles@redhat.com", + "credentials" : [ + { "type" : "password", + "value" : "password" } + ], + "groups": [ + "/roleRichGroup/level2group" + ], + "clientRoles": { + "test-app-scope": [ "test-app-allowed-by-scope", "test-app-disallowed-by-scope" ] + } + }, + { + "username" : "non-duplicate-email-user", + "enabled": true, + "email" : "non-duplicate-email-user@localhost", + "firstName": "Brian", + "lastName": "Cohen", + "credentials" : [ + { "type" : "password", + "value" : "password" } + ], + "realmRoles": ["user", "offline_access"], + "clientRoles": { + "test-app": [ "customer-user" ], + "account": [ "view-profile", "manage-account" ] + } + }, + { + "username" : "user-with-one-configured-otp", + "enabled": true, + "email" : "otp1@redhat.com", + "credentials" : [ + { + "type" : "password", + "value" : "password" + }, + { + "id" : "unique", + "type" : "otp", + "secretData" : "{\"value\":\"DJmQfC73VGFhw7D4QJ8A\"}", + "credentialData" : "{\"digits\":6,\"counter\":0,\"period\":30,\"algorithm\":\"HmacSHA1\",\"subType\":\"totp\"}" + } + ] + }, + { + "username" : "user-with-two-configured-otp", + "enabled": true, + "email" : "otp2@redhat.com", + "realmRoles": ["user"], + "credentials" : [ + { + "id" : "first", + "userLabel" : "first", + "type" : "otp", + "secretData" : "{\"value\":\"DJmQfC73VGFhw7D4QJ8A\"}", + "credentialData" : "{\"digits\":6,\"counter\":0,\"period\":30,\"algorithm\":\"HmacSHA1\",\"subType\":\"totp\"}" + }, + { + "type" : "password", + "value" : "password" + }, + { + "id" : "second", + "type" : "otp", + "secretData" : "{\"value\":\"ABCQfC73VGFhw7D4QJ8A\"}", + "credentialData" : "{\"digits\":6,\"counter\":0,\"period\":30,\"algorithm\":\"HmacSHA1\",\"subType\":\"totp\"}" + } + ] + }, + { + "username" : "special>>character", + "enabled": true, + "email" : "special-character@localhost", + "firstName": "Special", + "lastName": "Character", + "credentials" : [ + { "type" : "password", + "value" : "" } + ], + "realmRoles": ["user", "offline_access"] + } + ], + "scopeMappings": [ + { + "client": "third-party", + "roles": ["user"] + }, + { + "client": "test-app", + "roles": ["user"] + }, + { + "client": "test-app-scope", + "roles": ["user", "admin"] + } + ], + "clients": [ + { + "clientId": "test-app", + "enabled": true, + "baseUrl": "http://localhost:8180/auth/realms/master/app/auth", + "redirectUris": [ + "http://localhost:8180/auth/realms/master/app/auth/*", + "https://localhost:8543/auth/realms/master/app/auth/*", + "http://localhost:8180/auth/realms/test/app/auth/*", + "https://localhost:8543/auth/realms/test/app/auth/*" + ], + "adminUrl": "http://localhost:8180/auth/realms/master/app/admin", + "secret": "password" + }, + { + "clientId": "root-url-client", + "enabled": true, + "rootUrl": "http://localhost:8180/foo/bar", + "adminUrl": "http://localhost:8180/foo/bar", + "baseUrl": "/baz", + "redirectUris": [ + "http://localhost:8180/foo/bar/*", + "https://localhost:8543/foo/bar/*" + ], + "directAccessGrantsEnabled": true, + "secret": "password" + }, + { + "clientId" : "test-app-scope", + "enabled": true, + + "redirectUris": [ + "http://localhost:8180/auth/realms/master/app/*", + "https://localhost:8543/auth/realms/master/app/*" + ], + "secret": "password", + "fullScopeAllowed": "false" + }, + { + "clientId" : "third-party", + "description" : "A third party application", + "enabled": true, + "consentRequired": true, + + "baseUrl": "http://localhost:8180/auth/realms/master/app/auth", + "redirectUris": [ + "http://localhost:8180/auth/realms/master/app/*", + "https://localhost:8543/auth/realms/master/app/*" + ], + "secret": "password" + }, + { + "clientId": "test-app-authz", + "enabled": true, + "baseUrl": "/test-app-authz", + "adminUrl": "/test-app-authz", + "bearerOnly": false, + "authorizationSettings": { + "allowRemoteResourceManagement": true, + "policyEnforcementMode": "ENFORCING", + "resources": [ + { + "name": "Admin Resource", + "uri": "/protected/admin/*", + "type": "http://test-app-authz/protected/admin", + "scopes": [ + { + "name": "admin-access" + } + ] + }, + { + "name": "Protected Resource", + "uri": "/*", + "type": "http://test-app-authz/protected/resource", + "scopes": [ + { + "name": "resource-access" + } + ] + }, + { + "name": "Premium Resource", + "uri": "/protected/premium/*", + "type": "urn:test-app-authz:protected:resource", + "scopes": [ + { + "name": "premium-access" + } + ] + }, + { + "name": "Main Page", + "type": "urn:test-app-authz:protected:resource", + "scopes": [ + { + "name": "urn:test-app-authz:page:main:actionForAdmin" + }, + { + "name": "urn:test-app-authz:page:main:actionForUser" + }, + { + "name": "urn:test-app-authz:page:main:actionForPremiumUser" + } + ] + } + ], + "policies": [ + { + "name": "Any Admin Policy", + "description": "Defines that adminsitrators can do something", + "type": "role", + "config": { + "roles": "[{\"id\":\"admin\"}]" + } + }, + { + "name": "Any User Policy", + "description": "Defines that any user can do something", + "type": "role", + "config": { + "roles": "[{\"id\":\"user\"}]" + } + }, + { + "name": "Only Premium User Policy", + "description": "Defines that only premium users can do something", + "type": "role", + "logic": "POSITIVE", + "config": { + "roles": "[{\"id\":\"customer-user-premium\"}]" + } + }, + { + "name": "All Users Policy", + "description": "Defines that all users can do something", + "type": "aggregate", + "decisionStrategy": "AFFIRMATIVE", + "config": { + "applyPolicies": "[\"Any User Policy\",\"Any Admin Policy\",\"Only Premium User Policy\"]" + } + }, + { + "name": "Premium Resource Permission", + "description": "A policy that defines access to premium resources", + "type": "resource", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"Premium Resource\"]", + "applyPolicies": "[\"Only Premium User Policy\"]" + } + }, + { + "name": "Administrative Resource Permission", + "description": "A policy that defines access to administrative resources", + "type": "resource", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"Admin Resource\"]", + "applyPolicies": "[\"Any Admin Policy\"]" + } + }, + { + "name": "Protected Resource Permission", + "description": "A policy that defines access to any protected resource", + "type": "resource", + "decisionStrategy": "AFFIRMATIVE", + "config": { + "resources": "[\"Protected Resource\"]", + "applyPolicies": "[\"All Users Policy\"]" + } + }, + { + "name": "Action 1 on Main Page Resource Permission", + "description": "A policy that defines access to action 1 on the main page", + "type": "scope", + "decisionStrategy": "AFFIRMATIVE", + "config": { + "scopes": "[\"urn:test-app-authz:page:main:actionForAdmin\"]", + "applyPolicies": "[\"Any Admin Policy\"]" + } + }, + { + "name": "Action 2 on Main Page Resource Permission", + "description": "A policy that defines access to action 2 on the main page", + "type": "scope", + "decisionStrategy": "AFFIRMATIVE", + "config": { + "scopes": "[\"urn:test-app-authz:page:main:actionForUser\"]", + "applyPolicies": "[\"Any User Policy\"]" + } + }, + { + "name": "Action 3 on Main Page Resource Permission", + "description": "A policy that defines access to action 3 on the main page", + "type": "scope", + "decisionStrategy": "AFFIRMATIVE", + "config": { + "scopes": "[\"urn:test-app-authz:page:main:actionForPremiumUser\"]", + "applyPolicies": "[\"Only Premium User Policy\"]" + } + } + ] + }, + "redirectUris": [ + "/test-app-authz/*" + ], + "secret": "secret" + }, + { + "clientId": "named-test-app", + "name": "My Named Test App", + "enabled": true, + "baseUrl": "http://localhost:8180/namedapp/base", + "redirectUris": [ + "http://localhost:8180/namedapp/base/*", + "https://localhost:8543/namedapp/base/*" + ], + "adminUrl": "http://localhost:8180/namedapp/base/admin", + "secret": "password" + }, + { + "clientId": "var-named-test-app", + "name": "Test App Named - ${client_account}", + "enabled": true, + "baseUrl": "http://localhost:8180/varnamedapp/base", + "redirectUris": [ + "http://localhost:8180/varnamedapp/base/*", + "https://localhost:8543/varnamedapp/base/*" + ], + "adminUrl": "http://localhost:8180/varnamedapp/base/admin", + "secret": "password" + }, + { + "clientId": "direct-grant", + "enabled": true, + "directAccessGrantsEnabled": true, + "secret": "password", + "webOrigins": [ "http://localtest.me:8180" ], + "protocolMappers": [ + { + "name": "aud-account", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-mapper", + "config": { + "included.client.audience": "account", + "id.token.claim": "true", + "access.token.claim": "true" + } + }, + { + "name": "aud-admin", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-mapper", + "config": { + "included.client.audience": "security-admin-console", + "id.token.claim": "true", + "access.token.claim": "true" + } + } + ] + }, + + { + "clientId": "custom-audience", + "enabled": true, + "directAccessGrantsEnabled": true, + "secret": "password", + "protocolMappers": [ + { + "name": "aud", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-mapper", + "config": { + "id.token.claim": "true", + "access.token.claim": "true", + "included.custom.audience": "foo-bar" + } + }, + { + "name": "client roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-client-role-mapper", + "config": { + "user.attribute": "foo", + "access.token.claim": "true", + "claim.name": "resource_access.${client_id}.roles", + "jsonType.label": "String", + "multivalued": "true" + } + }, + { + "name": "realm roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "config": { + "user.attribute": "foo", + "access.token.claim": "true", + "claim.name": "realm_access.roles", + "jsonType.label": "String", + "multivalued": "true" + } + } + ], + "defaultClientScopes": [ + "web-origins", + "profile", + "email" + ] + } + ], + "roles" : { + "realm" : [ + { + "name": "user", + "description": "Have User privileges" + }, + { + "name": "admin", + "description": "Have Administrator privileges" + }, + { + "name": "customer-user-premium", + "description": "Have User Premium privileges" + }, + { + "name": "sample-realm-role", + "description": "Sample realm role" + }, + { + "name": "attribute-role", + "description": "has attributes assigned", + "attributes": { + "hello": [ + "world", + "keycloak" + ] + } + }, + { + "name": "realm-composite-role", + "description": "Realm composite role containing client role", + "composite" : true, + "composites" : { + "realm" : [ "sample-realm-role" ], + "client" : { + "test-app" : [ "sample-client-role" ], + "account" : [ "view-profile" ] + } + } + } + ], + "client" : { + "test-app" : [ + { + "name": "manage-account", + "description": "Allows application-initiated actions." + }, + { + "name": "customer-user", + "description": "Have Customer User privileges" + }, + { + "name": "customer-admin", + "description": "Have Customer Admin privileges" + }, + { + "name": "sample-client-role", + "description": "Sample client role", + "attributes": { + "sample-client-role-attribute": [ + "sample-client-role-attribute-value" + ] + } + }, + { + "name": "customer-admin-composite-role", + "description": "Have Customer Admin privileges via composite role", + "composite" : true, + "composites" : { + "realm" : [ "customer-user-premium" ], + "client" : { + "test-app" : [ "customer-admin" ] + } + } + } + ], + "test-app-scope" : [ + { + "name": "test-app-allowed-by-scope", + "description": "Role allowed by scope in test-app-scope" + }, + { + "name": "test-app-disallowed-by-scope", + "description": "Role disallowed by scope in test-app-scope" + } + ] + } + + }, + "groups" : [ + { + "name": "topGroup", + "attributes": { + "topAttribute": ["true"] + + }, + "realmRoles": ["user"], + + "subGroups": [ + { + "name": "level2group", + "realmRoles": ["admin"], + "clientRoles": { + "test-app": ["customer-user"] + }, + "attributes": { + "level2Attribute": ["true"] + + } + }, + { + "name": "level2group2", + "realmRoles": ["admin"], + "clientRoles": { + "test-app": ["customer-user"] + }, + "attributes": { + "level2Attribute": ["true"] + + } + } + ] + }, + { + "name": "roleRichGroup", + "attributes": { + "topAttribute": ["true"] + + }, + "realmRoles": ["user", "realm-composite-role"], + "clientRoles": { + "account": ["manage-account"] + }, + + "subGroups": [ + { + "name": "level2group", + "realmRoles": ["admin"], + "clientRoles": { + "test-app": ["customer-user", "customer-admin-composite-role"] + }, + "attributes": { + "level2Attribute": ["true"] + + } + }, + { + "name": "level2group2", + "realmRoles": ["admin"], + "clientRoles": { + "test-app": ["customer-user"] + }, + "attributes": { + "level2Attribute": ["true"] + + } + } + ] + }, + { + "name": "sample-realm-group" + } + ], + + + "clientScopeMappings": { + "test-app": [ + { + "client": "third-party", + "roles": ["customer-user"] + }, + { + "client": "test-app-scope", + "roles": ["customer-admin-composite-role"] + } + ], + "test-app-scope": [ + { + "client": "test-app-scope", + "roles": ["test-app-allowed-by-scope"] + } + ] + }, + + "internationalizationEnabled": true, + "supportedLocales": ["en", "de"], + "defaultLocale": "en", + "eventsListeners": ["jboss-logging", "event-queue"] +} diff --git a/testsuite/framework/src/main/java/org/keycloak/client/testsuite/common/Creator.java b/testsuite/framework/src/main/java/org/keycloak/client/testsuite/common/Creator.java new file mode 100644 index 0000000..b53d463 --- /dev/null +++ b/testsuite/framework/src/main/java/org/keycloak/client/testsuite/common/Creator.java @@ -0,0 +1,192 @@ +/* + * Copyright 2018 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * 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. + */ +package org.keycloak.client.testsuite.common; + +import jakarta.ws.rs.core.Response; +import org.jboss.logging.Logger; +import org.keycloak.admin.client.Keycloak; +import org.keycloak.admin.client.resource.AuthenticationManagementResource; +import org.keycloak.admin.client.resource.ClientResource; +import org.keycloak.admin.client.resource.ClientsResource; +import org.keycloak.admin.client.resource.ComponentResource; +import org.keycloak.admin.client.resource.ComponentsResource; +import org.keycloak.admin.client.resource.GroupResource; +import org.keycloak.admin.client.resource.GroupsResource; +import org.keycloak.admin.client.resource.IdentityProviderResource; +import org.keycloak.admin.client.resource.IdentityProvidersResource; +import org.keycloak.admin.client.resource.RealmResource; +import org.keycloak.admin.client.resource.UserResource; +import org.keycloak.admin.client.resource.UsersResource; +import org.keycloak.representations.idm.AuthenticationExecutionInfoRepresentation; +import org.keycloak.representations.idm.AuthenticationFlowRepresentation; +import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.ComponentRepresentation; +import org.keycloak.representations.idm.GroupRepresentation; +import org.keycloak.representations.idm.IdentityProviderMapperRepresentation; +import org.keycloak.representations.idm.IdentityProviderRepresentation; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.representations.idm.UserRepresentation; +import org.testcontainers.shaded.org.hamcrest.Matchers; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.keycloak.testsuite.util.ApiUtil.getCreatedId; +import static org.testcontainers.shaded.org.hamcrest.MatcherAssert.assertThat; + +/** + * Creates a temporary realm object and makes sure it is removed when used within try-with-resources. + */ +public class Creator implements AutoCloseable { + + private final static Logger LOG = Logger.getLogger(Creator.class); + + public static Creator create(Keycloak adminClient, RealmRepresentation rep) { + adminClient.realms().create(rep); + final RealmResource r = adminClient.realm(rep.getRealm()); + LOG.debugf("Created realm %s", rep.getRealm()); + return new Creator(rep.getRealm(), r, r::remove); + } + + public static Creator create(RealmResource realmResource, GroupRepresentation rep) { + final GroupsResource groups = realmResource.groups(); + try (Response response = groups.add(rep)) { + String createdId = getCreatedId(response); + final GroupResource r = groups.group(createdId); + LOG.debugf("Created group ID %s", createdId); + return new Creator(createdId, r, r::remove); + } + } + + public static Creator create(RealmResource realmResource, ClientRepresentation rep) { + final ClientsResource clients = realmResource.clients(); + try (Response response = clients.create(rep)) { + String createdId = getCreatedId(response); + final ClientResource r = clients.get(createdId); + LOG.debugf("Created client ID %s", createdId); + return new Creator(createdId, r, r::remove); + } + } + + public static Creator create(RealmResource realmResource, UserRepresentation rep) { + final UsersResource users = realmResource.users(); + try (Response response = users.create(rep)) { + String createdId = getCreatedId(response); + final UserResource r = users.get(createdId); + LOG.debugf("Created user ID %s", createdId); + return new Creator(createdId, r, r::remove); + } + } + + public static Creator create(RealmResource realmResource, ComponentRepresentation rep) { + final ComponentsResource components = realmResource.components(); + try (Response response = components.add(rep)) { + String createdId = getCreatedId(response); + final ComponentResource r = components.component(createdId); + LOG.debugf("Created component ID %s", createdId); + return new Creator(createdId, r, r::remove); + } + } + + public static Flow create(RealmResource realmResource, AuthenticationFlowRepresentation rep) { + final AuthenticationManagementResource authMgmgRes = realmResource.flows(); + try (Response response = authMgmgRes.createFlow(rep)) { + String createdId = getCreatedId(response); + LOG.debugf("Created flow ID %s", createdId); + return new Flow(createdId, rep.getAlias(), authMgmgRes, () -> authMgmgRes.deleteFlow(createdId)); + } + } + + public static Creator create(RealmResource realmResource, IdentityProviderRepresentation rep) { + final IdentityProvidersResource res = realmResource.identityProviders(); + assertThat("Identity provider alias must be specified", rep.getAlias(), Matchers.notNullValue()); + try (Response response = res.create(rep)) { + String createdId = getCreatedId(response); + final IdentityProviderResource r = res.get(rep.getAlias()); + LOG.debugf("Created identity provider ID %s", createdId); + return new Creator(createdId, r, r::remove); + } + } + + public static Creator create(RealmResource realmResource, String identityProviderAlias, IdentityProviderMapperRepresentation rep) { + final IdentityProvidersResource res = realmResource.identityProviders(); + assertThat("Identity provider alias must be specified", identityProviderAlias, Matchers.notNullValue()); + rep.setIdentityProviderAlias(identityProviderAlias); + try (Response response = res.get(identityProviderAlias).addMapper(rep)) { + String createdId = getCreatedId(response); + final IdentityProviderResource r = res.get(identityProviderAlias); + LOG.debugf("Created identity provider mapper ID %s", createdId); + return new Creator(createdId, r, () -> r.delete(createdId)); + } + } + + private final String id; + private final T resource; + private final Runnable closer; + private final AtomicBoolean closerRan = new AtomicBoolean(false); + + private Creator(String id, T resource, Runnable closer) { + this.id = id; + this.resource = resource; + this.closer = closer; + } + + public String id() { + return this.id; + } + + public T resource() { + return this.resource; + } + + @Override + public void close() { + if (this.closerRan.compareAndSet(false, true)) { + LOG.debugf("Removing resource ID %s", id); + try { + closer.run(); + } catch (jakarta.ws.rs.NotFoundException ex) { + LOG.debugf("Resource with ID %s perhaps removed in meantime.", id); + } + } else { + LOG.debugf("Already removed resource ID %s", id); + } + } + + public static class Flow extends Creator { + + private final String alias; + + public Flow(String id, String alias, AuthenticationManagementResource resource, Runnable closer) { + super(id, resource, closer); + this.alias = alias; + } + + public AuthenticationExecutionInfoRepresentation addExecution(String providerId) { + Map c = new HashMap<>(); + c.put("provider", providerId); + resource().addExecution(alias, c); // addExecution only handles "provider" in data + return resource().getExecutions(alias).stream() + .filter(aer -> Objects.equals(providerId, aer.getProviderId())) + .findFirst() + .orElse(null); + } + + } +} diff --git a/testsuite/framework/src/main/java/org/keycloak/client/testsuite/common/OAuthClient.java b/testsuite/framework/src/main/java/org/keycloak/client/testsuite/common/OAuthClient.java index 9c764a4..0deb6c8 100644 --- a/testsuite/framework/src/main/java/org/keycloak/client/testsuite/common/OAuthClient.java +++ b/testsuite/framework/src/main/java/org/keycloak/client/testsuite/common/OAuthClient.java @@ -37,9 +37,9 @@ * @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc. */ public class OAuthClient { - private String realm; private String clientId; + private String redirectUri; private boolean openid = true; private String scope = ""; @@ -59,6 +59,20 @@ public OAuthClient clientId(String clientId) { return this; } + public String getRedirectUri() { + return redirectUri; + } + + public OAuthClient redirectUri(String redirectUri) { + this.redirectUri = redirectUri; + return this; + } + + public OAuthClient scope(String scope) { + this.scope = scope; + return this; + } + public AccessTokenResponse doGrantAccessTokenRequest(String clientSecret, String username, String password) { return doGrantAccessTokenRequest(realm, username, password, null, clientId, clientSecret); } @@ -360,14 +374,6 @@ public LogoutUrlBuilder postLogoutRedirectUri(String redirectUri) { return this; } -// @Deprecated // Use only in backwards compatibility tests -// public LogoutUrlBuilder redirectUri(String redirectUri) { -// if (redirectUri != null) { -// b.queryParam(OAuth2Constants.REDIRECT_URI, redirectUri); -// } -// return this; -// } - public LogoutUrlBuilder state(String state) { if (state != null) { b.queryParam(OAuth2Constants.STATE, state); diff --git a/testsuite/framework/src/main/java/org/keycloak/client/testsuite/framework/KeycloakClientTestExtension.java b/testsuite/framework/src/main/java/org/keycloak/client/testsuite/framework/KeycloakClientTestExtension.java index f034115..dbe8c8f 100644 --- a/testsuite/framework/src/main/java/org/keycloak/client/testsuite/framework/KeycloakClientTestExtension.java +++ b/testsuite/framework/src/main/java/org/keycloak/client/testsuite/framework/KeycloakClientTestExtension.java @@ -1,12 +1,16 @@ package org.keycloak.client.testsuite.framework; import java.lang.reflect.Field; +import java.lang.reflect.Method; import org.jboss.logging.Logger; +import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.extension.AfterAllCallback; import org.junit.jupiter.api.extension.AfterEachCallback; import org.junit.jupiter.api.extension.BeforeEachCallback; import org.junit.jupiter.api.extension.ExtensionContext; +import org.keycloak.client.testsuite.TestConstants; +import org.keycloak.utils.StringUtil; /** * @author Marek Posolda @@ -34,6 +38,7 @@ public void beforeEach(ExtensionContext context) { field.set(testInstance, provider); } } + } testClass = testClass.getSuperclass(); @@ -42,6 +47,20 @@ public void beforeEach(ExtensionContext context) { } catch (Exception e) { throw new RuntimeException(e); } + + Method method = context.getRequiredTestMethod(); + if (method.isAnnotationPresent(KeycloakVersion.class)) { + KeycloakVersion annotation = method.getAnnotation(KeycloakVersion.class); + String currentVersion = System.getProperty(TestConstants.PROPERTY_KEYCLOAK_VERSION, TestConstants.KEYCLOAK_VERSION_DEFAULT); + String requiredMinVersion = annotation.min(); + String requiredMaxVersion = annotation.max(); + if(StringUtil.isNotBlank(requiredMinVersion)) { + Assumptions.assumeTrue(compareVersions(currentVersion, requiredMinVersion) >= 0, "Test skipped because the current version: " + currentVersion + " is lower then required: " + requiredMinVersion); + } + if(StringUtil.isNotBlank(requiredMaxVersion)) { + Assumptions.assumeTrue(compareVersions(currentVersion, requiredMaxVersion) <= 0, "Test skipped because the current version: " + currentVersion + " is higher then required: " + requiredMaxVersion); + } + } } @Override @@ -56,4 +75,36 @@ public void afterAll(ExtensionContext context) { TestRegistry.INSTANCE.afterTestClass(context.getTestClass()); } + + private int compareVersions(String currentVersion, String requiredVersion) { + + currentVersion = removeSuffix(currentVersion); + requiredVersion = removeSuffix(requiredVersion); + + if (currentVersion.equals(TestConstants.KEYCLOAK_VERSION_DEFAULT)) { + currentVersion = "9999"; + } + if (requiredVersion.equals(TestConstants.KEYCLOAK_VERSION_DEFAULT)) { + requiredVersion = "9999"; + } + + String[] currentVersionParts = currentVersion.split("\\."); + String[] requiredVersionParts = requiredVersion.split("\\."); + + int length = Math.max(currentVersionParts.length, requiredVersionParts.length); + for (int i = 0; i < length; i++) { + int part1 = i < currentVersionParts.length ? Integer.parseInt(currentVersionParts[i]) : 0; + int part2 = i < requiredVersionParts.length ? Integer.parseInt(requiredVersionParts[i]) : 0; + + if (part1 != part2) { + return Integer.compare(part1, part2); + } + } + return 0; // same version + } + + public static String removeSuffix(String version) { + int index = version.indexOf('-'); + return (index == -1) ? version : version.substring(0, index); + } } diff --git a/testsuite/framework/src/main/java/org/keycloak/client/testsuite/framework/KeycloakVersion.java b/testsuite/framework/src/main/java/org/keycloak/client/testsuite/framework/KeycloakVersion.java new file mode 100644 index 0000000..19dc00a --- /dev/null +++ b/testsuite/framework/src/main/java/org/keycloak/client/testsuite/framework/KeycloakVersion.java @@ -0,0 +1,16 @@ +package org.keycloak.client.testsuite.framework; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @author Marek Posolda + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface KeycloakVersion { + String min() default ""; + String max() default "";; +} diff --git a/testsuite/framework/src/main/java/org/keycloak/client/testsuite/model/CibaConfig.java b/testsuite/framework/src/main/java/org/keycloak/client/testsuite/model/CibaConfig.java new file mode 100644 index 0000000..97bcd4b --- /dev/null +++ b/testsuite/framework/src/main/java/org/keycloak/client/testsuite/model/CibaConfig.java @@ -0,0 +1,54 @@ +/* + * Copyright 2020 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * 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. + */ +package org.keycloak.client.testsuite.model; + + +import java.util.Arrays; +import java.util.List; + +public class CibaConfig { + + // Constants + public static final String CIBA_POLL_MODE = "poll"; + public static final String CIBA_PING_MODE = "ping"; + public static final String CIBA_PUSH_MODE = "push"; + public static final List CIBA_SUPPORTED_MODES = Arrays.asList(CIBA_POLL_MODE, CIBA_PING_MODE); + + // realm attribute names + public static final String CIBA_BACKCHANNEL_TOKEN_DELIVERY_MODE = "cibaBackchannelTokenDeliveryMode"; + public static final String CIBA_EXPIRES_IN = "cibaExpiresIn"; + public static final String CIBA_INTERVAL = "cibaInterval"; + public static final String CIBA_AUTH_REQUESTED_USER_HINT = "cibaAuthRequestedUserHint"; + + // default value + public static final String DEFAULT_CIBA_POLICY_TOKEN_DELIVERY_MODE = CIBA_POLL_MODE; + public static final int DEFAULT_CIBA_POLICY_EXPIRES_IN = 120; + public static final int DEFAULT_CIBA_POLICY_INTERVAL = 5; + public static final String DEFAULT_CIBA_POLICY_AUTH_REQUESTED_USER_HINT = "login_hint"; + + private String backchannelTokenDeliveryMode = DEFAULT_CIBA_POLICY_TOKEN_DELIVERY_MODE; + private int expiresIn = DEFAULT_CIBA_POLICY_EXPIRES_IN; + private int poolingInterval = DEFAULT_CIBA_POLICY_INTERVAL; + private String authRequestedUserHint = DEFAULT_CIBA_POLICY_AUTH_REQUESTED_USER_HINT; + + // client attribute names + public static final String OIDC_CIBA_GRANT_ENABLED = "oidc.ciba.grant.enabled"; + public static final String CIBA_BACKCHANNEL_TOKEN_DELIVERY_MODE_PER_CLIENT = "ciba.backchannel.token.delivery.mode"; + public static final String CIBA_BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT = "ciba.backchannel.client.notification.endpoint"; + public static final String CIBA_BACKCHANNEL_AUTH_REQUEST_SIGNING_ALG = "ciba.backchannel.auth.request.signing.alg"; + +} diff --git a/testsuite/framework/src/main/java/org/keycloak/client/testsuite/model/OAuth2DeviceConfig.java b/testsuite/framework/src/main/java/org/keycloak/client/testsuite/model/OAuth2DeviceConfig.java new file mode 100644 index 0000000..86bc837 --- /dev/null +++ b/testsuite/framework/src/main/java/org/keycloak/client/testsuite/model/OAuth2DeviceConfig.java @@ -0,0 +1,45 @@ +/* + * + * * Copyright 2021 Red Hat, Inc. and/or its affiliates + * * and other contributors as indicated by the @author tags. + * * + * * 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. + * + */ + +package org.keycloak.client.testsuite.model; + +import java.io.Serializable; + +/** + * @author Pedro Igor + */ +public final class OAuth2DeviceConfig implements Serializable { + + // 10 minutes + public static final int DEFAULT_OAUTH2_DEVICE_CODE_LIFESPAN = 600; + // 5 seconds + public static final int DEFAULT_OAUTH2_DEVICE_POLLING_INTERVAL = 5; + + // realm attribute names + public static String OAUTH2_DEVICE_CODE_LIFESPAN = "oauth2DeviceCodeLifespan"; + public static String OAUTH2_DEVICE_POLLING_INTERVAL = "oauth2DevicePollingInterval"; + + // client attribute names + public static String OAUTH2_DEVICE_CODE_LIFESPAN_PER_CLIENT = "oauth2.device.code.lifespan"; + public static String OAUTH2_DEVICE_POLLING_INTERVAL_PER_CLIENT = "oauth2.device.polling.interval"; + public static final String OAUTH2_DEVICE_AUTHORIZATION_GRANT_ENABLED = "oauth2.device.authorization.grant.enabled"; + + private int lifespan = DEFAULT_OAUTH2_DEVICE_CODE_LIFESPAN; + private int poolingInterval = DEFAULT_OAUTH2_DEVICE_POLLING_INTERVAL; +} diff --git a/testsuite/framework/src/main/java/org/keycloak/client/testsuite/model/ParConfig.java b/testsuite/framework/src/main/java/org/keycloak/client/testsuite/model/ParConfig.java new file mode 100644 index 0000000..a674abd --- /dev/null +++ b/testsuite/framework/src/main/java/org/keycloak/client/testsuite/model/ParConfig.java @@ -0,0 +1,32 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * 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. + */ +package org.keycloak.client.testsuite.model; + + +public class ParConfig { + + // realm attribute names + public static final String PAR_REQUEST_URI_LIFESPAN = "parRequestUriLifespan"; + + // default value + public static final int DEFAULT_PAR_REQUEST_URI_LIFESPAN = 60; // sec + + private int requestUriLifespan = DEFAULT_PAR_REQUEST_URI_LIFESPAN; + + // client attribute names + public static final String REQUIRE_PUSHED_AUTHORIZATION_REQUESTS = "require.pushed.authorization.requests"; +} diff --git a/testsuite/framework/src/main/java/org/keycloak/client/testsuite/model/RealmAttributes.java b/testsuite/framework/src/main/java/org/keycloak/client/testsuite/model/RealmAttributes.java new file mode 100644 index 0000000..79ac937 --- /dev/null +++ b/testsuite/framework/src/main/java/org/keycloak/client/testsuite/model/RealmAttributes.java @@ -0,0 +1,60 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * 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. + */ + +package org.keycloak.client.testsuite.model; + +/** + * @author Stian Thorgersen + */ +public interface RealmAttributes { + + String DISPLAY_NAME = "displayName"; + + String DISPLAY_NAME_HTML = "displayNameHtml"; + + String ACTION_TOKEN_GENERATED_BY_ADMIN_LIFESPAN = "actionTokenGeneratedByAdminLifespan"; + + String ACTION_TOKEN_GENERATED_BY_USER_LIFESPAN = "actionTokenGeneratedByUserLifespan"; + + // KEYCLOAK-7688 Offline Session Max for Offline Token + String OFFLINE_SESSION_MAX_LIFESPAN_ENABLED = "offlineSessionMaxLifespanEnabled"; + + String OFFLINE_SESSION_MAX_LIFESPAN = "offlineSessionMaxLifespan"; + + String CLIENT_SESSION_IDLE_TIMEOUT = "clientSessionIdleTimeout"; + String CLIENT_SESSION_MAX_LIFESPAN = "clientSessionMaxLifespan"; + String CLIENT_OFFLINE_SESSION_IDLE_TIMEOUT = "clientOfflineSessionIdleTimeout"; + String CLIENT_OFFLINE_SESSION_MAX_LIFESPAN = "clientOfflineSessionMaxLifespan"; + String WEBAUTHN_POLICY_RP_ENTITY_NAME = "webAuthnPolicyRpEntityName"; + String WEBAUTHN_POLICY_SIGNATURE_ALGORITHMS = "webAuthnPolicySignatureAlgorithms"; + + String WEBAUTHN_POLICY_RP_ID = "webAuthnPolicyRpId"; + String WEBAUTHN_POLICY_ATTESTATION_CONVEYANCE_PREFERENCE = "webAuthnPolicyAttestationConveyancePreference"; + String WEBAUTHN_POLICY_AUTHENTICATOR_ATTACHMENT = "webAuthnPolicyAuthenticatorAttachment"; + String WEBAUTHN_POLICY_REQUIRE_RESIDENT_KEY = "webAuthnPolicyRequireResidentKey"; + String WEBAUTHN_POLICY_USER_VERIFICATION_REQUIREMENT = "webAuthnPolicyUserVerificationRequirement"; + String WEBAUTHN_POLICY_CREATE_TIMEOUT = "webAuthnPolicyCreateTimeout"; + String WEBAUTHN_POLICY_AVOID_SAME_AUTHENTICATOR_REGISTER = "webAuthnPolicyAvoidSameAuthenticatorRegister"; + String WEBAUTHN_POLICY_ACCEPTABLE_AAGUIDS = "webAuthnPolicyAcceptableAaguids"; + String WEBAUTHN_POLICY_EXTRA_ORIGINS = "webAuthnPolicyExtraOrigins"; + + String ADMIN_EVENTS_EXPIRATION = "adminEventsExpiration"; + + String FIRST_BROKER_LOGIN_FLOW_ID = "firstBrokerLoginFlowId"; + + String ORGANIZATIONS_ENABLED = "organizationsEnabled"; +} diff --git a/testsuite/framework/src/main/java/org/keycloak/testsuite/client/Keycloak.java b/testsuite/framework/src/main/java/org/keycloak/testsuite/client/Keycloak.java new file mode 100644 index 0000000..26d38fa --- /dev/null +++ b/testsuite/framework/src/main/java/org/keycloak/testsuite/client/Keycloak.java @@ -0,0 +1,188 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * 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. + */ +package org.keycloak.testsuite.client; + +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.client.WebTarget; +import org.keycloak.admin.client.Config; +import org.keycloak.admin.client.resource.BearerAuthFilter; +import org.keycloak.admin.client.resource.RealmResource; +import org.keycloak.admin.client.resource.RealmsResource; +import org.keycloak.admin.client.resource.ServerInfoResource; +import org.keycloak.admin.client.spi.ResteasyClientProvider; +import org.keycloak.admin.client.token.TokenManager; + +import javax.net.ssl.SSLContext; +import java.net.URI; +import java.util.Iterator; +import java.util.ServiceLoader; + +import static org.keycloak.OAuth2Constants.PASSWORD; + +/** + * Provides a Keycloak client. By default, this implementation uses a the default RestEasy client builder settings. + * To customize the underling client, use a {@link org.keycloak.admin.client.KeycloakBuilder} to create a Keycloak client. + * + * To read Responses, you can use {@link org.keycloak.admin.client.CreatedResponseUtil} for objects created + * + * @author rodrigo.sasaki@icarros.com.br + * @see org.keycloak.admin.client.KeycloakBuilder + */ +public class Keycloak implements AutoCloseable { + + private static volatile ResteasyClientProvider CLIENT_PROVIDER = resolveResteasyClientProvider(); + + private static ResteasyClientProvider resolveResteasyClientProvider() { + Iterator providers = ServiceLoader.load(ResteasyClientProvider.class).iterator(); + + if (providers.hasNext()) { + ResteasyClientProvider provider = providers.next(); + + if (providers.hasNext()) { + throw new IllegalArgumentException("Multiple " + ResteasyClientProvider.class + " implementations found"); + } + + return provider; + } + + return createDefaultResteasyClientProvider(); + } + + private static ResteasyClientProvider createDefaultResteasyClientProvider() { + try { + return (ResteasyClientProvider) Keycloak.class.getClassLoader().loadClass("org.keycloak.admin.client.spi.ResteasyClientClassicProvider").getDeclaredConstructor().newInstance(); + } catch (Exception cause) { + throw new RuntimeException("Could not instantiate default client provider", cause); + } + } + + public static void setClientProvider(ResteasyClientProvider provider) { + CLIENT_PROVIDER = provider; + } + + public static ResteasyClientProvider getClientProvider() { + return CLIENT_PROVIDER; + } + + private final Config config; + private final TokenManager tokenManager; + private final String authToken; + private final WebTarget target; + private final Client client; + private boolean closed = false; + + Keycloak(String serverUrl, String realm, String username, String password, String clientId, String clientSecret, String grantType, Client resteasyClient, String authtoken, String scope) { + config = new Config(serverUrl, realm, username, password, clientId, clientSecret, grantType, scope); + client = resteasyClient != null ? resteasyClient : newRestEasyClient(null, null, false); + authToken = authtoken; + tokenManager = authtoken == null ? new TokenManager(config, client) : null; + + target = client.target(config.getServerUrl()); + target.register(newAuthFilter()); + } + + private static Client newRestEasyClient(Object customJacksonProvider, SSLContext sslContext, boolean disableTrustManager) { + return CLIENT_PROVIDER.newRestEasyClient(customJacksonProvider, sslContext, disableTrustManager); + } + + private BearerAuthFilter newAuthFilter() { + return authToken != null ? new BearerAuthFilter(authToken) : new BearerAuthFilter(tokenManager); + } + public static Keycloak getInstance(String serverUrl, String realm, String username, String password, String clientId, String clientSecret, SSLContext sslContext, Object customJacksonProvider, boolean disableTrustManager, String authToken, String scope) { + return new Keycloak(serverUrl, realm, username, password, clientId, clientSecret, PASSWORD, newRestEasyClient(customJacksonProvider, sslContext, disableTrustManager), authToken, scope); + } + public static Keycloak getInstance(String serverUrl, String realm, String username, String password, String grantType, String clientId, String clientSecret, SSLContext sslContext, Object customJacksonProvider, boolean disableTrustManager, String authToken, String scope) { + return new Keycloak(serverUrl, realm, username, password, clientId, clientSecret, grantType, newRestEasyClient(customJacksonProvider, sslContext, disableTrustManager), authToken, scope); + } + public static Keycloak getInstance(String serverUrl, String realm, String username, String password, String clientId, String clientSecret, SSLContext sslContext, Object customJacksonProvider, boolean disableTrustManager, String authToken) { + return new Keycloak(serverUrl, realm, username, password, clientId, clientSecret, PASSWORD, newRestEasyClient(customJacksonProvider, sslContext, disableTrustManager), authToken, null); + } + + public static Keycloak getInstance(String serverUrl, String realm, String username, String password, String clientId, String clientSecret) { + return getInstance(serverUrl, realm, username, password, clientId, clientSecret, null, null, false, null); + } + + public static Keycloak getInstance(String serverUrl, String realm, String username, String password, String clientId, String clientSecret, SSLContext sslContext) { + return getInstance(serverUrl, realm, username, password, clientId, clientSecret, sslContext, null, false, null); + } + + public static Keycloak getInstance(String serverUrl, String realm, String username, String password, String clientId, String clientSecret, SSLContext sslContext, Object customJacksonProvider) { + return getInstance(serverUrl, realm, username, password, clientId, clientSecret, sslContext, customJacksonProvider, false, null); + } + + public static Keycloak getInstance(String serverUrl, String realm, String username, String password, String clientId) { + return getInstance(serverUrl, realm, username, password, clientId, null, null, null, false, null); + } + + public static Keycloak getInstance(String serverUrl, String realm, String username, String password, String clientId, SSLContext sslContext) { + return getInstance(serverUrl, realm, username, password, clientId, null, sslContext, null, false, null); + } + + public static Keycloak getInstance(String serverUrl, String realm, String clientId, String authToken) { + return getInstance(serverUrl, realm, null, null, clientId, null, null, null, false, authToken); + } + + public static Keycloak getInstance(String serverUrl, String realm, String clientId, String authToken, SSLContext sllSslContext) { + return getInstance(serverUrl, realm, null, null, clientId, null, sllSslContext, null, false, authToken); + } + + public RealmsResource realms() { + return CLIENT_PROVIDER.targetProxy(target, RealmsResource.class); + } + + public RealmResource realm(String realmName) { + return realms().realm(realmName); + } + + public ServerInfoResource serverInfo() { + return CLIENT_PROVIDER.targetProxy(target, ServerInfoResource.class); + } + + public TokenManager tokenManager() { + return tokenManager; + } + + /** + * Create a secure proxy based on an absolute URI. + * All set up with appropriate token + * + * @param proxyClass + * @param absoluteURI + * @param + * @return + */ + public T proxy(Class proxyClass, URI absoluteURI) { + WebTarget register = client.target(absoluteURI).register(newAuthFilter()); + return CLIENT_PROVIDER.targetProxy(register, proxyClass); + } + + /** + * Closes the underlying client. After calling this method, this Keycloak instance cannot be reused. + */ + @Override + public void close() { + closed = true; + client.close(); + } + + /** + * @return true if the underlying client is closed. + */ + public boolean isClosed() { + return closed; + } +} diff --git a/testsuite/framework/src/main/java/org/keycloak/testsuite/util/AdminClientUtil.java b/testsuite/framework/src/main/java/org/keycloak/testsuite/util/AdminClientUtil.java new file mode 100644 index 0000000..5fe471d --- /dev/null +++ b/testsuite/framework/src/main/java/org/keycloak/testsuite/util/AdminClientUtil.java @@ -0,0 +1,135 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * 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. + */ + +package org.keycloak.testsuite.util; + +import org.apache.http.HttpHost; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.conn.HttpClientConnectionManager; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.jboss.resteasy.client.jaxrs.ClientHttpEngine; +import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder; +import org.jboss.resteasy.client.jaxrs.engines.ClientHttpEngineBuilder43; +import org.keycloak.OAuth2Constants; +import org.keycloak.models.Constants; +import org.keycloak.testsuite.client.Keycloak; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManagerFactory; +import java.io.IOException; +import java.io.InputStream; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.SecureRandom; + +import static org.keycloak.testsuite.util.ServerURLs.getAuthServerContextRoot; + + +public class AdminClientUtil { + + public static final int NUMBER_OF_CONNECTIONS = 10; + + private static final String TLS_KEYSTORE_FILENAME = "tls.jks"; + private static final String TLS_KEYSTORE_PASSWORD = "changeit"; + + public static Keycloak createAdminClient(boolean ignoreUnknownProperties, String authServerContextRoot) throws Exception { + return createAdminClient(ignoreUnknownProperties, authServerContextRoot, "master", "admin", "admin", + Constants.ADMIN_CLI_CLIENT_ID, null, null); + + } + + public static Keycloak createAdminClient(boolean ignoreUnknownProperties, String realmName, String username, + String password, String clientId, String clientSecret) { + return createAdminClient(ignoreUnknownProperties, getAuthServerContextRoot(), realmName, username, password, + clientId, clientSecret, null); + } + + public static Keycloak createAdminClient(boolean ignoreUnknownProperties, String authServerContextRoot, String realmName, + String username, String password, String clientId, String clientSecret, String scope) { + return Keycloak.getInstance(authServerContextRoot, realmName, username, password, clientId, clientSecret, buildSslContext()); + } + + public static Keycloak createAdminClientWithClientCredentials(String realmName, String clientId, String clientSecret, String scope) { + return Keycloak.getInstance(getAuthServerContextRoot(), realmName, null, null, OAuth2Constants.CLIENT_CREDENTIALS, clientId, clientSecret, buildSslContext(), null, false, null, scope); + } + + public static Keycloak createAdminClient() throws Exception { + return createAdminClient(false, getAuthServerContextRoot()); + } + + public static Keycloak createAdminClient(boolean ignoreUnknownProperties) throws Exception { + return createAdminClient(ignoreUnknownProperties, getAuthServerContextRoot()); + } + + private static SSLContext buildSslContext() { + SSLContext sslContext; + try { + KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + keyStore.load(loadResourceAsStream(TLS_KEYSTORE_FILENAME), TLS_KEYSTORE_PASSWORD.toCharArray()); + + TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init(keyStore); + sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, tmf.getTrustManagers(), new SecureRandom()); + } catch (GeneralSecurityException | IOException e) { + sslContext = null; + } + return sslContext; + } + + private static InputStream loadResourceAsStream(String filename) { + return AdminClientUtil.class.getClassLoader().getResourceAsStream(filename); + } + + public static ClientHttpEngine getCustomClientHttpEngine(ResteasyClientBuilder resteasyClientBuilder, int validateAfterInactivity, Boolean followRedirects) { + return new CustomClientHttpEngineBuilder43(validateAfterInactivity, followRedirects).resteasyClientBuilder(resteasyClientBuilder).build(); + } + + /** + * Adds a possibility to pass validateAfterInactivity parameter into underlying ConnectionManager. The parameter affects how + * long the connection is being used without testing if it became stale, default value is 2000ms + */ + private static class CustomClientHttpEngineBuilder43 extends ClientHttpEngineBuilder43 { + + private final int validateAfterInactivity; + private final Boolean followRedirects; + + private CustomClientHttpEngineBuilder43(int validateAfterInactivity, Boolean followRedirects) { + this.validateAfterInactivity = validateAfterInactivity; + this.followRedirects = followRedirects; + } + + @Override + protected ClientHttpEngine createEngine(final HttpClientConnectionManager cm, final RequestConfig.Builder rcBuilder, + final HttpHost defaultProxy, final int responseBufferSize, final HostnameVerifier verifier, final SSLContext theContext) { + final ClientHttpEngine engine; + if (cm instanceof PoolingHttpClientConnectionManager) { + PoolingHttpClientConnectionManager pcm = (PoolingHttpClientConnectionManager) cm; + pcm.setValidateAfterInactivity(validateAfterInactivity); + engine = super.createEngine(pcm, rcBuilder, defaultProxy, responseBufferSize, verifier, theContext); + } else { + engine = super.createEngine(cm, rcBuilder, defaultProxy, responseBufferSize, verifier, theContext); + } + if (followRedirects != null) { + engine.setFollowRedirects(followRedirects); + } + return engine; + } + } + +} diff --git a/testsuite/framework/src/main/java/org/keycloak/testsuite/util/ApiUtil.java b/testsuite/framework/src/main/java/org/keycloak/testsuite/util/ApiUtil.java new file mode 100644 index 0000000..37bcb64 --- /dev/null +++ b/testsuite/framework/src/main/java/org/keycloak/testsuite/util/ApiUtil.java @@ -0,0 +1,321 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * 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. + */ +package org.keycloak.testsuite.util; + +import org.jboss.logging.Logger; +import org.keycloak.admin.client.resource.AuthorizationResource; +import org.keycloak.admin.client.resource.ClientResource; +import org.keycloak.admin.client.resource.ClientScopeResource; +import org.keycloak.admin.client.resource.GroupResource; +import org.keycloak.admin.client.resource.RealmResource; +import org.keycloak.admin.client.resource.RoleResource; +import org.keycloak.admin.client.resource.UserResource; +import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.ClientScopeRepresentation; +import org.keycloak.representations.idm.CredentialRepresentation; +import org.keycloak.representations.idm.GroupRepresentation; +import org.keycloak.representations.idm.ProtocolMapperRepresentation; +import org.keycloak.representations.idm.RequiredActionProviderRepresentation; +import org.keycloak.representations.idm.RoleRepresentation; +import org.keycloak.representations.idm.UserRepresentation; + +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.Response.Status; +import jakarta.ws.rs.core.Response.StatusType; +import java.net.URI; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import static org.keycloak.representations.idm.CredentialRepresentation.PASSWORD; + +/** + * @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc. + */ +public class ApiUtil { + + private static final Logger log = Logger.getLogger(ApiUtil.class); + + public static String getCreatedId(Response response) { + URI location = response.getLocation(); + if (!response.getStatusInfo().equals(Status.CREATED)) { + StatusType statusInfo = response.getStatusInfo(); + response.bufferEntity(); + String body = response.readEntity(String.class); + throw new WebApplicationException("Create method returned status " + + statusInfo.getReasonPhrase() + " (Code: " + statusInfo.getStatusCode() + "); expected status: Created (201). Response body: " + body, response); + } + if (location == null) { + return null; + } + String path = location.getPath(); + return path.substring(path.lastIndexOf('/') + 1); + } + + public static ClientResource findClientResourceById(RealmResource realm, String id) { + for (ClientRepresentation c : realm.clients().findAll()) { + if (c.getId().equals(id)) { + return realm.clients().get(c.getId()); + } + } + return null; + } + + public static ClientResource findClientResourceByClientId(RealmResource realm, String clientId) { + for (ClientRepresentation c : realm.clients().findAll()) { + if (c.getClientId().equals(clientId)) { + return realm.clients().get(c.getId()); + } + } + return null; + } + + public static ClientResource findClientResourceByName(RealmResource realm, String name) { + for (ClientRepresentation c : realm.clients().findAll()) { + if (name.equals(c.getName())) { + return realm.clients().get(c.getId()); + } + } + return null; + } + + public static ClientResource findClientByClientId(RealmResource realm, String clientId) { + for (ClientRepresentation c : realm.clients().findAll()) { + if (clientId.equals(c.getClientId())) { + return realm.clients().get(c.getId()); + } + } + return null; + } + + public static RoleResource findClientRoleByName(ClientResource client, String role) { + return client.roles().get(role); + } + + public static ProtocolMapperRepresentation findProtocolMapperByName(ClientResource client, String name) { + for (ProtocolMapperRepresentation p : client.getProtocolMappers().getMappers()) { + if (p.getName().equals(name)) { + return p; + } + } + return null; + } + + public static ProtocolMapperRepresentation findProtocolMapperByName(ClientScopeResource scope, String name) { + for (ProtocolMapperRepresentation p : scope.getProtocolMappers().getMappers()) { + if (p.getName().equals(name)) { + return p; + } + } + return null; + } + + public static ClientScopeResource findClientScopeByName(RealmResource realm, String clientScopeName) { + for (ClientScopeRepresentation clientScope : realm.clientScopes().findAll()) { + if (clientScopeName.equals(clientScope.getName())) { + return realm.clientScopes().get(clientScope.getId()); + } + } + return null; + } + + public static RoleResource findRealmRoleByName(RealmResource realm, String role) { + return realm.roles().get(role); + } + + public static UserRepresentation findUserByUsername(RealmResource realm, String username) { + UserRepresentation user = null; + List ur = realm.users().search(username, true); + if (ur.size() == 1) { + user = ur.get(0); + } + + if (ur.size() > 1) { // try to be more specific + for (UserRepresentation rep : ur) { + if (rep.getUsername().equalsIgnoreCase(username)) { + return rep; + } + } + } + + return user; + } + + public static UserResource findUserByUsernameId(RealmResource realm, String username) { + return realm.users().get(findUserByUsername(realm, username).getId()); + } + + /** + * Creates a user + * @param realm + * @param user + * @return ID of the new user + */ + public static String createUserWithAdminClient(RealmResource realm, UserRepresentation user) { + Response response = realm.users().create(user); + String createdId = getCreatedId(response); + response.close(); + return createdId; + } + + /** + * Creates a user and sets the password + * @param realm + * @param user + * @param password + * @return ID of the new user + */ + public static String createUserAndResetPasswordWithAdminClient(RealmResource realm, UserRepresentation user, String password) { + return createUserAndResetPasswordWithAdminClient(realm, user, password, false); + } + + /** + * Creates a user and sets the password + * @param realm + * @param user + * @param password + * @param temporary + * @return ID of the new user + */ + public static String createUserAndResetPasswordWithAdminClient(RealmResource realm, UserRepresentation user, String password, boolean temporary) { + String id = createUserWithAdminClient(realm, user); + resetUserPassword(realm.users().get(id), password, temporary); + return id; + } + + public static void resetUserPassword(UserResource userResource, String newPassword, boolean temporary) { + CredentialRepresentation newCredential = new CredentialRepresentation(); + newCredential.setType(PASSWORD); + newCredential.setValue(newPassword); + newCredential.setTemporary(temporary); + userResource.resetPassword(newCredential); + } + + public static void assignRealmRoles(RealmResource realm, String userId, String... roles) { + String realmName = realm.toRepresentation().getRealm(); + + List roleRepresentations = new ArrayList<>(); + for (String roleName : roles) { + RoleRepresentation role = realm.roles().get(roleName).toRepresentation(); + roleRepresentations.add(role); + } + + UserResource userResource = realm.users().get(userId); + log.info("assigning roles " + Arrays.toString(roles) + " to user: \"" + + userResource.toRepresentation().getUsername() + "\" in realm: \"" + realmName + "\""); + userResource.roles().realmLevel().add(roleRepresentations); + } + + public static void removeUserByUsername(RealmResource realmResource, String username) { + UserRepresentation user = findUserByUsername(realmResource, username); + if (user != null) { + realmResource.users().delete(user.getId()); + } + } + + public static void assignClientRoles(RealmResource realm, String userId, String clientName, String... roles) { + String realmName = realm.toRepresentation().getRealm(); + String clientId = ""; + for (ClientRepresentation clientRepresentation : realm.clients().findAll()) { + if (clientRepresentation.getClientId().equals(clientName)) { + clientId = clientRepresentation.getId(); + } + } + + if (!clientId.isEmpty()) { + ClientResource clientResource = realm.clients().get(clientId); + + List roleRepresentations = new ArrayList<>(); + for (String roleName : roles) { + RoleRepresentation role = clientResource.roles().get(roleName).toRepresentation(); + roleRepresentations.add(role); + } + + UserResource userResource = realm.users().get(userId); + log.info("assigning role: " + Arrays.toString(roles) + " to user: \"" + + userResource.toRepresentation().getUsername() + "\" of client: \"" + + clientName + "\" in realm: \"" + realmName + "\""); + userResource.roles().clientLevel(clientId).add(roleRepresentations); + } else { + log.warn("client with name " + clientName + " doesn't exist in realm " + realmName); + } + } + + public static boolean groupContainsSubgroup(GroupResource groupsResource, GroupRepresentation subgroup) { + boolean contains = false; + for (GroupRepresentation sg : groupsResource.getSubGroups(null,null, true)) { + if (subgroup.getId().equals(sg.getId())) { + contains = true; + break; + } + } + return contains; + } + + public static AuthorizationResource findAuthorizationSettings(RealmResource realm, String clientId) { + for (ClientRepresentation c : realm.clients().findAll()) { + if (c.getClientId().equals(clientId)) { + return realm.clients().get(c.getId()).authorization(); + } + } + return null; + } + + public static void updateRequiredActionsOrderByAlias(final RealmResource realmResource, + final List requiredActionsInTargetOrder) { + final var realmName = realmResource.toRepresentation().getRealm(); + final var initialRequiredActionsOrdered = realmResource.flows().getRequiredActions().stream() + .map(RequiredActionProviderRepresentation::getAlias).collect(Collectors.toList()); + log.infof("initial required actions order for realm '%s': %s", realmName, initialRequiredActionsOrdered); + log.infof("target order for realm '%s' (maybe partial): %s", realmName, requiredActionsInTargetOrder); + + final var requiredActionsToConfigureWithLowerPrio = new ArrayList<>(requiredActionsInTargetOrder); + for (final var requiredActionAlias : requiredActionsInTargetOrder) { + var allRequiredActionsOrdered = realmResource.flows().getRequiredActions().stream() + .map(RequiredActionProviderRepresentation::getAlias).collect(Collectors.toList()); + + requiredActionsToConfigureWithLowerPrio.remove(requiredActionAlias); + + final var currentIndex = allRequiredActionsOrdered.indexOf(requiredActionAlias); + if (currentIndex == -1) { + throw new IllegalStateException("Required action not found: " + requiredActionAlias); + } + + final var aliasOfCurrentlyFirstActionWithLowerTargetPrioOpt = allRequiredActionsOrdered.stream() + .filter(requiredActionsToConfigureWithLowerPrio::contains).findFirst(); + aliasOfCurrentlyFirstActionWithLowerTargetPrioOpt + .ifPresent(aliasOfCurrentlyFirstActionWithLowerTargetPrio -> { + final var indexOfCurrentlyFirstActionWithLowerTargetPrio = + allRequiredActionsOrdered.indexOf(aliasOfCurrentlyFirstActionWithLowerTargetPrio); + final var positionsToMoveCurrentActionUp = + Math.max(currentIndex - indexOfCurrentlyFirstActionWithLowerTargetPrio, 0); + if (positionsToMoveCurrentActionUp > 0) { + for (var i = 0; i < positionsToMoveCurrentActionUp; i++) { + realmResource.flows().raiseRequiredActionPriority(requiredActionAlias); + } + } + }); + } + + final var updatedRequiredActionsOrdered = realmResource.flows().getRequiredActions().stream() + .map(RequiredActionProviderRepresentation::getAlias).collect(Collectors.toList()); + log.infof("updated required actions order for realm '%s': %s", realmName, updatedRequiredActionsOrdered); + } + +} diff --git a/testsuite/framework/src/main/java/org/keycloak/testsuite/util/ClientScopeBuilder.java b/testsuite/framework/src/main/java/org/keycloak/testsuite/util/ClientScopeBuilder.java new file mode 100644 index 0000000..c6f4ed6 --- /dev/null +++ b/testsuite/framework/src/main/java/org/keycloak/testsuite/util/ClientScopeBuilder.java @@ -0,0 +1,59 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * 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. + */ +package org.keycloak.testsuite.util; + +import org.keycloak.representations.idm.ClientScopeRepresentation; + +/** + * @author Yoshiyuki Tabata + */ +public class ClientScopeBuilder { + + private final ClientScopeRepresentation rep; + + public static ClientScopeBuilder create() { + ClientScopeRepresentation rep = new ClientScopeRepresentation(); + return new ClientScopeBuilder(rep); + } + + public static ClientScopeBuilder edit(ClientScopeRepresentation rep) { + return new ClientScopeBuilder(rep); + } + + private ClientScopeBuilder(ClientScopeRepresentation rep) { + this.rep = rep; + } + + public ClientScopeRepresentation build() { + return rep; + } + + public ClientScopeBuilder name(String name) { + rep.setName(name); + return this; + } + + public ClientScopeBuilder description(String description) { + rep.setDescription(description); + return this; + } + + public ClientScopeBuilder protocol(String protocol) { + rep.setProtocol(protocol); + return this; + } +} diff --git a/testsuite/framework/src/main/java/org/keycloak/testsuite/util/CredentialBuilder.java b/testsuite/framework/src/main/java/org/keycloak/testsuite/util/CredentialBuilder.java new file mode 100644 index 0000000..ea3adb0 --- /dev/null +++ b/testsuite/framework/src/main/java/org/keycloak/testsuite/util/CredentialBuilder.java @@ -0,0 +1,46 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * 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. + */ + +package org.keycloak.testsuite.util; + +import org.keycloak.representations.idm.CredentialRepresentation; + +/** + * @author Stian Thorgersen + */ +public class CredentialBuilder { + + private CredentialRepresentation rep = new CredentialRepresentation(); + + public static CredentialBuilder create() { + return new CredentialBuilder(); + } + + private CredentialBuilder() { + } + + public CredentialBuilder password(String password) { + rep.setType(CredentialRepresentation.PASSWORD); + rep.setValue(password); + return this; + } + + public CredentialRepresentation build() { + return rep; + } + +} diff --git a/testsuite/framework/src/main/java/org/keycloak/testsuite/util/KeycloakModelUtils.java b/testsuite/framework/src/main/java/org/keycloak/testsuite/util/KeycloakModelUtils.java new file mode 100644 index 0000000..c9c79b6 --- /dev/null +++ b/testsuite/framework/src/main/java/org/keycloak/testsuite/util/KeycloakModelUtils.java @@ -0,0 +1,48 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * 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. + */ + +package org.keycloak.testsuite.util; + +import org.jboss.logging.Logger; + +import java.util.UUID; + +/** + * Set of helper methods, which are useful in various model implementations. + * + * @author Marek Posolda, + * Daniel Fesenmeyer + */ +public final class KeycloakModelUtils { + + private static final Logger logger = Logger.getLogger(KeycloakModelUtils.class); + + public static final String AUTH_TYPE_CLIENT_SECRET = "client-secret"; + public static final String AUTH_TYPE_CLIENT_SECRET_JWT = "client-secret-jwt"; + + public static final String GROUP_PATH_SEPARATOR = "/"; + public static final String GROUP_PATH_ESCAPE = "~"; + private static final char CLIENT_ROLE_SEPARATOR = '.'; + + private KeycloakModelUtils() { + } + + public static String generateId() { + return UUID.randomUUID().toString(); + } + +} diff --git a/testsuite/framework/src/main/java/org/keycloak/testsuite/util/MailServerConfiguration.java b/testsuite/framework/src/main/java/org/keycloak/testsuite/util/MailServerConfiguration.java new file mode 100644 index 0000000..50c8ed3 --- /dev/null +++ b/testsuite/framework/src/main/java/org/keycloak/testsuite/util/MailServerConfiguration.java @@ -0,0 +1,30 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * 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. + */ + +package org.keycloak.testsuite.util; + +/** + * + * @author vramik + */ +public class MailServerConfiguration { + public static final String FROM = "server@mail.test"; + public static final String HOST = "localhost"; + public static final String PORT = "3025"; + public static final String PORT_SSL = "3465"; + public static final String STARTTLS = "true"; +} diff --git a/testsuite/framework/src/main/java/org/keycloak/testsuite/util/MediaType.java b/testsuite/framework/src/main/java/org/keycloak/testsuite/util/MediaType.java new file mode 100644 index 0000000..7dabec6 --- /dev/null +++ b/testsuite/framework/src/main/java/org/keycloak/testsuite/util/MediaType.java @@ -0,0 +1,47 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * 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. + */ + +package org.keycloak.testsuite.util; + +/** + * @author Stian Thorgersen + */ +public class MediaType { + + public static final String TEXT_HTML_UTF_8 = "text/html; charset=utf-8"; + public static final jakarta.ws.rs.core.MediaType TEXT_HTML_UTF_8_TYPE = new jakarta.ws.rs.core.MediaType("text", "html", "utf-8"); + + public static final String TEXT_PLAIN_UTF_8 = "text/plain; charset=utf-8"; + public static final jakarta.ws.rs.core.MediaType TEXT_PLAIN_UTF_8_TYPE = new jakarta.ws.rs.core.MediaType("text", "plain", "utf-8"); + + public static final String TEXT_PLAIN_JAVASCRIPT = "text/javascript; charset=utf-8"; + public static final jakarta.ws.rs.core.MediaType TEXT_JAVASCRIPT_UTF_8_TYPE = new jakarta.ws.rs.core.MediaType("text", "javascript", "utf-8"); + + public static final String APPLICATION_JSON = jakarta.ws.rs.core.MediaType.APPLICATION_JSON; + public static final jakarta.ws.rs.core.MediaType APPLICATION_JSON_TYPE = jakarta.ws.rs.core.MediaType.APPLICATION_JSON_TYPE; + + public static final String APPLICATION_FORM_URLENCODED = jakarta.ws.rs.core.MediaType.APPLICATION_FORM_URLENCODED; + public static final jakarta.ws.rs.core.MediaType APPLICATION_FORM_URLENCODED_TYPE = jakarta.ws.rs.core.MediaType.APPLICATION_FORM_URLENCODED_TYPE; + + public static final String APPLICATION_JWT = "application/jwt"; + public static final jakarta.ws.rs.core.MediaType APPLICATION_JWT_TYPE = new jakarta.ws.rs.core.MediaType("application", "jwt"); + + public static final String APPLICATION_XML = jakarta.ws.rs.core.MediaType.APPLICATION_XML; + + public static final String TEXT_XML = jakarta.ws.rs.core.MediaType.TEXT_XML; + +} diff --git a/testsuite/framework/src/main/java/org/keycloak/testsuite/util/RealmBuilder.java b/testsuite/framework/src/main/java/org/keycloak/testsuite/util/RealmBuilder.java index 60b1c7f..c034f6c 100644 --- a/testsuite/framework/src/main/java/org/keycloak/testsuite/util/RealmBuilder.java +++ b/testsuite/framework/src/main/java/org/keycloak/testsuite/util/RealmBuilder.java @@ -96,14 +96,14 @@ public RealmBuilder attribute(String key, String value) { return this; } -// public RealmBuilder testMail() { -// Map config = new HashMap<>(); -// config.put("from", MailServerConfiguration.FROM); -// config.put("host", MailServerConfiguration.HOST); -// config.put("port", MailServerConfiguration.PORT); -// rep.setSmtpServer(config); -// return this; -// } + public RealmBuilder testMail() { + Map config = new HashMap<>(); + config.put("from", MailServerConfiguration.FROM); + config.put("host", MailServerConfiguration.HOST); + config.put("port", MailServerConfiguration.PORT); + rep.setSmtpServer(config); + return this; + } // public RealmBuilder testEventListener() { // if (rep.getEventsListeners() == null) { diff --git a/testsuite/framework/src/main/java/org/keycloak/testsuite/util/ServerURLs.java b/testsuite/framework/src/main/java/org/keycloak/testsuite/util/ServerURLs.java index cae9c2e..4b810d9 100644 --- a/testsuite/framework/src/main/java/org/keycloak/testsuite/util/ServerURLs.java +++ b/testsuite/framework/src/main/java/org/keycloak/testsuite/util/ServerURLs.java @@ -6,13 +6,15 @@ import org.keycloak.client.testsuite.framework.TestRegistry; import org.keycloak.client.testsuite.server.KeycloakServerProvider; +import static java.lang.Integer.parseInt; + /** * Fork of ServerURLs class from Keycloak */ public class ServerURLs { public static final String AUTH_SERVER_URL = TestRegistry.INSTANCE.getOrCreateProvider(KeycloakServerProvider.class).getAuthServerUrl(); - + public static final String AUTH_SERVER_HOST = getAuthServerHost(); public static final boolean AUTH_SERVER_SSL_REQUIRED = AUTH_SERVER_URL.startsWith("https"); public static final String AUTH_SERVER_PORT = getAuthServerPort(); public static final String AUTH_SERVER_SCHEME = AUTH_SERVER_SSL_REQUIRED ? "https" : "http"; @@ -25,4 +27,24 @@ private static String getAuthServerPort() { throw new RuntimeException(mue); } } + + private static String getAuthServerHost() { + try { + return new URL(AUTH_SERVER_URL).getHost(); + } catch (MalformedURLException mue) { + throw new RuntimeException(mue); + } + } + + public static String getAuthServerContextRoot() { + return getAuthServerContextRoot(0); + } + + public static String getAuthServerContextRoot(int clusterPortOffset) { + return removeDefaultPorts(String.format("%s://%s:%s", AUTH_SERVER_SCHEME, AUTH_SERVER_HOST, parseInt(AUTH_SERVER_PORT) + clusterPortOffset)); + } + + public static String removeDefaultPorts(String url) { + return url != null ? url.replaceFirst("(.*)(:80)(\\/.*)?$", "$1$3").replaceFirst("(.*)(:443)(\\/.*)?$", "$1$3") : null; + } } From 6e002e5015f80f74ef6865e924ee9929a526b82a Mon Sep 17 00:00:00 2001 From: Giuseppe Graziano Date: Mon, 9 Sep 2024 11:24:06 +0200 Subject: [PATCH 2/2] Initial tests for admin-client Closes #31869 Signed-off-by: Giuseppe Graziano --- admin-client/pom.xml | 15 -- .../client/testsuite/AdminClientTest.java | 20 +- .../keycloak/client/testsuite/ClientTest.java | 5 +- .../client/testsuite/RealmRolesTest.java | 2 +- .../keycloak/client/testsuite/RealmTest.java | 25 ++- .../client/testsuite}/events/EventType.java | 5 +- .../testsuite/framework/KeycloakVersion.java | 2 +- .../testsuite}/models/AccountRoles.java | 2 +- .../{model => models}/CibaConfig.java | 2 +- .../client/testsuite}/models/Constants.java | 2 +- .../{model => models}/OAuth2DeviceConfig.java | 2 +- .../{model => models}/ParConfig.java | 2 +- .../{model => models}/RealmAttributes.java | 2 +- .../keycloak/testsuite/client/Keycloak.java | 188 ------------------ .../testsuite/util/AdminClientUtil.java | 55 ++++- 15 files changed, 81 insertions(+), 248 deletions(-) rename {admin-client/src/main/java/org/keycloak => testsuite/framework/src/main/java/org/keycloak/client/testsuite}/events/EventType.java (99%) rename {admin-client/src/main/java/org/keycloak => testsuite/framework/src/main/java/org/keycloak/client/testsuite}/models/AccountRoles.java (96%) rename testsuite/framework/src/main/java/org/keycloak/client/testsuite/{model => models}/CibaConfig.java (98%) rename {admin-client/src/main/java/org/keycloak => testsuite/framework/src/main/java/org/keycloak/client/testsuite}/models/Constants.java (99%) rename testsuite/framework/src/main/java/org/keycloak/client/testsuite/{model => models}/OAuth2DeviceConfig.java (97%) rename testsuite/framework/src/main/java/org/keycloak/client/testsuite/{model => models}/ParConfig.java (96%) rename testsuite/framework/src/main/java/org/keycloak/client/testsuite/{model => models}/RealmAttributes.java (98%) delete mode 100644 testsuite/framework/src/main/java/org/keycloak/testsuite/client/Keycloak.java diff --git a/admin-client/pom.xml b/admin-client/pom.xml index 34a68c3..2138d04 100755 --- a/admin-client/pom.xml +++ b/admin-client/pom.xml @@ -132,7 +132,6 @@ org/keycloak/util/EnumWithStableIndex.java, org/keycloak/util/JsonSerialization.java, org/keycloak/util/SystemPropertiesJsonParserFactory.java, - org/keycloak/util/Time.java, @@ -168,20 +167,6 @@ org/keycloak/utils/StringUtil.java, - - org.keycloak - keycloak-server-spi-private - ${keycloak.version} - jar - sources - true - ${project.build.directory}/unpacked - - org/keycloak/models/Constants.java, - org/keycloak/models/AccountRoles.java, - org/keycloak/events/EventType.java, - - false true diff --git a/testsuite/admin-client-tests/src/test/java/org/keycloak/client/testsuite/AdminClientTest.java b/testsuite/admin-client-tests/src/test/java/org/keycloak/client/testsuite/AdminClientTest.java index 60b2bf3..b8c51a4 100644 --- a/testsuite/admin-client-tests/src/test/java/org/keycloak/client/testsuite/AdminClientTest.java +++ b/testsuite/admin-client-tests/src/test/java/org/keycloak/client/testsuite/AdminClientTest.java @@ -22,15 +22,15 @@ import jakarta.ws.rs.core.Response; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.keycloak.admin.client.Keycloak; import org.keycloak.admin.client.resource.ClientResource; import org.keycloak.admin.client.resource.RealmResource; +import org.keycloak.client.testsuite.models.Constants; import org.keycloak.common.constants.ServiceAccountConstants; -import org.keycloak.models.Constants; import org.keycloak.representations.AccessTokenResponse; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ClientScopeRepresentation; import org.keycloak.representations.idm.RealmRepresentation; -import org.keycloak.testsuite.client.Keycloak; import org.keycloak.testsuite.util.AdminClientUtil; import org.keycloak.testsuite.util.ApiUtil; import org.keycloak.testsuite.util.ClientBuilder; @@ -40,10 +40,7 @@ import org.keycloak.testsuite.util.UserBuilder; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; import java.util.List; -import java.util.stream.Stream; /** * Test for the various "Advanced" scenarios of java admin-client @@ -100,19 +97,6 @@ public void before() { clientUUID = adminClient.realm(realmName).clients().findByClientId(clientId).get(0).getId(); } - @Test - public void clientCredentialsAuthSuccess() throws Exception { - // Check possible to load the realm - RealmRepresentation realm = adminClient.realm(realmName).toRepresentation(); - Assert.assertEquals(realmName, realm.getRealm()); - - //setTimeOffset(1000); - - // Check still possible to load the realm after original token expired (admin client should automatically re-authenticate) - realm = adminClient.realm(realmName).toRepresentation(); - Assert.assertEquals(realmName, realm.getRealm()); - } - @Test public void clientCredentialsClientDisabled() throws Exception { try (Keycloak adminClient = AdminClientUtil.createAdminClientWithClientCredentials(realmName, clientId, clientSecret, null)) { diff --git a/testsuite/admin-client-tests/src/test/java/org/keycloak/client/testsuite/ClientTest.java b/testsuite/admin-client-tests/src/test/java/org/keycloak/client/testsuite/ClientTest.java index cc3ede9..d167a65 100644 --- a/testsuite/admin-client-tests/src/test/java/org/keycloak/client/testsuite/ClientTest.java +++ b/testsuite/admin-client-tests/src/test/java/org/keycloak/client/testsuite/ClientTest.java @@ -27,10 +27,9 @@ import org.keycloak.admin.client.resource.RoleMappingResource; import org.keycloak.client.testsuite.common.OAuthClient; import org.keycloak.client.testsuite.framework.KeycloakVersion; +import org.keycloak.client.testsuite.models.AccountRoles; +import org.keycloak.client.testsuite.models.Constants; import org.keycloak.common.util.Time; -import org.keycloak.models.AccountRoles; -import org.keycloak.models.Constants; -import org.keycloak.representations.adapters.action.GlobalRequestResult; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.OAuth2ErrorRepresentation; diff --git a/testsuite/admin-client-tests/src/test/java/org/keycloak/client/testsuite/RealmRolesTest.java b/testsuite/admin-client-tests/src/test/java/org/keycloak/client/testsuite/RealmRolesTest.java index 79127bc..8bae38e 100644 --- a/testsuite/admin-client-tests/src/test/java/org/keycloak/client/testsuite/RealmRolesTest.java +++ b/testsuite/admin-client-tests/src/test/java/org/keycloak/client/testsuite/RealmRolesTest.java @@ -27,7 +27,7 @@ import org.keycloak.admin.client.resource.RoleResource; import org.keycloak.admin.client.resource.RolesResource; import org.keycloak.admin.client.resource.UserResource; -import org.keycloak.models.Constants; +import org.keycloak.client.testsuite.models.Constants; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.GroupRepresentation; import org.keycloak.representations.idm.RoleRepresentation; diff --git a/testsuite/admin-client-tests/src/test/java/org/keycloak/client/testsuite/RealmTest.java b/testsuite/admin-client-tests/src/test/java/org/keycloak/client/testsuite/RealmTest.java index 7393bca..73558f9 100644 --- a/testsuite/admin-client-tests/src/test/java/org/keycloak/client/testsuite/RealmTest.java +++ b/testsuite/admin-client-tests/src/test/java/org/keycloak/client/testsuite/RealmTest.java @@ -23,21 +23,21 @@ import jakarta.ws.rs.NotFoundException; import org.apache.commons.io.IOUtils; import org.junit.jupiter.api.Test; +import org.keycloak.admin.client.Keycloak; import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.client.testsuite.common.Creator; import org.keycloak.client.testsuite.framework.KeycloakVersion; -import org.keycloak.events.EventType; -import org.keycloak.client.testsuite.model.CibaConfig; -import org.keycloak.models.Constants; -import org.keycloak.client.testsuite.model.OAuth2DeviceConfig; -import org.keycloak.client.testsuite.model.ParConfig; -import org.keycloak.client.testsuite.model.RealmAttributes; +import org.keycloak.client.testsuite.events.EventType; +import org.keycloak.client.testsuite.models.CibaConfig; +import org.keycloak.client.testsuite.models.Constants; +import org.keycloak.client.testsuite.models.OAuth2DeviceConfig; +import org.keycloak.client.testsuite.models.ParConfig; +import org.keycloak.client.testsuite.models.RealmAttributes; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ComponentRepresentation; import org.keycloak.representations.idm.RealmEventsConfigRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RoleRepresentation; -import org.keycloak.testsuite.client.Keycloak; import org.keycloak.testsuite.util.AdminClientUtil; import org.keycloak.testsuite.util.RealmBuilder; import org.keycloak.testsuite.util.UserBuilder; @@ -571,7 +571,6 @@ public void updateRealmWithReservedCharInNameOrEmptyName() { } @Test - @KeycloakVersion(min = "26.0.0") public void updateRealm() { // first change RealmRepresentation rep = realm.toRepresentation(); @@ -620,18 +619,24 @@ public void updateRealm() { assertEquals(Boolean.FALSE, rep.isRegistrationEmailAsUsername()); assertEquals(Boolean.FALSE, rep.isEditUsernameAllowed()); assertEquals(Boolean.FALSE, rep.isUserManagedAccessAllowed()); + } + @Test + @KeycloakVersion(min = "26.0.0") + public void updateRealmWithIncorrectTimeoutValues() { + // first change + RealmRepresentation rep = realm.toRepresentation(); rep.setAccessCodeLifespanLogin(0); rep.setAccessCodeLifespanUserAction(0); - try { realm.update(rep); - fail("Not expected to successfully update the realm"); + Assert.fail("Not expected to successfully update the realm"); } catch (Exception expected) { // Expected exception assertEquals("HTTP 400 Bad Request", expected.getMessage()); } } + @Test public void updateRealmWithNewRepresentation() { // first change diff --git a/admin-client/src/main/java/org/keycloak/events/EventType.java b/testsuite/framework/src/main/java/org/keycloak/client/testsuite/events/EventType.java similarity index 99% rename from admin-client/src/main/java/org/keycloak/events/EventType.java rename to testsuite/framework/src/main/java/org/keycloak/client/testsuite/events/EventType.java index 16697c5..5e92cc3 100755 --- a/admin-client/src/main/java/org/keycloak/events/EventType.java +++ b/testsuite/framework/src/main/java/org/keycloak/client/testsuite/events/EventType.java @@ -15,11 +15,12 @@ * limitations under the License. */ -package org.keycloak.events; +package org.keycloak.client.testsuite.events; + +import org.keycloak.util.EnumWithStableIndex; import java.util.Map; import java.util.Objects; -import org.keycloak.util.EnumWithStableIndex; /** * @author Stian Thorgersen diff --git a/testsuite/framework/src/main/java/org/keycloak/client/testsuite/framework/KeycloakVersion.java b/testsuite/framework/src/main/java/org/keycloak/client/testsuite/framework/KeycloakVersion.java index 19dc00a..f9f76be 100644 --- a/testsuite/framework/src/main/java/org/keycloak/client/testsuite/framework/KeycloakVersion.java +++ b/testsuite/framework/src/main/java/org/keycloak/client/testsuite/framework/KeycloakVersion.java @@ -6,7 +6,7 @@ import java.lang.annotation.Target; /** - * @author Marek Posolda + * @author Giuseppe Graziano */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) diff --git a/admin-client/src/main/java/org/keycloak/models/AccountRoles.java b/testsuite/framework/src/main/java/org/keycloak/client/testsuite/models/AccountRoles.java similarity index 96% rename from admin-client/src/main/java/org/keycloak/models/AccountRoles.java rename to testsuite/framework/src/main/java/org/keycloak/client/testsuite/models/AccountRoles.java index f607664..50e4cc1 100644 --- a/admin-client/src/main/java/org/keycloak/models/AccountRoles.java +++ b/testsuite/framework/src/main/java/org/keycloak/client/testsuite/models/AccountRoles.java @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.keycloak.models; +package org.keycloak.client.testsuite.models; /** * @author Stian Thorgersen diff --git a/testsuite/framework/src/main/java/org/keycloak/client/testsuite/model/CibaConfig.java b/testsuite/framework/src/main/java/org/keycloak/client/testsuite/models/CibaConfig.java similarity index 98% rename from testsuite/framework/src/main/java/org/keycloak/client/testsuite/model/CibaConfig.java rename to testsuite/framework/src/main/java/org/keycloak/client/testsuite/models/CibaConfig.java index 97bcd4b..685ae14 100644 --- a/testsuite/framework/src/main/java/org/keycloak/client/testsuite/model/CibaConfig.java +++ b/testsuite/framework/src/main/java/org/keycloak/client/testsuite/models/CibaConfig.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.keycloak.client.testsuite.model; +package org.keycloak.client.testsuite.models; import java.util.Arrays; diff --git a/admin-client/src/main/java/org/keycloak/models/Constants.java b/testsuite/framework/src/main/java/org/keycloak/client/testsuite/models/Constants.java similarity index 99% rename from admin-client/src/main/java/org/keycloak/models/Constants.java rename to testsuite/framework/src/main/java/org/keycloak/client/testsuite/models/Constants.java index 6fd1188..6be58bf 100755 --- a/admin-client/src/main/java/org/keycloak/models/Constants.java +++ b/testsuite/framework/src/main/java/org/keycloak/client/testsuite/models/Constants.java @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.keycloak.models; +package org.keycloak.client.testsuite.models; import org.keycloak.OAuth2Constants; import org.keycloak.crypto.Algorithm; diff --git a/testsuite/framework/src/main/java/org/keycloak/client/testsuite/model/OAuth2DeviceConfig.java b/testsuite/framework/src/main/java/org/keycloak/client/testsuite/models/OAuth2DeviceConfig.java similarity index 97% rename from testsuite/framework/src/main/java/org/keycloak/client/testsuite/model/OAuth2DeviceConfig.java rename to testsuite/framework/src/main/java/org/keycloak/client/testsuite/models/OAuth2DeviceConfig.java index 86bc837..6c1a30a 100644 --- a/testsuite/framework/src/main/java/org/keycloak/client/testsuite/model/OAuth2DeviceConfig.java +++ b/testsuite/framework/src/main/java/org/keycloak/client/testsuite/models/OAuth2DeviceConfig.java @@ -17,7 +17,7 @@ * */ -package org.keycloak.client.testsuite.model; +package org.keycloak.client.testsuite.models; import java.io.Serializable; diff --git a/testsuite/framework/src/main/java/org/keycloak/client/testsuite/model/ParConfig.java b/testsuite/framework/src/main/java/org/keycloak/client/testsuite/models/ParConfig.java similarity index 96% rename from testsuite/framework/src/main/java/org/keycloak/client/testsuite/model/ParConfig.java rename to testsuite/framework/src/main/java/org/keycloak/client/testsuite/models/ParConfig.java index a674abd..a9482a5 100644 --- a/testsuite/framework/src/main/java/org/keycloak/client/testsuite/model/ParConfig.java +++ b/testsuite/framework/src/main/java/org/keycloak/client/testsuite/models/ParConfig.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.keycloak.client.testsuite.model; +package org.keycloak.client.testsuite.models; public class ParConfig { diff --git a/testsuite/framework/src/main/java/org/keycloak/client/testsuite/model/RealmAttributes.java b/testsuite/framework/src/main/java/org/keycloak/client/testsuite/models/RealmAttributes.java similarity index 98% rename from testsuite/framework/src/main/java/org/keycloak/client/testsuite/model/RealmAttributes.java rename to testsuite/framework/src/main/java/org/keycloak/client/testsuite/models/RealmAttributes.java index 79ac937..0433443 100644 --- a/testsuite/framework/src/main/java/org/keycloak/client/testsuite/model/RealmAttributes.java +++ b/testsuite/framework/src/main/java/org/keycloak/client/testsuite/models/RealmAttributes.java @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.keycloak.client.testsuite.model; +package org.keycloak.client.testsuite.models; /** * @author Stian Thorgersen diff --git a/testsuite/framework/src/main/java/org/keycloak/testsuite/client/Keycloak.java b/testsuite/framework/src/main/java/org/keycloak/testsuite/client/Keycloak.java deleted file mode 100644 index 26d38fa..0000000 --- a/testsuite/framework/src/main/java/org/keycloak/testsuite/client/Keycloak.java +++ /dev/null @@ -1,188 +0,0 @@ -/* - * Copyright 2016 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * 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. - */ -package org.keycloak.testsuite.client; - -import jakarta.ws.rs.client.Client; -import jakarta.ws.rs.client.WebTarget; -import org.keycloak.admin.client.Config; -import org.keycloak.admin.client.resource.BearerAuthFilter; -import org.keycloak.admin.client.resource.RealmResource; -import org.keycloak.admin.client.resource.RealmsResource; -import org.keycloak.admin.client.resource.ServerInfoResource; -import org.keycloak.admin.client.spi.ResteasyClientProvider; -import org.keycloak.admin.client.token.TokenManager; - -import javax.net.ssl.SSLContext; -import java.net.URI; -import java.util.Iterator; -import java.util.ServiceLoader; - -import static org.keycloak.OAuth2Constants.PASSWORD; - -/** - * Provides a Keycloak client. By default, this implementation uses a the default RestEasy client builder settings. - * To customize the underling client, use a {@link org.keycloak.admin.client.KeycloakBuilder} to create a Keycloak client. - * - * To read Responses, you can use {@link org.keycloak.admin.client.CreatedResponseUtil} for objects created - * - * @author rodrigo.sasaki@icarros.com.br - * @see org.keycloak.admin.client.KeycloakBuilder - */ -public class Keycloak implements AutoCloseable { - - private static volatile ResteasyClientProvider CLIENT_PROVIDER = resolveResteasyClientProvider(); - - private static ResteasyClientProvider resolveResteasyClientProvider() { - Iterator providers = ServiceLoader.load(ResteasyClientProvider.class).iterator(); - - if (providers.hasNext()) { - ResteasyClientProvider provider = providers.next(); - - if (providers.hasNext()) { - throw new IllegalArgumentException("Multiple " + ResteasyClientProvider.class + " implementations found"); - } - - return provider; - } - - return createDefaultResteasyClientProvider(); - } - - private static ResteasyClientProvider createDefaultResteasyClientProvider() { - try { - return (ResteasyClientProvider) Keycloak.class.getClassLoader().loadClass("org.keycloak.admin.client.spi.ResteasyClientClassicProvider").getDeclaredConstructor().newInstance(); - } catch (Exception cause) { - throw new RuntimeException("Could not instantiate default client provider", cause); - } - } - - public static void setClientProvider(ResteasyClientProvider provider) { - CLIENT_PROVIDER = provider; - } - - public static ResteasyClientProvider getClientProvider() { - return CLIENT_PROVIDER; - } - - private final Config config; - private final TokenManager tokenManager; - private final String authToken; - private final WebTarget target; - private final Client client; - private boolean closed = false; - - Keycloak(String serverUrl, String realm, String username, String password, String clientId, String clientSecret, String grantType, Client resteasyClient, String authtoken, String scope) { - config = new Config(serverUrl, realm, username, password, clientId, clientSecret, grantType, scope); - client = resteasyClient != null ? resteasyClient : newRestEasyClient(null, null, false); - authToken = authtoken; - tokenManager = authtoken == null ? new TokenManager(config, client) : null; - - target = client.target(config.getServerUrl()); - target.register(newAuthFilter()); - } - - private static Client newRestEasyClient(Object customJacksonProvider, SSLContext sslContext, boolean disableTrustManager) { - return CLIENT_PROVIDER.newRestEasyClient(customJacksonProvider, sslContext, disableTrustManager); - } - - private BearerAuthFilter newAuthFilter() { - return authToken != null ? new BearerAuthFilter(authToken) : new BearerAuthFilter(tokenManager); - } - public static Keycloak getInstance(String serverUrl, String realm, String username, String password, String clientId, String clientSecret, SSLContext sslContext, Object customJacksonProvider, boolean disableTrustManager, String authToken, String scope) { - return new Keycloak(serverUrl, realm, username, password, clientId, clientSecret, PASSWORD, newRestEasyClient(customJacksonProvider, sslContext, disableTrustManager), authToken, scope); - } - public static Keycloak getInstance(String serverUrl, String realm, String username, String password, String grantType, String clientId, String clientSecret, SSLContext sslContext, Object customJacksonProvider, boolean disableTrustManager, String authToken, String scope) { - return new Keycloak(serverUrl, realm, username, password, clientId, clientSecret, grantType, newRestEasyClient(customJacksonProvider, sslContext, disableTrustManager), authToken, scope); - } - public static Keycloak getInstance(String serverUrl, String realm, String username, String password, String clientId, String clientSecret, SSLContext sslContext, Object customJacksonProvider, boolean disableTrustManager, String authToken) { - return new Keycloak(serverUrl, realm, username, password, clientId, clientSecret, PASSWORD, newRestEasyClient(customJacksonProvider, sslContext, disableTrustManager), authToken, null); - } - - public static Keycloak getInstance(String serverUrl, String realm, String username, String password, String clientId, String clientSecret) { - return getInstance(serverUrl, realm, username, password, clientId, clientSecret, null, null, false, null); - } - - public static Keycloak getInstance(String serverUrl, String realm, String username, String password, String clientId, String clientSecret, SSLContext sslContext) { - return getInstance(serverUrl, realm, username, password, clientId, clientSecret, sslContext, null, false, null); - } - - public static Keycloak getInstance(String serverUrl, String realm, String username, String password, String clientId, String clientSecret, SSLContext sslContext, Object customJacksonProvider) { - return getInstance(serverUrl, realm, username, password, clientId, clientSecret, sslContext, customJacksonProvider, false, null); - } - - public static Keycloak getInstance(String serverUrl, String realm, String username, String password, String clientId) { - return getInstance(serverUrl, realm, username, password, clientId, null, null, null, false, null); - } - - public static Keycloak getInstance(String serverUrl, String realm, String username, String password, String clientId, SSLContext sslContext) { - return getInstance(serverUrl, realm, username, password, clientId, null, sslContext, null, false, null); - } - - public static Keycloak getInstance(String serverUrl, String realm, String clientId, String authToken) { - return getInstance(serverUrl, realm, null, null, clientId, null, null, null, false, authToken); - } - - public static Keycloak getInstance(String serverUrl, String realm, String clientId, String authToken, SSLContext sllSslContext) { - return getInstance(serverUrl, realm, null, null, clientId, null, sllSslContext, null, false, authToken); - } - - public RealmsResource realms() { - return CLIENT_PROVIDER.targetProxy(target, RealmsResource.class); - } - - public RealmResource realm(String realmName) { - return realms().realm(realmName); - } - - public ServerInfoResource serverInfo() { - return CLIENT_PROVIDER.targetProxy(target, ServerInfoResource.class); - } - - public TokenManager tokenManager() { - return tokenManager; - } - - /** - * Create a secure proxy based on an absolute URI. - * All set up with appropriate token - * - * @param proxyClass - * @param absoluteURI - * @param - * @return - */ - public T proxy(Class proxyClass, URI absoluteURI) { - WebTarget register = client.target(absoluteURI).register(newAuthFilter()); - return CLIENT_PROVIDER.targetProxy(register, proxyClass); - } - - /** - * Closes the underlying client. After calling this method, this Keycloak instance cannot be reused. - */ - @Override - public void close() { - closed = true; - client.close(); - } - - /** - * @return true if the underlying client is closed. - */ - public boolean isClosed() { - return closed; - } -} diff --git a/testsuite/framework/src/main/java/org/keycloak/testsuite/util/AdminClientUtil.java b/testsuite/framework/src/main/java/org/keycloak/testsuite/util/AdminClientUtil.java index 5fe471d..fd0905b 100644 --- a/testsuite/framework/src/main/java/org/keycloak/testsuite/util/AdminClientUtil.java +++ b/testsuite/framework/src/main/java/org/keycloak/testsuite/util/AdminClientUtil.java @@ -17,16 +17,21 @@ package org.keycloak.testsuite.util; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.http.HttpHost; import org.apache.http.client.config.RequestConfig; import org.apache.http.conn.HttpClientConnectionManager; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; import org.jboss.resteasy.client.jaxrs.ClientHttpEngine; +import org.jboss.resteasy.client.jaxrs.ResteasyClient; import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder; import org.jboss.resteasy.client.jaxrs.engines.ClientHttpEngineBuilder43; +import org.jboss.resteasy.plugins.providers.jackson.ResteasyJackson2Provider; import org.keycloak.OAuth2Constants; -import org.keycloak.models.Constants; -import org.keycloak.testsuite.client.Keycloak; +import org.keycloak.admin.client.Keycloak; +import org.keycloak.admin.client.KeycloakBuilder; +import org.keycloak.client.testsuite.models.Constants; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLContext; @@ -36,7 +41,6 @@ import java.security.GeneralSecurityException; import java.security.KeyStore; import java.security.SecureRandom; - import static org.keycloak.testsuite.util.ServerURLs.getAuthServerContextRoot; @@ -65,7 +69,18 @@ public static Keycloak createAdminClient(boolean ignoreUnknownProperties, String } public static Keycloak createAdminClientWithClientCredentials(String realmName, String clientId, String clientSecret, String scope) { - return Keycloak.getInstance(getAuthServerContextRoot(), realmName, null, null, OAuth2Constants.CLIENT_CREDENTIALS, clientId, clientSecret, buildSslContext(), null, false, null, scope); + + boolean ignoreUnknownProperties = false; + ResteasyClient resteasyClient = createResteasyClient(ignoreUnknownProperties, null); + + return KeycloakBuilder.builder() + .serverUrl(getAuthServerContextRoot()) + .realm(realmName) + .grantType(OAuth2Constants.CLIENT_CREDENTIALS) + .clientId(clientId) + .clientSecret(clientSecret) + .resteasyClient(resteasyClient) + .scope(scope).build(); } public static Keycloak createAdminClient() throws Exception { @@ -76,6 +91,38 @@ public static Keycloak createAdminClient(boolean ignoreUnknownProperties) throws return createAdminClient(ignoreUnknownProperties, getAuthServerContextRoot()); } + public static ResteasyClient createResteasyClient() { + try { + return createResteasyClient(false, null); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public static ResteasyClient createResteasyClient(boolean ignoreUnknownProperties, Boolean followRedirects) { + ResteasyClientBuilder resteasyClientBuilder = (ResteasyClientBuilder) ResteasyClientBuilder.newBuilder(); + resteasyClientBuilder.sslContext(buildSslContext()); + + // We need to ignore unknown JSON properties e.g. in the adapter configuration representation + // during adapter backward compatibility testing + if (ignoreUnknownProperties) { + // We need to use anonymous class to avoid the following error from RESTEasy: + // Provider class org.jboss.resteasy.plugins.providers.jackson.ResteasyJackson2Provider is already registered. 2nd registration is being ignored. + ResteasyJackson2Provider jacksonProvider = new ResteasyJackson2Provider() {}; + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + jacksonProvider.setMapper(objectMapper); + resteasyClientBuilder.register(jacksonProvider, 100); + } + + resteasyClientBuilder + .hostnameVerification(ResteasyClientBuilder.HostnameVerificationPolicy.WILDCARD) + .connectionPoolSize(NUMBER_OF_CONNECTIONS) + .httpEngine(getCustomClientHttpEngine(resteasyClientBuilder, 1, followRedirects)); + + return resteasyClientBuilder.build(); + } + private static SSLContext buildSslContext() { SSLContext sslContext; try {