From 209eac32e744443b8517b699404bea56c7229301 Mon Sep 17 00:00:00 2001 From: Ramon Spahr Date: Fri, 3 Jul 2020 07:52:58 +0200 Subject: [PATCH] improve identity provider handling --- .editorconfig | 3 +- CHANGELOG.md | 4 + docs/FEATURES.md | 7 +- docs/MANAGED.md | 20 +- .../properties/ImportConfigProperties.java | 15 +- .../IdentityProviderMapperRepository.java | 91 +++++ .../IdentityProviderRepository.java | 40 +- .../IdentityProviderImportService.java | 121 +++++- src/main/resources/application.properties | 2 + .../config/ImportIdentityProvidersIT.java | 353 ++++++++++++++++++ .../config/ImportManagedNoDeleteIT.java | 16 + .../ImportConfigPropertiesTest.java | 4 + .../config/test/util/KeycloakVersion.java | 36 ++ ...e_identity-provider_for_keycloak-oidc.json | 31 ++ ...e_identity-provider_for_keycloak-oidc.json | 31 ++ ...rovider_for_keycloak-oidc_with_mapper.json | 43 +++ ...for_keycloak-oidc_with_updated_mapper.json | 43 +++ ...or_keycloak-oidc_with_replaced_mapper.json | 43 +++ ...for_keycloak-oidc_with_deleted_mapper.json | 34 ++ ...e_identity-provider_for_keycloak-oidc.json | 5 + .../0_create_simple-realm.json | 76 +++- .../1_update-realm_not-delete-one.json | 41 +- .../2_update-realm_not-delete-all.json | 4 +- 23 files changed, 1034 insertions(+), 29 deletions(-) create mode 100644 src/main/java/de/adorsys/keycloak/config/repository/IdentityProviderMapperRepository.java create mode 100644 src/test/java/de/adorsys/keycloak/config/test/util/KeycloakVersion.java create mode 100644 src/test/resources/import-files/identity-providers/3_create_identity-provider_for_keycloak-oidc.json create mode 100644 src/test/resources/import-files/identity-providers/4_update_identity-provider_for_keycloak-oidc.json create mode 100644 src/test/resources/import-files/identity-providers/5_update_identity-provider_for_keycloak-oidc_with_mapper.json create mode 100644 src/test/resources/import-files/identity-providers/6_update_identity-provider_for_keycloak-oidc_with_updated_mapper.json create mode 100644 src/test/resources/import-files/identity-providers/7_update_identity-provider_for_keycloak-oidc_with_replaced_mapper.json create mode 100644 src/test/resources/import-files/identity-providers/8_update_identity-provider_for_keycloak-oidc_with_deleted_mapper.json create mode 100644 src/test/resources/import-files/identity-providers/9_delete_identity-provider_for_keycloak-oidc.json diff --git a/.editorconfig b/.editorconfig index 6473d702a..4a7d643ec 100644 --- a/.editorconfig +++ b/.editorconfig @@ -9,5 +9,6 @@ indent_style = space [*.{json, yaml, xml}] indent_size = 2 -[*.xml] +[*.{xml, java}] indent_size = 4 +continuation_indent_size = 8 diff --git a/CHANGELOG.md b/CHANGELOG.md index aa493007d..20b8501f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Create, Update, Delete IdentityProviderMappers +- Support for only updating changed IdentityProviders +- Support for managed IdentityProviders + ### Changed ### Fixed diff --git a/docs/FEATURES.md b/docs/FEATURES.md index 6f171b08b..738f73b64 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -40,7 +40,12 @@ | Add required-actions | 1.0.0 | Add required-actions while creating or updating realms | | Update required-actions | 1.0.0 | Update properties of existing required-actions while updating realms | | Remove required-actions | 2.0.0 | Remove existing required-actions while updating realms | -| Update identity providers | 1.2.0 | Update properties of existing identity providers while updating realms | +| Add identity providers | 1.2.0 | Add identity providers while creating or updating realms | +| Update identity providers | 1.2.0 | Update identity providers while updating realms (improved with 2.0.0) | +| Remove identity providers | 2.0.0 | Remove identity providers while updating realms | +| Add identity provider mappers | 2.0.0 | Add identityProviderMappers while updating realms | +| Update identity provider mappers | 2.0.0 | Update identityProviderMappers while updating realms | +| Remove identity provider mappers | 2.0.0 | Remove identityProviderMappers while updating realms | | Add clientScopes | 2.0.0 | Add clientScopes (inclusive protocolMappers) while creating or updating realms | | Update clientScopes | 2.0.0 | Update existing (inclusive protocolMappers) clientScopes while creating or updating realms | | Remove clientScopes | 2.0.0 | Remove existing clientScopes while creating or updating realms | diff --git a/docs/MANAGED.md b/docs/MANAGED.md index 688114560..149019d50 100644 --- a/docs/MANAGED.md +++ b/docs/MANAGED.md @@ -24,15 +24,17 @@ For example if you define `groups` but set an empty array, keycloak will delete ## Supported full managed entities -| Type | Additional Information | -| -------------------- | -------------------------------------------------------------------------------- | -| Groups | - | -| Required Actions | You have to copy the default one to you import json. | -| Client Scopes | - | -| Scope Mappings | - | -| Components | You have to copy the default components to you import json. | -| Sub Components | You have to copy the default components to you import json. | -| Authentication Flows | You have to copy the default components to you import json, expect bulitin flows | +| Type | Additional Information | Property Name | +| --------------------------- | -------------------------------------------------------------------------------- | --------------------------- | +| Groups | - | `group` | +| Required Actions | You have to copy the default one to you import json. | `required-action` | +| Client Scopes | - | `client-scope` | +| Scope Mappings | - | `scope-mapping` | +| Components | You have to copy the default components to you import json. | `component` | +| Sub Components | You have to copy the default components to you import json. | `sub-component` | +| Authentication Flows | You have to copy the default components to you import json, expect bulitin flows | `authentication-flow` | +| Identity Providers | - | `identity-provider` | +| Identity Provider Mappers | - | `identity-provider-mapper` | ## Disable deletion of managed entities diff --git a/src/main/java/de/adorsys/keycloak/config/properties/ImportConfigProperties.java b/src/main/java/de/adorsys/keycloak/config/properties/ImportConfigProperties.java index 9878334c2..25e1ec944 100644 --- a/src/main/java/de/adorsys/keycloak/config/properties/ImportConfigProperties.java +++ b/src/main/java/de/adorsys/keycloak/config/properties/ImportConfigProperties.java @@ -119,11 +119,18 @@ public static class ImportManagedProperties { @NotNull private final ImportManagedPropertiesValues authenticationFlow; + @NotNull + private final ImportManagedPropertiesValues identityProvider; + + @NotNull + private final ImportManagedPropertiesValues identityProviderMapper; + public ImportManagedProperties( ImportManagedPropertiesValues requiredAction, ImportManagedPropertiesValues group, ImportManagedPropertiesValues clientScope, ImportManagedPropertiesValues scopeMapping, ImportManagedPropertiesValues component, ImportManagedPropertiesValues subComponent, - ImportManagedPropertiesValues authenticationFlow) { + ImportManagedPropertiesValues authenticationFlow, ImportManagedPropertiesValues identityProvider, + ImportManagedPropertiesValues identityProviderMapper) { this.requiredAction = requiredAction; this.group = group; this.clientScope = clientScope; @@ -131,6 +138,8 @@ public ImportManagedProperties( this.component = component; this.subComponent = subComponent; this.authenticationFlow = authenticationFlow; + this.identityProvider = identityProvider; + this.identityProviderMapper = identityProviderMapper; } public ImportManagedPropertiesValues getRequiredAction() { @@ -161,6 +170,10 @@ public ImportManagedPropertiesValues getGroup() { return group; } + public ImportManagedPropertiesValues getIdentityProvider() { return identityProvider; } + + public ImportManagedPropertiesValues getIdentityProviderMapper() { return identityProviderMapper; } + public enum ImportManagedPropertiesValues { FULL, NO_DELETE diff --git a/src/main/java/de/adorsys/keycloak/config/repository/IdentityProviderMapperRepository.java b/src/main/java/de/adorsys/keycloak/config/repository/IdentityProviderMapperRepository.java new file mode 100644 index 000000000..a57b72c60 --- /dev/null +++ b/src/main/java/de/adorsys/keycloak/config/repository/IdentityProviderMapperRepository.java @@ -0,0 +1,91 @@ +/*- + * ---license-start + * keycloak-config-cli + * --- + * Copyright (C) 2017 - 2020 adorsys GmbH & Co. KG @ https://adorsys.de + * --- + * 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. + * ---license-end + */ + +package de.adorsys.keycloak.config.repository; + +import de.adorsys.keycloak.config.util.ResponseUtil; +import de.adorsys.keycloak.config.util.StreamUtil; +import org.keycloak.admin.client.resource.IdentityProvidersResource; +import org.keycloak.representations.idm.IdentityProviderMapperRepresentation; +import org.keycloak.representations.idm.IdentityProviderRepresentation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.ws.rs.core.Response; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +@Service +public class IdentityProviderMapperRepository { + + private final RealmRepository realmRepository; + + @Autowired + public IdentityProviderMapperRepository(RealmRepository realmRepository) { + this.realmRepository = realmRepository; + } + + public Optional tryToFindIdentityProviderMapper(String realm, String identityProviderAlias, String name) { + return loadIdentityProviderMapperByName(realm, identityProviderAlias, name); + } + + public IdentityProviderMapperRepresentation getIdentityProviderMapperByName(String realm, String identityProviderAlias, String name) { + Optional maybeIdentityProviderMapper = loadIdentityProviderMapperByName(realm, identityProviderAlias, name); + + return maybeIdentityProviderMapper.orElse(null); + } + + public List getIdentityProviderMappers(String realm) { + List mappers = new ArrayList<>(); + IdentityProvidersResource identityProvidersResource = realmRepository.loadRealm(realm).identityProviders(); + List identityProviders = identityProvidersResource.findAll(); + + for(IdentityProviderRepresentation identityProvider : identityProviders) { + mappers.addAll(identityProvidersResource.get(identityProvider.getAlias()).getMappers()); + } + return mappers; + } + + public void createIdentityProviderMapper(String realm, IdentityProviderMapperRepresentation identityProviderMapper) { + IdentityProvidersResource identityProvidersResource = realmRepository.loadRealm(realm).identityProviders(); + + Response response = identityProvidersResource.get(identityProviderMapper.getIdentityProviderAlias()).addMapper(identityProviderMapper); + ResponseUtil.throwOnError(response); + } + + public void updateIdentityProviderMapper(String realm, IdentityProviderMapperRepresentation identityProviderMapperToUpdate) { + IdentityProvidersResource identityProvidersResource = realmRepository.loadRealm(realm).identityProviders(); + + identityProvidersResource.get(identityProviderMapperToUpdate.getIdentityProviderAlias()).update(identityProviderMapperToUpdate.getId(), identityProviderMapperToUpdate); + } + + public void deleteIdentityProviderMapper(String realm, IdentityProviderMapperRepresentation identityProviderMapperToDelete) { + IdentityProvidersResource identityProvidersResource = realmRepository.loadRealm(realm).identityProviders(); + String identityProviderAlias = identityProviderMapperToDelete.getIdentityProviderAlias(); + + identityProvidersResource.get(identityProviderAlias).delete(identityProviderMapperToDelete.getId()); + } + + private Optional loadIdentityProviderMapperByName(String realm, String identityProviderAlias, String name) { + return StreamUtil.collectionAsStream(realmRepository.get(realm).getIdentityProviderMappers()).filter(m -> m.getName().equals(name) && m.getIdentityProviderAlias().equals(identityProviderAlias)).findFirst(); + } + +} diff --git a/src/main/java/de/adorsys/keycloak/config/repository/IdentityProviderRepository.java b/src/main/java/de/adorsys/keycloak/config/repository/IdentityProviderRepository.java index dca25ab57..0c94ea7bb 100644 --- a/src/main/java/de/adorsys/keycloak/config/repository/IdentityProviderRepository.java +++ b/src/main/java/de/adorsys/keycloak/config/repository/IdentityProviderRepository.java @@ -20,6 +20,7 @@ package de.adorsys.keycloak.config.repository; +import de.adorsys.keycloak.config.util.ResponseUtil; import org.keycloak.admin.client.resource.IdentityProviderResource; import org.keycloak.admin.client.resource.IdentityProvidersResource; import org.keycloak.representations.idm.IdentityProviderRepresentation; @@ -27,6 +28,8 @@ import org.springframework.stereotype.Service; import javax.ws.rs.NotFoundException; +import javax.ws.rs.core.Response; +import java.util.List; import java.util.Optional; @Service @@ -35,17 +38,14 @@ public class IdentityProviderRepository { private final RealmRepository realmRepository; @Autowired - public IdentityProviderRepository( - RealmRepository realmRepository - ) { + public IdentityProviderRepository(RealmRepository realmRepository) { this.realmRepository = realmRepository; } - public Optional tryToFindIdentityProvider(String realm, String name) { + public Optional tryToFindIdentityProvider(String realm, String alias) { Optional maybeIdentityProvider; - IdentityProvidersResource identityProvidersResource = realmRepository.loadRealm(realm).identityProviders(); - IdentityProviderResource identityProviderResource = identityProvidersResource.get(name); + IdentityProviderResource identityProviderResource = loadIdentityProviderByAlias(realm, alias); try { maybeIdentityProvider = Optional.of(identityProviderResource.toRepresentation()); @@ -56,9 +56,22 @@ public Optional tryToFindIdentityProvider(String return maybeIdentityProvider; } + public IdentityProviderRepresentation getIdentityProviderByAlias(String realm, String alias) { + IdentityProviderResource identityProviderResource = loadIdentityProviderByAlias(realm, alias); + if (identityProviderResource == null) { + return null; + } + return identityProviderResource.toRepresentation(); + } + + public List getIdentityProviders(String realm) { + return realmRepository.loadRealm(realm).identityProviders().findAll(); + } + public void createIdentityProvider(String realm, IdentityProviderRepresentation identityProvider) { IdentityProvidersResource identityProvidersResource = realmRepository.loadRealm(realm).identityProviders(); - identityProvidersResource.create(identityProvider); + Response response = identityProvidersResource.create(identityProvider); + ResponseUtil.throwOnError(response); } public void updateIdentityProvider(String realm, IdentityProviderRepresentation identityProviderToUpdate) { @@ -68,4 +81,17 @@ public void updateIdentityProvider(String realm, IdentityProviderRepresentation identityProviderResource.update(identityProviderToUpdate); } + + public void deleteIdentityProvider(String realm, IdentityProviderRepresentation identityProviderToDelete) { + IdentityProviderResource identityProviderResource = realmRepository.loadRealm(realm) + .identityProviders() + .get(identityProviderToDelete.getInternalId()); + + identityProviderResource.remove(); + } + + private IdentityProviderResource loadIdentityProviderByAlias(String realm, String identityProviderAlias) { + return realmRepository.loadRealm(realm) + .identityProviders().get(identityProviderAlias); + } } diff --git a/src/main/java/de/adorsys/keycloak/config/service/IdentityProviderImportService.java b/src/main/java/de/adorsys/keycloak/config/service/IdentityProviderImportService.java index c984b7a76..03503de74 100644 --- a/src/main/java/de/adorsys/keycloak/config/service/IdentityProviderImportService.java +++ b/src/main/java/de/adorsys/keycloak/config/service/IdentityProviderImportService.java @@ -21,7 +21,11 @@ package de.adorsys.keycloak.config.service; import de.adorsys.keycloak.config.model.RealmImport; +import de.adorsys.keycloak.config.properties.ImportConfigProperties; +import de.adorsys.keycloak.config.repository.IdentityProviderMapperRepository; import de.adorsys.keycloak.config.repository.IdentityProviderRepository; +import de.adorsys.keycloak.config.util.CloneUtil; +import org.keycloak.representations.idm.IdentityProviderMapperRepresentation; import org.keycloak.representations.idm.IdentityProviderRepresentation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -29,6 +33,7 @@ import org.springframework.stereotype.Service; import java.util.List; +import java.util.Objects; import java.util.Optional; @Service @@ -36,27 +41,46 @@ public class IdentityProviderImportService { private static final Logger logger = LoggerFactory.getLogger(IdentityProviderImportService.class); private final IdentityProviderRepository identityProviderRepository; + private final IdentityProviderMapperRepository identityProviderMapperRepository; + private final ImportConfigProperties importConfigProperties; @Autowired - public IdentityProviderImportService( - IdentityProviderRepository identityProviderRepository - ) { + public IdentityProviderImportService(IdentityProviderRepository identityProviderRepository, IdentityProviderMapperRepository identityProviderMapperRepository, ImportConfigProperties importConfigProperties) { this.identityProviderRepository = identityProviderRepository; + this.identityProviderMapperRepository = identityProviderMapperRepository; + this.importConfigProperties = importConfigProperties; } public void doImport(RealmImport realmImport) { - createOrUpdateIdentityProviders(realmImport); + createOrUpdateOrDeleteIdentityProviders(realmImport); + createOrUpdateOrDeleteIdentityProviderMappers(realmImport); } - private void createOrUpdateIdentityProviders(RealmImport realmImport) { + private void createOrUpdateOrDeleteIdentityProviders(RealmImport realmImport) { + String realm = realmImport.getRealm(); List identityProviders = realmImport.getIdentityProviders(); + List existingIdentityProviders = identityProviderRepository.getIdentityProviders(realm); + if (identityProviders == null) return; + if (importConfigProperties.getManaged().getIdentityProvider() == ImportConfigProperties.ImportManagedProperties.ImportManagedPropertiesValues.FULL) { + deleteIdentityProvidersMissingInImport(realm, identityProviders, existingIdentityProviders); + } + for (IdentityProviderRepresentation identityProvider : identityProviders) { createOrUpdateIdentityProvider(realmImport, identityProvider); } } + private void deleteIdentityProvidersMissingInImport(String realm, List identityProviders, List existingIdentityProviders) { + for (IdentityProviderRepresentation identityProvider : existingIdentityProviders) { + if(!hasIdentityProviderWithAlias(identityProviders, identityProvider.getAlias())) { + logger.debug("Delete identityProvider '{}' in realm '{}'", identityProvider.getAlias(), realm); + identityProviderRepository.deleteIdentityProvider(realm, identityProvider); + } + } + } + private void createOrUpdateIdentityProvider(RealmImport realmImport, IdentityProviderRepresentation identityProvider) { String identityProviderName = identityProvider.getAlias(); String realm = realmImport.getRealm(); @@ -64,11 +88,94 @@ private void createOrUpdateIdentityProvider(RealmImport realmImport, IdentityPro Optional maybeIdentityProvider = identityProviderRepository.tryToFindIdentityProvider(realm, identityProviderName); if (maybeIdentityProvider.isPresent()) { - logger.debug("Update identityProvider '{}' in realm '{}'", identityProviderName, realm); - identityProviderRepository.updateIdentityProvider(realm, identityProvider); + updateIdentityProviderIfNecessary(realm, identityProvider); } else { logger.debug("Create identityProvider '{}' in realm '{}'", identityProviderName, realm); identityProviderRepository.createIdentityProvider(realm, identityProvider); } } + + private void updateIdentityProviderIfNecessary(String realm, IdentityProviderRepresentation identityProvider) { + IdentityProviderRepresentation existingIdentityProvider = identityProviderRepository.getIdentityProviderByAlias(realm, identityProvider.getAlias()); + IdentityProviderRepresentation patchedIdentityProvider = CloneUtil.patch(existingIdentityProvider, identityProvider); + String identityProviderAlias = existingIdentityProvider.getAlias(); + + if (isIdentityProviderEqual(existingIdentityProvider, patchedIdentityProvider)) { + logger.debug("No need to update identityProvider '{}' in realm '{}'", identityProviderAlias, realm); + } else { + logger.debug("Update identityProvider '{}' in realm '{}'", identityProviderAlias, realm); + identityProviderRepository.updateIdentityProvider(realm, patchedIdentityProvider); + } + } + + private boolean isIdentityProviderEqual(IdentityProviderRepresentation existingIdentityProvider, IdentityProviderRepresentation patchedIdentityProvider) { + return CloneUtil.deepEquals(existingIdentityProvider, patchedIdentityProvider); + } + + private boolean hasIdentityProviderWithAlias(List identityProviders, String identityProviderAlias) { + return identityProviders.stream().anyMatch(idp -> Objects.equals(idp.getAlias(), identityProviderAlias)); + } + + private void createOrUpdateOrDeleteIdentityProviderMappers(RealmImport realmImport) { + String realm = realmImport.getRealm(); + List identityProviderMappers = realmImport.getIdentityProviderMappers(); + List existingIdentityProviderMappers = identityProviderMapperRepository.getIdentityProviderMappers(realm); + + if (identityProviderMappers == null) return; + + if (importConfigProperties.getManaged().getIdentityProviderMapper() == ImportConfigProperties.ImportManagedProperties.ImportManagedPropertiesValues.FULL) { + deleteIdentityProviderMappersMissingInImport(realm, identityProviderMappers, existingIdentityProviderMappers); + } + + for (IdentityProviderMapperRepresentation identityProviderMapper : identityProviderMappers) { + createOrUpdateIdentityProviderMapper(realmImport, identityProviderMapper); + } + } + + private void createOrUpdateIdentityProviderMapper(RealmImport realmImport, IdentityProviderMapperRepresentation identityProviderMapper) { + String identityProviderMapperName = identityProviderMapper.getName(); + String realm = realmImport.getRealm(); + + Optional maybeIdentityProviderMapper = identityProviderMapperRepository.tryToFindIdentityProviderMapper(realm, identityProviderMapper.getIdentityProviderAlias(), identityProviderMapperName); + + if (maybeIdentityProviderMapper.isPresent()) { + updateIdentityProviderMapperIfNecessary(realm, identityProviderMapper); + } else { + logger.debug("Create identityProviderMapper '{}' in realm '{}'", identityProviderMapperName, realm); + identityProviderMapperRepository.createIdentityProviderMapper(realm, identityProviderMapper); + } + } + + private void updateIdentityProviderMapperIfNecessary(String realm, IdentityProviderMapperRepresentation identityProviderMapper) { + IdentityProviderMapperRepresentation existingIdentityProviderMapper = identityProviderMapperRepository.getIdentityProviderMapperByName(realm, identityProviderMapper.getIdentityProviderAlias(), identityProviderMapper.getName()); + IdentityProviderMapperRepresentation patchedIdentityProviderMapper = CloneUtil.patch(existingIdentityProviderMapper, identityProviderMapper); + String identityProviderMapperName = existingIdentityProviderMapper.getName(); + String identityProviderAlias = existingIdentityProviderMapper.getIdentityProviderAlias(); + + if (isIdentityProviderMapperEqual(existingIdentityProviderMapper, patchedIdentityProviderMapper)) { + logger.debug("No need to update identityProviderMapper for identityProvider '{}' in realm '{}' in realm '{}'", identityProviderMapperName, identityProviderAlias, realm); + } else { + logger.debug("Update identityProviderMapper '{}' for identityProvider '{}' in realm '{}'", identityProviderMapperName, identityProviderAlias, realm); + identityProviderMapperRepository.updateIdentityProviderMapper(realm, patchedIdentityProviderMapper); + } + } + + private boolean isIdentityProviderMapperEqual(IdentityProviderMapperRepresentation existingIdentityProviderMapper, IdentityProviderMapperRepresentation patchedIdentityProviderMapper) { + return CloneUtil.deepEquals(existingIdentityProviderMapper, patchedIdentityProviderMapper); + } + + + private void deleteIdentityProviderMappersMissingInImport(String realm, List identityProviderMappers, List existingIdentityProviderMappers) { + for (IdentityProviderMapperRepresentation identityProviderMapper : existingIdentityProviderMappers) { + if(!hasIdentityProviderMapperWithNameForAlias(identityProviderMappers, identityProviderMapper)) { + logger.debug("Delete identityProviderMapper '{}' in realm '{}'", identityProviderMapper.getName(), realm); + identityProviderMapperRepository.deleteIdentityProviderMapper(realm, identityProviderMapper); + } + } + } + + private boolean hasIdentityProviderMapperWithNameForAlias(List identityProviderMappers, IdentityProviderMapperRepresentation identityProviderMapper) { + return identityProviderMappers.stream().anyMatch(idpm -> Objects.equals(idpm.getName(), identityProviderMapper.getName()) && Objects.equals(idpm.getIdentityProviderAlias(), identityProviderMapper.getIdentityProviderAlias())); + } + } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index a33efd90a..e6e8e4d96 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -24,3 +24,5 @@ import.managed.client-scope=full import.managed.scope-mapping=full import.managed.component=full import.managed.sub-component=full +import.managed.identity-provider=full +import.managed.identity-provider-mapper=full diff --git a/src/test/java/de/adorsys/keycloak/config/ImportIdentityProvidersIT.java b/src/test/java/de/adorsys/keycloak/config/ImportIdentityProvidersIT.java index 54d4a66f9..429d64a6f 100644 --- a/src/test/java/de/adorsys/keycloak/config/ImportIdentityProvidersIT.java +++ b/src/test/java/de/adorsys/keycloak/config/ImportIdentityProvidersIT.java @@ -20,15 +20,19 @@ package de.adorsys.keycloak.config; +import de.adorsys.keycloak.config.test.util.KeycloakVersion; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; +import org.keycloak.representations.idm.IdentityProviderMapperRepresentation; import org.keycloak.representations.idm.IdentityProviderRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import java.util.List; import java.util.Map; +import java.util.Optional; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.core.Is.is; import static org.hamcrest.core.IsNull.nullValue; @@ -209,4 +213,353 @@ void shouldCreateOtherIdentityProvider() { assertThat(createdIdentityProviderConfig.get("encryptionPublicKey"), is("MIIDEjCCAfqgAwIBAgIVAPVbodo8Su7/BaHXUHykx0Pi5CFaMA0GCSqGSIb3DQEB\nCwUAMBYxFDASBgNVBAMMC3NhbWx0ZXN0LmlkMB4XDTE4MDgyNDIxMTQwOVoXDTM4\nMDgyNDIxMTQwOVowFjEUMBIGA1UEAwwLc2FtbHRlc3QuaWQwggEiMA0GCSqGSIb3\nDQEBAQUAA4IBDwAwggEKAoIBAQCQb+1a7uDdTTBBFfwOUun3IQ9nEuKM98SmJDWa\nMwM877elswKUTIBVh5gB2RIXAPZt7J/KGqypmgw9UNXFnoslpeZbA9fcAqqu28Z4\nsSb2YSajV1ZgEYPUKvXwQEmLWN6aDhkn8HnEZNrmeXihTFdyr7wjsLj0JpQ+VUlc\n4/J+hNuU7rGYZ1rKY8AA34qDVd4DiJ+DXW2PESfOu8lJSOteEaNtbmnvH8KlwkDs\n1NvPTsI0W/m4SK0UdXo6LLaV8saIpJfnkVC/FwpBolBrRC/Em64UlBsRZm2T89ca\nuzDee2yPUvbBd5kLErw+sC7i4xXa2rGmsQLYcBPhsRwnmBmlAgMBAAGjVzBVMB0G\nA1UdDgQWBBRZ3exEu6rCwRe5C7f5QrPcAKRPUjA0BgNVHREELTArggtzYW1sdGVz\ndC5pZIYcaHR0cHM6Ly9zYW1sdGVzdC5pZC9zYW1sL2lkcDANBgkqhkiG9w0BAQsF\nAAOCAQEABZDFRNtcbvIRmblnZItoWCFhVUlq81ceSQddLYs8DqK340//hWNAbYdj\nWcP85HhIZnrw6NGCO4bUipxZXhiqTA/A9d1BUll0vYB8qckYDEdPDduYCOYemKkD\ndmnHMQWs9Y6zWiYuNKEJ9mf3+1N8knN/PK0TYVjVjXAf2CnOETDbLtlj6Nqb8La3\nsQkYmU+aUdopbjd5JFFwbZRaj6KiHXHtnIRgu8sUXNPrgipUgZUOVhP0C0N5OfE4\nJW8ZBrKgQC/6vJ2rSa9TlzI6JAa5Ww7gMXMP9M+cJUNQklcq+SBnTK8G+uBHgPKR\nzBDsMIEzRtQZm4GIoHJae4zmnCekkQ==")); assertThat(createdIdentityProviderConfig.get("principalType"), is("SUBJECT")); } + + @Test + @Order(3) + void shouldCreateOidcIdentityProvider() { + doImport("3_create_identity-provider_for_keycloak-oidc.json"); + + RealmRepresentation createdRealm = keycloakProvider.get().realm(REALM_NAME).toRepresentation(); + + assertThat(createdRealm.getRealm(), is(REALM_NAME)); + assertThat(createdRealm.isEnabled(), is(true)); + + List identityProviders = createdRealm.getIdentityProviders(); + assertThat(identityProviders.size(), is(1)); + + IdentityProviderRepresentation oidcIdentityProvider = identityProviders.stream() + .filter(e -> e.getAlias().equals("keycloak-oidc")) + .findFirst() + .orElse(null); + + assertThat(oidcIdentityProvider, notNullValue()); + assertThat(oidcIdentityProvider.getAlias(), is("keycloak-oidc")); + assertThat(oidcIdentityProvider.getProviderId(), is("keycloak-oidc")); + assertThat(oidcIdentityProvider.getDisplayName(), is("my-keycloak-oidc")); + assertThat(oidcIdentityProvider.isEnabled(), is(true)); + assertThat(oidcIdentityProvider.isTrustEmail(), is(true)); + assertThat(oidcIdentityProvider.isStoreToken(), is(false)); + assertThat(oidcIdentityProvider.isAddReadTokenRoleOnCreate(), is(false)); + assertThat(oidcIdentityProvider.isLinkOnly(), is(false)); + assertThat(oidcIdentityProvider.getFirstBrokerLoginFlowAlias(), is("first broker login")); + + Map updatedIdentityProviderConfig = oidcIdentityProvider.getConfig(); + + assertThat(updatedIdentityProviderConfig.get("tokenUrl"), is("https://example.com/protocol/openid-connect/token")); + assertThat(updatedIdentityProviderConfig.get("authorizationUrl"), is("https://example.com/protocol/openid-connect/auth")); + assertThat(updatedIdentityProviderConfig.get("clientAuthMethod"), is("client_secret_post")); + assertThat(updatedIdentityProviderConfig.get("logoutUrl"), is("https://example.com/protocol/openid-connect/logout")); + assertThat(updatedIdentityProviderConfig.get("syncMode"), is("FORCE")); + assertThat(updatedIdentityProviderConfig.get("clientId"), is("example-client-id")); + assertThat(updatedIdentityProviderConfig.get("clientSecret"), is("example-client-secret")); + assertThat(updatedIdentityProviderConfig.get("backchannelSupported"), is("true")); + if(!KeycloakVersion.isKeycloak8()) { + assertThat(updatedIdentityProviderConfig.get("defaultScope"), is(nullValue())); + } else { + assertThat(updatedIdentityProviderConfig.get("defaultScope"), is("")); + } + assertThat(updatedIdentityProviderConfig.get("guiOrder"), is("0")); + assertThat(updatedIdentityProviderConfig.get("useJwksUrl"), is("true")); + } + + @Test + @Order(4) + void shouldUpdateOidcIdentityProvider() { + doImport("4_update_identity-provider_for_keycloak-oidc.json"); + + RealmRepresentation createdRealm = keycloakProvider.get().realm(REALM_NAME).toRepresentation(); + + assertThat(createdRealm.getRealm(), is(REALM_NAME)); + assertThat(createdRealm.isEnabled(), is(true)); + + List identityProviders = createdRealm.getIdentityProviders(); + assertThat(identityProviders.size(), is(1)); + + IdentityProviderRepresentation oidcIdentityProvider = identityProviders.stream() + .filter(e -> e.getAlias().equals("keycloak-oidc")) + .findFirst() + .orElse(null); + + assertThat(oidcIdentityProvider, notNullValue()); + assertThat(oidcIdentityProvider.getAlias(), is("keycloak-oidc")); + assertThat(oidcIdentityProvider.getProviderId(), is("keycloak-oidc")); + assertThat(oidcIdentityProvider.getDisplayName(), is("changed-my-keycloak-oidc")); + assertThat(oidcIdentityProvider.isEnabled(), is(true)); + assertThat(oidcIdentityProvider.isTrustEmail(), is(true)); + assertThat(oidcIdentityProvider.isStoreToken(), is(false)); + assertThat(oidcIdentityProvider.isAddReadTokenRoleOnCreate(), is(false)); + assertThat(oidcIdentityProvider.isLinkOnly(), is(false)); + assertThat(oidcIdentityProvider.getFirstBrokerLoginFlowAlias(), is("first broker login")); + + Map updatedIdentityProviderConfig = oidcIdentityProvider.getConfig(); + + assertThat(updatedIdentityProviderConfig.get("tokenUrl"), is("https://example.com/protocol/openid-connect/token")); + assertThat(updatedIdentityProviderConfig.get("authorizationUrl"), is("https://example.com/protocol/openid-connect/auth")); + assertThat(updatedIdentityProviderConfig.get("clientAuthMethod"), is("client_secret_post")); + assertThat(updatedIdentityProviderConfig.get("logoutUrl"), is("https://example.com/protocol/openid-connect/logout")); + assertThat(updatedIdentityProviderConfig.get("syncMode"), is("FORCE")); + assertThat(updatedIdentityProviderConfig.get("clientId"), is("changed-example-client-id")); + assertThat(updatedIdentityProviderConfig.get("clientSecret"), is("example-client-secret")); + assertThat(updatedIdentityProviderConfig.get("backchannelSupported"), is("true")); + if(!KeycloakVersion.isKeycloak8()) { + assertThat(updatedIdentityProviderConfig.get("defaultScope"), is(nullValue())); + } else { + assertThat(updatedIdentityProviderConfig.get("defaultScope"), is("")); + } + assertThat(updatedIdentityProviderConfig.get("guiOrder"), is("0")); + assertThat(updatedIdentityProviderConfig.get("useJwksUrl"), is("true")); + } + + @Test + @Order(5) + void shouldUpdateOidcIdentityProviderWithMapper() { + doImport("5_update_identity-provider_for_keycloak-oidc_with_mapper.json"); + + RealmRepresentation createdRealm = keycloakProvider.get().realm(REALM_NAME).toRepresentation(); + + assertThat(createdRealm.getRealm(), is(REALM_NAME)); + assertThat(createdRealm.isEnabled(), is(true)); + + List identityProviders = createdRealm.getIdentityProviders(); + assertThat(identityProviders.size(), is(1)); + + IdentityProviderRepresentation oidcIdentityProvider = identityProviders.stream() + .filter(e -> e.getAlias().equals("keycloak-oidc")) + .findFirst() + .orElse(null); + + assertThat(oidcIdentityProvider, notNullValue()); + assertThat(oidcIdentityProvider.getAlias(), is("keycloak-oidc")); + assertThat(oidcIdentityProvider.getProviderId(), is("keycloak-oidc")); + assertThat(oidcIdentityProvider.getDisplayName(), is("my-keycloak-oidc")); + assertThat(oidcIdentityProvider.isEnabled(), is(true)); + assertThat(oidcIdentityProvider.isTrustEmail(), is(true)); + assertThat(oidcIdentityProvider.isStoreToken(), is(false)); + assertThat(oidcIdentityProvider.isAddReadTokenRoleOnCreate(), is(false)); + assertThat(oidcIdentityProvider.isLinkOnly(), is(false)); + assertThat(oidcIdentityProvider.getFirstBrokerLoginFlowAlias(), is("first broker login")); + + Map updatedIdentityProviderConfig = oidcIdentityProvider.getConfig(); + + assertThat(updatedIdentityProviderConfig.get("tokenUrl"), is("https://example.com/protocol/openid-connect/token")); + assertThat(updatedIdentityProviderConfig.get("authorizationUrl"), is("https://example.com/protocol/openid-connect/auth")); + assertThat(updatedIdentityProviderConfig.get("clientAuthMethod"), is("client_secret_post")); + assertThat(updatedIdentityProviderConfig.get("logoutUrl"), is("https://example.com/protocol/openid-connect/logout")); + assertThat(updatedIdentityProviderConfig.get("syncMode"), is("FORCE")); + assertThat(updatedIdentityProviderConfig.get("clientId"), is("example-client-id")); + assertThat(updatedIdentityProviderConfig.get("clientSecret"), is("example-client-secret")); + assertThat(updatedIdentityProviderConfig.get("backchannelSupported"), is("true")); + if(!KeycloakVersion.isKeycloak8()) { + assertThat(updatedIdentityProviderConfig.get("defaultScope"), is(nullValue())); + } else { + assertThat(updatedIdentityProviderConfig.get("defaultScope"), is("")); + } + assertThat(updatedIdentityProviderConfig.get("guiOrder"), is("0")); + assertThat(updatedIdentityProviderConfig.get("useJwksUrl"), is("true")); + + List identityProviderMappers = createdRealm.getIdentityProviderMappers(); + assertThat(identityProviderMappers.size(), is(1)); + + IdentityProviderMapperRepresentation myUsernameMapper = createdRealm.getIdentityProviderMappers().stream().filter(m -> m.getName().equals("my-username-mapper")).findFirst().get(); + + assertThat(myUsernameMapper, not((is(nullValue())))); + assertThat(myUsernameMapper.getIdentityProviderAlias(), is("keycloak-oidc")); + assertThat(myUsernameMapper.getIdentityProviderMapper(), is("oidc-username-idp-mapper")); + + Map myUsernameMapperConfig = myUsernameMapper.getConfig(); + + assertThat(myUsernameMapperConfig.get("template"), (is("${ALIAS}.${CLAIM.email}"))); + assertThat(myUsernameMapperConfig.get("syncMode"), (is("INHERIT"))); + } + + @Test + @Order(6) + void shouldUpdateOidcIdentityProviderWithUpdatedMapper() { + doImport("6_update_identity-provider_for_keycloak-oidc_with_updated_mapper.json"); + + RealmRepresentation createdRealm = keycloakProvider.get().realm(REALM_NAME).toRepresentation(); + + assertThat(createdRealm.getRealm(), is(REALM_NAME)); + assertThat(createdRealm.isEnabled(), is(true)); + + List identityProviders = createdRealm.getIdentityProviders(); + assertThat(identityProviders.size(), is(1)); + + IdentityProviderRepresentation oidcIdentityProvider = identityProviders.stream() + .filter(e -> e.getAlias().equals("keycloak-oidc")) + .findFirst() + .orElse(null); + + assertThat(oidcIdentityProvider, notNullValue()); + assertThat(oidcIdentityProvider.getAlias(), is("keycloak-oidc")); + assertThat(oidcIdentityProvider.getProviderId(), is("keycloak-oidc")); + assertThat(oidcIdentityProvider.getDisplayName(), is("my-keycloak-oidc")); + assertThat(oidcIdentityProvider.isEnabled(), is(true)); + assertThat(oidcIdentityProvider.isTrustEmail(), is(true)); + assertThat(oidcIdentityProvider.isStoreToken(), is(false)); + assertThat(oidcIdentityProvider.isAddReadTokenRoleOnCreate(), is(false)); + assertThat(oidcIdentityProvider.isLinkOnly(), is(false)); + assertThat(oidcIdentityProvider.getFirstBrokerLoginFlowAlias(), is("first broker login")); + + Map updatedIdentityProviderConfig = oidcIdentityProvider.getConfig(); + + assertThat(updatedIdentityProviderConfig.get("tokenUrl"), is("https://example.com/protocol/openid-connect/token")); + assertThat(updatedIdentityProviderConfig.get("authorizationUrl"), is("https://example.com/protocol/openid-connect/auth")); + assertThat(updatedIdentityProviderConfig.get("clientAuthMethod"), is("client_secret_post")); + assertThat(updatedIdentityProviderConfig.get("logoutUrl"), is("https://example.com/protocol/openid-connect/logout")); + assertThat(updatedIdentityProviderConfig.get("syncMode"), is("FORCE")); + assertThat(updatedIdentityProviderConfig.get("clientId"), is("example-client-id")); + assertThat(updatedIdentityProviderConfig.get("clientSecret"), is("example-client-secret")); + assertThat(updatedIdentityProviderConfig.get("backchannelSupported"), is("true")); + if(!KeycloakVersion.isKeycloak8()) { + assertThat(updatedIdentityProviderConfig.get("defaultScope"), is(nullValue())); + } else { + assertThat(updatedIdentityProviderConfig.get("defaultScope"), is("")); + } + assertThat(updatedIdentityProviderConfig.get("guiOrder"), is("0")); + assertThat(updatedIdentityProviderConfig.get("useJwksUrl"), is("true")); + + List identityProviderMappers = createdRealm.getIdentityProviderMappers(); + assertThat(identityProviderMappers.size(), is(1)); + + IdentityProviderMapperRepresentation myUsernameMapper = createdRealm.getIdentityProviderMappers().stream().filter(m -> m.getName().equals("my-username-mapper")).findFirst().get(); + + assertThat(myUsernameMapper, not((is(nullValue())))); + assertThat(myUsernameMapper.getIdentityProviderAlias(), is("keycloak-oidc")); + assertThat(myUsernameMapper.getIdentityProviderMapper(), is("oidc-username-idp-mapper")); + + Map myUsernameMapperConfig = myUsernameMapper.getConfig(); + + assertThat(myUsernameMapperConfig.get("template"), (is("${CLAIM.email}"))); + assertThat(myUsernameMapperConfig.get("syncMode"), (is("FORCE"))); + } + + + @Test + @Order(7) + void shouldUpdateOidcIdentityProviderWithReplacedMapper() { + doImport("7_update_identity-provider_for_keycloak-oidc_with_replaced_mapper.json"); + + RealmRepresentation createdRealm = keycloakProvider.get().realm(REALM_NAME).toRepresentation(); + + assertThat(createdRealm.getRealm(), is(REALM_NAME)); + assertThat(createdRealm.isEnabled(), is(true)); + + List identityProviders = createdRealm.getIdentityProviders(); + assertThat(identityProviders.size(), is(1)); + + IdentityProviderRepresentation oidcIdentityProvider = identityProviders.stream() + .filter(e -> e.getAlias().equals("keycloak-oidc")) + .findFirst() + .orElse(null); + + assertThat(oidcIdentityProvider, notNullValue()); + assertThat(oidcIdentityProvider.getAlias(), is("keycloak-oidc")); + assertThat(oidcIdentityProvider.getProviderId(), is("keycloak-oidc")); + assertThat(oidcIdentityProvider.getDisplayName(), is("my-keycloak-oidc")); + assertThat(oidcIdentityProvider.isEnabled(), is(true)); + assertThat(oidcIdentityProvider.isTrustEmail(), is(true)); + assertThat(oidcIdentityProvider.isStoreToken(), is(false)); + assertThat(oidcIdentityProvider.isAddReadTokenRoleOnCreate(), is(false)); + assertThat(oidcIdentityProvider.isLinkOnly(), is(false)); + assertThat(oidcIdentityProvider.getFirstBrokerLoginFlowAlias(), is("first broker login")); + + Map updatedIdentityProviderConfig = oidcIdentityProvider.getConfig(); + + assertThat(updatedIdentityProviderConfig.get("tokenUrl"), is("https://example.com/protocol/openid-connect/token")); + assertThat(updatedIdentityProviderConfig.get("authorizationUrl"), is("https://example.com/protocol/openid-connect/auth")); + assertThat(updatedIdentityProviderConfig.get("clientAuthMethod"), is("client_secret_post")); + assertThat(updatedIdentityProviderConfig.get("logoutUrl"), is("https://example.com/protocol/openid-connect/logout")); + assertThat(updatedIdentityProviderConfig.get("syncMode"), is("FORCE")); + assertThat(updatedIdentityProviderConfig.get("clientId"), is("example-client-id")); + assertThat(updatedIdentityProviderConfig.get("clientSecret"), is("example-client-secret")); + assertThat(updatedIdentityProviderConfig.get("backchannelSupported"), is("true")); + if(!KeycloakVersion.isKeycloak8()) { + assertThat(updatedIdentityProviderConfig.get("defaultScope"), is(nullValue())); + } else { + assertThat(updatedIdentityProviderConfig.get("defaultScope"), is("")); + } + assertThat(updatedIdentityProviderConfig.get("guiOrder"), is("0")); + assertThat(updatedIdentityProviderConfig.get("useJwksUrl"), is("true")); + + List identityProviderMappers = createdRealm.getIdentityProviderMappers(); + assertThat(identityProviderMappers.size(), is(1)); + + IdentityProviderMapperRepresentation myUsernameMapper = createdRealm.getIdentityProviderMappers().stream().filter(m -> m.getName().equals("my-changed-username-mapper")).findFirst().get(); + + assertThat(myUsernameMapper, not((is(nullValue())))); + assertThat(myUsernameMapper.getIdentityProviderAlias(), is("keycloak-oidc")); + assertThat(myUsernameMapper.getIdentityProviderMapper(), is("oidc-username-idp-mapper")); + + Map myUsernameMapperConfig = myUsernameMapper.getConfig(); + + assertThat(myUsernameMapperConfig.get("template"), (is("${CLAIM.email}"))); + assertThat(myUsernameMapperConfig.get("syncMode"), (is("FORCE"))); + } + + @Test + @Order(8) + void shouldUpdateOidcIdentityProviderWithDeleteAllMappers() { + doImport("8_update_identity-provider_for_keycloak-oidc_with_deleted_mapper.json"); + + RealmRepresentation createdRealm = keycloakProvider.get().realm(REALM_NAME).toRepresentation(); + + assertThat(createdRealm.getRealm(), is(REALM_NAME)); + assertThat(createdRealm.isEnabled(), is(true)); + + List identityProviders = createdRealm.getIdentityProviders(); + assertThat(identityProviders.size(), is(1)); + + IdentityProviderRepresentation oidcIdentityProvider = identityProviders.stream() + .filter(e -> e.getAlias().equals("keycloak-oidc")) + .findFirst() + .orElse(null); + + assertThat(oidcIdentityProvider, notNullValue()); + assertThat(oidcIdentityProvider.getAlias(), is("keycloak-oidc")); + assertThat(oidcIdentityProvider.getProviderId(), is("keycloak-oidc")); + assertThat(oidcIdentityProvider.getDisplayName(), is("my-keycloak-oidc")); + assertThat(oidcIdentityProvider.isEnabled(), is(true)); + assertThat(oidcIdentityProvider.isTrustEmail(), is(true)); + assertThat(oidcIdentityProvider.isStoreToken(), is(false)); + assertThat(oidcIdentityProvider.isAddReadTokenRoleOnCreate(), is(false)); + assertThat(oidcIdentityProvider.isLinkOnly(), is(false)); + assertThat(oidcIdentityProvider.getFirstBrokerLoginFlowAlias(), is("first broker login")); + + Map updatedIdentityProviderConfig = oidcIdentityProvider.getConfig(); + + assertThat(updatedIdentityProviderConfig.get("tokenUrl"), is("https://example.com/protocol/openid-connect/token")); + assertThat(updatedIdentityProviderConfig.get("authorizationUrl"), is("https://example.com/protocol/openid-connect/auth")); + assertThat(updatedIdentityProviderConfig.get("clientAuthMethod"), is("client_secret_post")); + assertThat(updatedIdentityProviderConfig.get("logoutUrl"), is("https://example.com/protocol/openid-connect/logout")); + assertThat(updatedIdentityProviderConfig.get("syncMode"), is("FORCE")); + assertThat(updatedIdentityProviderConfig.get("clientId"), is("example-client-id")); + assertThat(updatedIdentityProviderConfig.get("clientSecret"), is("example-client-secret")); + assertThat(updatedIdentityProviderConfig.get("backchannelSupported"), is("true")); + if(!KeycloakVersion.isKeycloak8()) { + assertThat(updatedIdentityProviderConfig.get("defaultScope"), is(nullValue())); + } else { + assertThat(updatedIdentityProviderConfig.get("defaultScope"), is("")); + } + assertThat(updatedIdentityProviderConfig.get("guiOrder"), is("0")); + assertThat(updatedIdentityProviderConfig.get("useJwksUrl"), is("true")); + + List identityProviderMappers = createdRealm.getIdentityProviderMappers(); + assertThat(identityProviderMappers, is(nullValue())); + } + + + @Test + @Order(9) + void shouldDeleteOidcIdentityProvider() { + doImport("9_delete_identity-provider_for_keycloak-oidc.json"); + + RealmRepresentation createdRealm = keycloakProvider.get().realm(REALM_NAME).toRepresentation(); + + assertThat(createdRealm.getRealm(), is(REALM_NAME)); + assertThat(createdRealm.isEnabled(), is(true)); + + List identityProviders = createdRealm.getIdentityProviders(); + assertThat(identityProviders, is(nullValue())); + } } diff --git a/src/test/java/de/adorsys/keycloak/config/ImportManagedNoDeleteIT.java b/src/test/java/de/adorsys/keycloak/config/ImportManagedNoDeleteIT.java index 70b68b346..b2dc91721 100644 --- a/src/test/java/de/adorsys/keycloak/config/ImportManagedNoDeleteIT.java +++ b/src/test/java/de/adorsys/keycloak/config/ImportManagedNoDeleteIT.java @@ -40,6 +40,8 @@ "import.managed.scope-mapping=no-delete", "import.managed.component=no-delete", "import.managed.sub-component=no-delete", + "import.managed.identity-provider=no-delete", + "import.managed.identity-provider-mapper=no-delete", }) class ImportManagedNoDeleteIT extends AbstractImportTest { private static final String REALM_NAME = "realmWithNoDelete"; @@ -112,5 +114,19 @@ private void assertRealm() { .filter((authenticationFlow) -> authenticationFlowsList.contains(authenticationFlow.getAlias())) .collect(Collectors.toList()); assertThat(createdAuthenticationFlows, hasSize(3)); + + List identityProviderList = Arrays.asList("my-first-idp", "my-second-idp"); + List createdIdentityProviders = createdRealm.getIdentityProviders() + .stream() + .filter((identityProvider) -> identityProviderList.contains(identityProvider.getAlias())) + .collect(Collectors.toList()); + assertThat(createdIdentityProviders, hasSize(2)); + + List identityProviderMapperList = Arrays.asList("my-first-idp-mapper", "my-second-idp-mapper"); + List createdIdentityProviderMappers = createdRealm.getIdentityProviderMappers() + .stream() + .filter((identityProviderMapper) -> identityProviderMapperList.contains(identityProviderMapper.getName())) + .collect(Collectors.toList()); + assertThat(createdIdentityProviderMappers, hasSize(2)); } } diff --git a/src/test/java/de/adorsys/keycloak/config/properties/ImportConfigPropertiesTest.java b/src/test/java/de/adorsys/keycloak/config/properties/ImportConfigPropertiesTest.java index e9a0bb1fe..fc128553a 100644 --- a/src/test/java/de/adorsys/keycloak/config/properties/ImportConfigPropertiesTest.java +++ b/src/test/java/de/adorsys/keycloak/config/properties/ImportConfigPropertiesTest.java @@ -50,6 +50,8 @@ "import.managed.scope-mapping=no-delete", "import.managed.component=no-delete", "import.managed.sub-component=no-delete", + "import.managed.identity-provider=no-delete", + "import.managed.identity-provider-mapper=no-delete" }) class ImportConfigPropertiesTest { @@ -71,6 +73,8 @@ void shouldPopulateConfigurationProperties() { assertThat(properties.getManaged().getScopeMapping(), is(ImportManagedPropertiesValues.NO_DELETE)); assertThat(properties.getManaged().getComponent(), is(ImportManagedPropertiesValues.NO_DELETE)); assertThat(properties.getManaged().getSubComponent(), is(ImportManagedPropertiesValues.NO_DELETE)); + assertThat(properties.getManaged().getIdentityProvider(), is(ImportManagedPropertiesValues.NO_DELETE)); + assertThat(properties.getManaged().getIdentityProviderMapper(), is(ImportManagedPropertiesValues.NO_DELETE)); } @EnableConfigurationProperties(ImportConfigProperties.class) diff --git a/src/test/java/de/adorsys/keycloak/config/test/util/KeycloakVersion.java b/src/test/java/de/adorsys/keycloak/config/test/util/KeycloakVersion.java new file mode 100644 index 000000000..8195c3ab2 --- /dev/null +++ b/src/test/java/de/adorsys/keycloak/config/test/util/KeycloakVersion.java @@ -0,0 +1,36 @@ +/*- + * ---license-start + * keycloak-config-cli + * --- + * Copyright (C) 2017 - 2020 adorsys GmbH & Co. KG @ https://adorsys.de + * --- + * 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. + * ---license-end + */ + +package de.adorsys.keycloak.config.test.util; + +public class KeycloakVersion { + + private static final String KEYCLOAK_VERSION_ENV_NAME = "KEYCLOAK_VERSION"; + + public static boolean isKeycloak8() { + String version = getKeycloakVersion(); + return version != null && version.startsWith("8."); + } + + private static String getKeycloakVersion() { + return System.getProperty(KEYCLOAK_VERSION_ENV_NAME); + } + +} diff --git a/src/test/resources/import-files/identity-providers/3_create_identity-provider_for_keycloak-oidc.json b/src/test/resources/import-files/identity-providers/3_create_identity-provider_for_keycloak-oidc.json new file mode 100644 index 000000000..01a334b47 --- /dev/null +++ b/src/test/resources/import-files/identity-providers/3_create_identity-provider_for_keycloak-oidc.json @@ -0,0 +1,31 @@ +{ + "enabled": true, + "realm": "realmWithIdentityProviders", + "identityProviders": [ + { + "alias": "keycloak-oidc", + "displayName": "my-keycloak-oidc", + "providerId": "keycloak-oidc", + "enabled": true, + "trustEmail": true, + "storeToken": false, + "addReadTokenRoleOnCreate": false, + "authenticateByDefault": false, + "linkOnly": false, + "firstBrokerLoginFlowAlias": "first broker login", + "config": { + "clientId": "example-client-id", + "tokenUrl": "https://example.com/protocol/openid-connect/token", + "authorizationUrl": "https://example.com/protocol/openid-connect/auth", + "clientAuthMethod": "client_secret_post", + "logoutUrl": "https://example.com/protocol/openid-connect/logout", + "syncMode": "FORCE", + "clientSecret": "example-client-secret", + "backchannelSupported": "true", + "defaultScope": "", + "guiOrder": "0", + "useJwksUrl": "true" + } + } + ] +} diff --git a/src/test/resources/import-files/identity-providers/4_update_identity-provider_for_keycloak-oidc.json b/src/test/resources/import-files/identity-providers/4_update_identity-provider_for_keycloak-oidc.json new file mode 100644 index 000000000..228b8838b --- /dev/null +++ b/src/test/resources/import-files/identity-providers/4_update_identity-provider_for_keycloak-oidc.json @@ -0,0 +1,31 @@ +{ + "enabled": true, + "realm": "realmWithIdentityProviders", + "identityProviders": [ + { + "alias": "keycloak-oidc", + "displayName": "changed-my-keycloak-oidc", + "providerId": "keycloak-oidc", + "enabled": true, + "trustEmail": true, + "storeToken": false, + "addReadTokenRoleOnCreate": false, + "authenticateByDefault": false, + "linkOnly": false, + "firstBrokerLoginFlowAlias": "first broker login", + "config": { + "clientId": "changed-example-client-id", + "tokenUrl": "https://example.com/protocol/openid-connect/token", + "authorizationUrl": "https://example.com/protocol/openid-connect/auth", + "clientAuthMethod": "client_secret_post", + "logoutUrl": "https://example.com/protocol/openid-connect/logout", + "syncMode": "FORCE", + "clientSecret": "example-client-secret", + "backchannelSupported": "true", + "defaultScope": "", + "guiOrder": "0", + "useJwksUrl": "true" + } + } + ] +} diff --git a/src/test/resources/import-files/identity-providers/5_update_identity-provider_for_keycloak-oidc_with_mapper.json b/src/test/resources/import-files/identity-providers/5_update_identity-provider_for_keycloak-oidc_with_mapper.json new file mode 100644 index 000000000..278924589 --- /dev/null +++ b/src/test/resources/import-files/identity-providers/5_update_identity-provider_for_keycloak-oidc_with_mapper.json @@ -0,0 +1,43 @@ +{ + "enabled": true, + "realm": "realmWithIdentityProviders", + "identityProviders": [ + { + "alias": "keycloak-oidc", + "displayName": "my-keycloak-oidc", + "providerId": "keycloak-oidc", + "enabled": true, + "updateProfileFirstLoginMode": "on", + "trustEmail": true, + "storeToken": false, + "addReadTokenRoleOnCreate": false, + "authenticateByDefault": false, + "linkOnly": false, + "firstBrokerLoginFlowAlias": "first broker login", + "config": { + "clientId": "example-client-id", + "tokenUrl": "https://example.com/protocol/openid-connect/token", + "authorizationUrl": "https://example.com/protocol/openid-connect/auth", + "clientAuthMethod": "client_secret_post", + "logoutUrl": "https://example.com/protocol/openid-connect/logout", + "syncMode": "FORCE", + "clientSecret": "example-client-secret", + "backchannelSupported": "true", + "defaultScope": "", + "guiOrder": "0", + "useJwksUrl": "true" + } + } + ], + "identityProviderMappers": [ + { + "name": "my-username-mapper", + "identityProviderAlias": "keycloak-oidc", + "identityProviderMapper": "oidc-username-idp-mapper", + "config": { + "template": "${ALIAS}.${CLAIM.email}", + "syncMode": "INHERIT" + } + } + ] +} diff --git a/src/test/resources/import-files/identity-providers/6_update_identity-provider_for_keycloak-oidc_with_updated_mapper.json b/src/test/resources/import-files/identity-providers/6_update_identity-provider_for_keycloak-oidc_with_updated_mapper.json new file mode 100644 index 000000000..bf613decc --- /dev/null +++ b/src/test/resources/import-files/identity-providers/6_update_identity-provider_for_keycloak-oidc_with_updated_mapper.json @@ -0,0 +1,43 @@ +{ + "enabled": true, + "realm": "realmWithIdentityProviders", + "identityProviders": [ + { + "alias": "keycloak-oidc", + "displayName": "my-keycloak-oidc", + "providerId": "keycloak-oidc", + "enabled": true, + "updateProfileFirstLoginMode": "on", + "trustEmail": true, + "storeToken": false, + "addReadTokenRoleOnCreate": false, + "authenticateByDefault": false, + "linkOnly": false, + "firstBrokerLoginFlowAlias": "first broker login", + "config": { + "clientId": "example-client-id", + "tokenUrl": "https://example.com/protocol/openid-connect/token", + "authorizationUrl": "https://example.com/protocol/openid-connect/auth", + "clientAuthMethod": "client_secret_post", + "logoutUrl": "https://example.com/protocol/openid-connect/logout", + "syncMode": "FORCE", + "clientSecret": "example-client-secret", + "backchannelSupported": "true", + "defaultScope": "", + "guiOrder": "0", + "useJwksUrl": "true" + } + } + ], + "identityProviderMappers": [ + { + "name": "my-username-mapper", + "identityProviderAlias": "keycloak-oidc", + "identityProviderMapper": "oidc-username-idp-mapper", + "config": { + "template": "${CLAIM.email}", + "syncMode": "FORCE" + } + } + ] +} diff --git a/src/test/resources/import-files/identity-providers/7_update_identity-provider_for_keycloak-oidc_with_replaced_mapper.json b/src/test/resources/import-files/identity-providers/7_update_identity-provider_for_keycloak-oidc_with_replaced_mapper.json new file mode 100644 index 000000000..266366e8f --- /dev/null +++ b/src/test/resources/import-files/identity-providers/7_update_identity-provider_for_keycloak-oidc_with_replaced_mapper.json @@ -0,0 +1,43 @@ +{ + "enabled": true, + "realm": "realmWithIdentityProviders", + "identityProviders": [ + { + "alias": "keycloak-oidc", + "displayName": "my-keycloak-oidc", + "providerId": "keycloak-oidc", + "enabled": true, + "updateProfileFirstLoginMode": "on", + "trustEmail": true, + "storeToken": false, + "addReadTokenRoleOnCreate": false, + "authenticateByDefault": false, + "linkOnly": false, + "firstBrokerLoginFlowAlias": "first broker login", + "config": { + "clientId": "example-client-id", + "tokenUrl": "https://example.com/protocol/openid-connect/token", + "authorizationUrl": "https://example.com/protocol/openid-connect/auth", + "clientAuthMethod": "client_secret_post", + "logoutUrl": "https://example.com/protocol/openid-connect/logout", + "syncMode": "FORCE", + "clientSecret": "example-client-secret", + "backchannelSupported": "true", + "defaultScope": "", + "guiOrder": "0", + "useJwksUrl": "true" + } + } + ], + "identityProviderMappers": [ + { + "name": "my-changed-username-mapper", + "identityProviderAlias": "keycloak-oidc", + "identityProviderMapper": "oidc-username-idp-mapper", + "config": { + "template": "${CLAIM.email}", + "syncMode": "FORCE" + } + } + ] +} diff --git a/src/test/resources/import-files/identity-providers/8_update_identity-provider_for_keycloak-oidc_with_deleted_mapper.json b/src/test/resources/import-files/identity-providers/8_update_identity-provider_for_keycloak-oidc_with_deleted_mapper.json new file mode 100644 index 000000000..e7921b697 --- /dev/null +++ b/src/test/resources/import-files/identity-providers/8_update_identity-provider_for_keycloak-oidc_with_deleted_mapper.json @@ -0,0 +1,34 @@ +{ + "enabled": true, + "realm": "realmWithIdentityProviders", + "identityProviders": [ + { + "alias": "keycloak-oidc", + "displayName": "my-keycloak-oidc", + "providerId": "keycloak-oidc", + "enabled": true, + "updateProfileFirstLoginMode": "on", + "trustEmail": true, + "storeToken": false, + "addReadTokenRoleOnCreate": false, + "authenticateByDefault": false, + "linkOnly": false, + "firstBrokerLoginFlowAlias": "first broker login", + "config": { + "clientId": "example-client-id", + "tokenUrl": "https://example.com/protocol/openid-connect/token", + "authorizationUrl": "https://example.com/protocol/openid-connect/auth", + "clientAuthMethod": "client_secret_post", + "logoutUrl": "https://example.com/protocol/openid-connect/logout", + "syncMode": "FORCE", + "clientSecret": "example-client-secret", + "backchannelSupported": "true", + "defaultScope": "", + "guiOrder": "0", + "useJwksUrl": "true" + } + } + ], + "identityProviderMappers": [ + ] +} diff --git a/src/test/resources/import-files/identity-providers/9_delete_identity-provider_for_keycloak-oidc.json b/src/test/resources/import-files/identity-providers/9_delete_identity-provider_for_keycloak-oidc.json new file mode 100644 index 000000000..7ff66b84f --- /dev/null +++ b/src/test/resources/import-files/identity-providers/9_delete_identity-provider_for_keycloak-oidc.json @@ -0,0 +1,5 @@ +{ + "enabled": true, + "realm": "realmWithIdentityProviders", + "identityProviders": [] +} diff --git a/src/test/resources/import-files/managed-no-delete/0_create_simple-realm.json b/src/test/resources/import-files/managed-no-delete/0_create_simple-realm.json index 5918274e4..7dc4e9077 100644 --- a/src/test/resources/import-files/managed-no-delete/0_create_simple-realm.json +++ b/src/test/resources/import-files/managed-no-delete/0_create_simple-realm.json @@ -308,5 +308,79 @@ } } ] - } + }, + "identityProviders": [ + { + "alias": "my-first-idp", + "displayName": "my-first-idp", + "providerId": "keycloak-oidc", + "enabled": true, + "updateProfileFirstLoginMode": "on", + "trustEmail": true, + "storeToken": false, + "addReadTokenRoleOnCreate": false, + "authenticateByDefault": false, + "linkOnly": false, + "firstBrokerLoginFlowAlias": "first broker login", + "config": { + "clientId": "example-client-id", + "tokenUrl": "https://example.com/protocol/openid-connect/token", + "authorizationUrl": "https://example.com/protocol/openid-connect/auth", + "clientAuthMethod": "client_secret_post", + "logoutUrl": "https://example.com/protocol/openid-connect/logout", + "syncMode": "FORCE", + "clientSecret": "example-client-secret", + "backchannelSupported": "true", + "defaultScope": "", + "guiOrder": "0", + "useJwksUrl": "true" + } + }, + { + "alias": "my-second-idp", + "displayName": "my-second-idp", + "providerId": "keycloak-oidc", + "enabled": true, + "updateProfileFirstLoginMode": "on", + "trustEmail": true, + "storeToken": false, + "addReadTokenRoleOnCreate": false, + "authenticateByDefault": false, + "linkOnly": false, + "firstBrokerLoginFlowAlias": "first broker login", + "config": { + "clientId": "example-client-id", + "tokenUrl": "https://example.com/protocol/openid-connect/token", + "authorizationUrl": "https://example.com/protocol/openid-connect/auth", + "clientAuthMethod": "client_secret_post", + "logoutUrl": "https://example.com/protocol/openid-connect/logout", + "syncMode": "FORCE", + "clientSecret": "example-client-secret", + "backchannelSupported": "true", + "defaultScope": "", + "guiOrder": "0", + "useJwksUrl": "true" + } + } + ], + "identityProviderMappers": [ + { + "name": "my-first-idp-mapper", + "identityProviderAlias": "my-first-idp", + "identityProviderMapper": "oidc-username-idp-mapper", + "config": { + "template": "${ALIAS}.${CLAIM.email}", + "syncMode": "INHERIT" + } + }, + { + "name": "my-second-idp-mapper", + "identityProviderAlias": "my-first-idp", + "identityProviderMapper": "oidc-username-idp-mapper", + "config": { + "template": "${ALIAS}.${CLAIM.email}", + "syncMode": "INHERIT" + } + } + ] } diff --git a/src/test/resources/import-files/managed-no-delete/1_update-realm_not-delete-one.json b/src/test/resources/import-files/managed-no-delete/1_update-realm_not-delete-one.json index aadf52638..463ef8e04 100644 --- a/src/test/resources/import-files/managed-no-delete/1_update-realm_not-delete-one.json +++ b/src/test/resources/import-files/managed-no-delete/1_update-realm_not-delete-one.json @@ -221,5 +221,44 @@ } } ] - } + }, + "identityProviders": [ + { + "alias": "my-first-idp", + "displayName": "my-first-idp", + "providerId": "keycloak-oidc", + "enabled": true, + "updateProfileFirstLoginMode": "on", + "trustEmail": true, + "storeToken": false, + "addReadTokenRoleOnCreate": false, + "authenticateByDefault": false, + "linkOnly": false, + "firstBrokerLoginFlowAlias": "first broker login", + "config": { + "clientId": "example-client-id", + "tokenUrl": "https://example.com/protocol/openid-connect/token", + "authorizationUrl": "https://example.com/protocol/openid-connect/auth", + "clientAuthMethod": "client_secret_post", + "logoutUrl": "https://example.com/protocol/openid-connect/logout", + "syncMode": "FORCE", + "clientSecret": "example-client-secret", + "backchannelSupported": "true", + "defaultScope": "", + "guiOrder": "0", + "useJwksUrl": "true" + } + } + ], + "identityProviderMappers": [ + { + "name": "my-first-idp-mapper", + "identityProviderAlias": "my-first-idp", + "identityProviderMapper": "oidc-username-idp-mapper", + "config": { + "template": "${ALIAS}.${CLAIM.email}", + "syncMode": "INHERIT" + } + } + ] } diff --git a/src/test/resources/import-files/managed-no-delete/2_update-realm_not-delete-all.json b/src/test/resources/import-files/managed-no-delete/2_update-realm_not-delete-all.json index 36b8f1532..e9da70988 100644 --- a/src/test/resources/import-files/managed-no-delete/2_update-realm_not-delete-all.json +++ b/src/test/resources/import-files/managed-no-delete/2_update-realm_not-delete-all.json @@ -6,5 +6,7 @@ "requiredActions": [], "clientScopes": [], "scopeMappings": [], - "components": {} + "components": {}, + "identityProviders": [], + "identityProviderMappers": [] }