diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 770706b66..035d64630 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -55,6 +55,11 @@ jobs: key: ${{ runner.os }}-maven-${{ matrix.env.KEYCLOAK_VERSION }}-${{ hashFiles('**/pom.xml') }} restore-keys: ${{ runner.os }}-maven-${{ matrix.env.KEYCLOAK_VERSION }} + - name: Adapt sources for Keycloak versions < 24.0.0 + if: ${{ matrix.env.KEYCLOAK_VERSION < '24.0.0' }} + run: | + echo "COMPATIBILITY_PROFILE=-Ppre-keycloak24" >> $GITHUB_ENV + - name: Adapt sources for Keycloak versions < 23.0.0 if: ${{ matrix.env.KEYCLOAK_VERSION < '23.0.0' }} run: | @@ -185,6 +190,11 @@ jobs: key: ${{ runner.os }}-${{ matrix.java }}-maven-build-pom-${{ hashFiles('**/pom.xml') }} restore-keys: ${{ runner.os }}-${{ matrix.java }}-maven-build-pom + - name: Adapt sources for Keycloak versions < 24.0.0 + if: ${{ matrix.env.KEYCLOAK_VERSION < '24.0.0' }} + run: | + echo "COMPATIBILITY_PROFILE=-Ppre-keycloak24" >> $GITHUB_ENV + - name: Adapt sources for Keycloak versions < 23.0.0 if: ${{ matrix.env.KEYCLOAK_VERSION < '23.0.0' }} run: | @@ -222,6 +232,10 @@ jobs: key: ${{ runner.os }}-maven-keycloak-legacy-${{ hashFiles('**/pom.xml') }} restore-keys: | ${{ runner.os }}-maven-keycloak-legacy + - name: Adapt sources for Keycloak versions < 24.0.0 + if: ${{ matrix.env.KEYCLOAK_VERSION < '24.0.0' }} + run: | + echo "COMPATIBILITY_PROFILE=-Ppre-keycloak24" >> $GITHUB_ENV - name: Adapt sources for Keycloak versions < 23.0.0 if: ${{ matrix.env.KEYCLOAK_VERSION < '23.0.0' }} run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index 95b27e768..ab2fc8ec1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] +### Added + +- Support for first broker login flows defined on realm level + ### Fixed - Allow executions of same provider with different configurations in Sub-Auth-Flows diff --git a/pom.xml b/pom.xml index fe7b116e3..111fb3dce 100644 --- a/pom.xml +++ b/pom.xml @@ -883,6 +883,42 @@ import org.keycloak.representations.userprofile.config.UPConfig; + + pre-keycloak24 + + + + com.coderplus.maven.plugins + copy-rename-maven-plugin + 1.0.1 + + + replace-used-authentication-flow-workaround-with-legacy + generate-sources + + copy + + + ${project.basedir}/src/main/java/de/adorsys/keycloak/config/factory/UsedAuthenticationFlowWorkaroundFactory.java.legacy + ${project.basedir}/src/main/java/de/adorsys/keycloak/config/factory/UsedAuthenticationFlowWorkaroundFactory.java + + + + replace-authentication-flow-import-service-with-legacy + generate-sources + + copy + + + ${project.basedir}/src/main/java/de/adorsys/keycloak/config/service/AuthenticationFlowsImportService.java.legacy + ${project.basedir}/src/main/java/de/adorsys/keycloak/config/service/AuthenticationFlowsImportService.java + + + + + + + coverage diff --git a/src/main/java/de/adorsys/keycloak/config/factory/UsedAuthenticationFlowWorkaroundFactory.java b/src/main/java/de/adorsys/keycloak/config/factory/UsedAuthenticationFlowWorkaroundFactory.java index 4b9252936..645e926f3 100644 --- a/src/main/java/de/adorsys/keycloak/config/factory/UsedAuthenticationFlowWorkaroundFactory.java +++ b/src/main/java/de/adorsys/keycloak/config/factory/UsedAuthenticationFlowWorkaroundFactory.java @@ -75,6 +75,7 @@ public class UsedAuthenticationFlowWorkaround { private String dockerAuthenticationFlow; private String registrationFlow; private String resetCredentialsFlow; + private String firstBrokerLoginFlow; private UsedAuthenticationFlowWorkaround(RealmImport realmImport) { this.realmImport = realmImport; @@ -168,6 +169,13 @@ private void disableFirstBrokerLoginFlowsIfNeeded(String topLevelFlowAlias, Real } } } + if (Objects.equals(existingRealm.getFirstBrokerLoginFlow(), topLevelFlowAlias)) { + logger.debug( + "Temporary disable first-broker-login-flow for in realm '{}' which is '{}'", + realmImport.getRealm(), topLevelFlowAlias + ); + disableFirstBrokerLoginFlow(existingRealm); + } } private void disablePostBrokerLoginFlowsIfNeeded(String topLevelFlowAlias, RealmRepresentation existingRealm) { @@ -241,6 +249,15 @@ private void disableResetCredentialsFlow(RealmRepresentation existingRealm) { realmRepository.update(existingRealm); } + private void disableFirstBrokerLoginFlow(RealmRepresentation existingRealm) { + String otherFlowAlias = searchTemporaryCreatedTopLevelFlowForReplacement(); + + firstBrokerLoginFlow = existingRealm.getFirstBrokerLoginFlow(); + + existingRealm.setFirstBrokerLoginFlow(otherFlowAlias); + realmRepository.update(existingRealm); + } + private void disableFirstBrokerLoginFlow(String realmName, IdentityProviderRepresentation identityProvider) { String otherFlowAlias = searchTemporaryCreatedTopLevelFlowForReplacement(); @@ -323,7 +340,8 @@ private boolean hasToResetFlows() { || Strings.isNotBlank(registrationFlow) || Strings.isNotBlank(resetCredentialsFlow) || !resetFirstBrokerLoginFlow.isEmpty() - || !resetPostBrokerLoginFlow.isEmpty(); + || !resetPostBrokerLoginFlow.isEmpty() + || Strings.isNotBlank(firstBrokerLoginFlow); } private void resetFlows(RealmRepresentation existingRealm) { @@ -416,6 +434,14 @@ private void resetFirstBrokerLoginFlowsIfNeeded(RealmRepresentation existingReal identityProviderRepresentation.setFirstBrokerLoginFlowAlias(entry.getValue()); identityProviderRepository.update(existingRealm.getRealm(), identityProviderRepresentation); } + if (Strings.isNotBlank(firstBrokerLoginFlow)) { + logger.debug( + "Reset first-broker-login-flow in realm '{}' to '{}'", + realmImport.getRealm(), firstBrokerLoginFlow + ); + + existingRealm.setFirstBrokerLoginFlow(firstBrokerLoginFlow); + } } private void resetPostBrokerLoginFlowsIfNeeded(RealmRepresentation existingRealm) { diff --git a/src/main/java/de/adorsys/keycloak/config/factory/UsedAuthenticationFlowWorkaroundFactory.java.legacy b/src/main/java/de/adorsys/keycloak/config/factory/UsedAuthenticationFlowWorkaroundFactory.java.legacy new file mode 100644 index 000000000..4b9252936 --- /dev/null +++ b/src/main/java/de/adorsys/keycloak/config/factory/UsedAuthenticationFlowWorkaroundFactory.java.legacy @@ -0,0 +1,457 @@ +/*- + * ---license-start + * keycloak-config-cli + * --- + * Copyright (C) 2017 - 2021 adorsys GmbH & Co. KG @ https://adorsys.com + * --- + * 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.factory; + +import de.adorsys.keycloak.config.model.RealmImport; +import de.adorsys.keycloak.config.repository.AuthenticationFlowRepository; +import de.adorsys.keycloak.config.repository.IdentityProviderRepository; +import de.adorsys.keycloak.config.repository.RealmRepository; +import org.apache.logging.log4j.util.Strings; +import org.keycloak.representations.idm.AuthenticationFlowRepresentation; +import org.keycloak.representations.idm.IdentityProviderRepresentation; +import org.keycloak.representations.idm.RealmRepresentation; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.*; + +@Service +public class UsedAuthenticationFlowWorkaroundFactory { + + private final RealmRepository realmRepository; + private final IdentityProviderRepository identityProviderRepository; + private final AuthenticationFlowRepository authenticationFlowRepository; + + @Autowired + public UsedAuthenticationFlowWorkaroundFactory( + RealmRepository realmRepository, + IdentityProviderRepository identityProviderRepository, + AuthenticationFlowRepository authenticationFlowRepository + ) { + this.realmRepository = realmRepository; + this.identityProviderRepository = identityProviderRepository; + this.authenticationFlowRepository = authenticationFlowRepository; + } + + public UsedAuthenticationFlowWorkaround buildFor(RealmImport realmImport) { + return new UsedAuthenticationFlowWorkaround(realmImport); + } + + /** + * There is no chance to update a top-level-flow, and it's not possible to recreate a top-level-flow + * which is currently in use. + * So we have to disable our top-level-flow by use a temporary created flow as long as updating the considered flow. + * This code could be maybe replace by a better update-algorithm of top-level-flows + */ + public class UsedAuthenticationFlowWorkaround { + private static final String TEMPORARY_CREATED_AUTH_FLOW = "TEMPORARY_CREATED_AUTH_FLOW"; + private final Logger logger = LoggerFactory.getLogger(UsedAuthenticationFlowWorkaround.class); + private final RealmImport realmImport; + private final Map resetFirstBrokerLoginFlow = new HashMap<>(); + private final Map resetPostBrokerLoginFlow = new HashMap<>(); + private String browserFlow; + private String directGrantFlow; + private String clientAuthenticationFlow; + private String dockerAuthenticationFlow; + private String registrationFlow; + private String resetCredentialsFlow; + + private UsedAuthenticationFlowWorkaround(RealmImport realmImport) { + this.realmImport = realmImport; + } + + public void disableTopLevelFlowIfNeeded(String topLevelFlowAlias) { + RealmRepresentation existingRealm = realmRepository.get(realmImport.getRealm()); + + disableBrowserFlowIfNeeded(topLevelFlowAlias, existingRealm); + disableDirectGrantFlowIfNeeded(topLevelFlowAlias, existingRealm); + disableClientAuthenticationFlowIfNeeded(topLevelFlowAlias, existingRealm); + disableDockerAuthenticationFlowIfNeeded(topLevelFlowAlias, existingRealm); + disableRegistrationFlowIfNeeded(topLevelFlowAlias, existingRealm); + disableResetCredentialsFlowIfNeeded(topLevelFlowAlias, existingRealm); + disableFirstBrokerLoginFlowsIfNeeded(topLevelFlowAlias, existingRealm); + disablePostBrokerLoginFlowsIfNeeded(topLevelFlowAlias, existingRealm); + } + + private void disableBrowserFlowIfNeeded(String topLevelFlowAlias, RealmRepresentation existingRealm) { + if (Objects.equals(existingRealm.getBrowserFlow(), topLevelFlowAlias)) { + logger.debug( + "Temporary disable browser-flow in realm '{}' which is '{}'", + realmImport.getRealm(), topLevelFlowAlias + ); + disableBrowserFlow(existingRealm); + } + } + + private void disableDirectGrantFlowIfNeeded(String topLevelFlowAlias, RealmRepresentation existingRealm) { + if (Objects.equals(existingRealm.getDirectGrantFlow(), topLevelFlowAlias)) { + logger.debug( + "Temporary disable direct-grant-flow in realm '{}' which is '{}'", + realmImport.getRealm(), topLevelFlowAlias + ); + disableDirectGrantFlow(existingRealm); + } + } + + private void disableClientAuthenticationFlowIfNeeded(String topLevelFlowAlias, RealmRepresentation existingRealm) { + if (Objects.equals(existingRealm.getClientAuthenticationFlow(), topLevelFlowAlias)) { + logger.debug( + "Temporary disable client-authentication-flow in realm '{}' which is '{}'", + realmImport.getRealm(), topLevelFlowAlias + ); + disableClientAuthenticationFlow(existingRealm); + } + } + + private void disableDockerAuthenticationFlowIfNeeded(String topLevelFlowAlias, RealmRepresentation existingRealm) { + if (Objects.equals(existingRealm.getDockerAuthenticationFlow(), topLevelFlowAlias)) { + logger.debug( + "Temporary disable docker-authentication-flow in realm '{}' which is '{}'", + realmImport.getRealm(), topLevelFlowAlias + ); + disableDockerAuthenticationFlow(existingRealm); + } + } + + private void disableRegistrationFlowIfNeeded(String topLevelFlowAlias, RealmRepresentation existingRealm) { + if (Objects.equals(existingRealm.getRegistrationFlow(), topLevelFlowAlias)) { + logger.debug( + "Temporary disable registration-flow in realm '{}' which is '{}'", + realmImport.getRealm(), topLevelFlowAlias + ); + disableRegistrationFlow(existingRealm); + } + } + + private void disableResetCredentialsFlowIfNeeded(String topLevelFlowAlias, RealmRepresentation existingRealm) { + if (Objects.equals(existingRealm.getResetCredentialsFlow(), topLevelFlowAlias)) { + logger.debug( + "Temporary disable reset-credentials-flow in realm '{}' which is '{}'", + realmImport.getRealm(), topLevelFlowAlias + ); + disableResetCredentialsFlow(existingRealm); + } + } + + private void disableFirstBrokerLoginFlowsIfNeeded(String topLevelFlowAlias, RealmRepresentation existingRealm) { + List identityProviders = existingRealm.getIdentityProviders(); + if (identityProviders != null) { + for (IdentityProviderRepresentation identityProvider : identityProviders) { + if (Objects.equals(identityProvider.getFirstBrokerLoginFlowAlias(), topLevelFlowAlias)) { + logger.debug( + "Temporary disable first-broker-login-flow for " + + "identity-provider '{}' in realm '{}' which is '{}'", + identityProvider.getAlias(), realmImport.getRealm(), topLevelFlowAlias + ); + + disableFirstBrokerLoginFlow(existingRealm.getRealm(), identityProvider); + } + } + } + } + + private void disablePostBrokerLoginFlowsIfNeeded(String topLevelFlowAlias, RealmRepresentation existingRealm) { + List identityProviders = existingRealm.getIdentityProviders(); + if (identityProviders != null) { + for (IdentityProviderRepresentation identityProvider : identityProviders) { + if (Objects.equals(identityProvider.getPostBrokerLoginFlowAlias(), topLevelFlowAlias)) { + logger.debug( + "Temporary disable post-broker-login-flow for " + + "identity-provider '{}' in realm '{}' which is '{}'", + identityProvider.getAlias(), realmImport.getRealm(), topLevelFlowAlias + ); + + disablePostBrokerLoginFlow(existingRealm.getRealm(), identityProvider); + } + } + } + } + + private void disableBrowserFlow(RealmRepresentation existingRealm) { + String otherFlowAlias = searchTemporaryCreatedTopLevelFlowForReplacement(); + + browserFlow = existingRealm.getBrowserFlow(); + + existingRealm.setBrowserFlow(otherFlowAlias); + realmRepository.update(existingRealm); + } + + private void disableDirectGrantFlow(RealmRepresentation existingRealm) { + String otherFlowAlias = searchTemporaryCreatedTopLevelFlowForReplacement(); + + directGrantFlow = existingRealm.getDirectGrantFlow(); + + existingRealm.setDirectGrantFlow(otherFlowAlias); + realmRepository.update(existingRealm); + } + + private void disableClientAuthenticationFlow(RealmRepresentation existingRealm) { + String otherFlowAlias = searchTemporaryCreatedTopLevelFlowForReplacement(); + + clientAuthenticationFlow = existingRealm.getClientAuthenticationFlow(); + + existingRealm.setClientAuthenticationFlow(otherFlowAlias); + realmRepository.update(existingRealm); + } + + private void disableDockerAuthenticationFlow(RealmRepresentation existingRealm) { + String otherFlowAlias = searchTemporaryCreatedTopLevelFlowForReplacement(); + + dockerAuthenticationFlow = existingRealm.getDockerAuthenticationFlow(); + + existingRealm.setDockerAuthenticationFlow(otherFlowAlias); + realmRepository.update(existingRealm); + } + + private void disableRegistrationFlow(RealmRepresentation existingRealm) { + String otherFlowAlias = searchTemporaryCreatedTopLevelFlowForReplacement(); + + registrationFlow = existingRealm.getRegistrationFlow(); + + existingRealm.setRegistrationFlow(otherFlowAlias); + realmRepository.update(existingRealm); + } + + private void disableResetCredentialsFlow(RealmRepresentation existingRealm) { + String otherFlowAlias = searchTemporaryCreatedTopLevelFlowForReplacement(); + + resetCredentialsFlow = existingRealm.getResetCredentialsFlow(); + + existingRealm.setResetCredentialsFlow(otherFlowAlias); + realmRepository.update(existingRealm); + } + + private void disableFirstBrokerLoginFlow(String realmName, IdentityProviderRepresentation identityProvider) { + String otherFlowAlias = searchTemporaryCreatedTopLevelFlowForReplacement(); + + resetFirstBrokerLoginFlow.put(identityProvider.getAlias(), identityProvider + .getFirstBrokerLoginFlowAlias()); + + identityProvider.setFirstBrokerLoginFlowAlias(otherFlowAlias); + identityProviderRepository.update(realmName, identityProvider); + } + + private void disablePostBrokerLoginFlow(String realmName, IdentityProviderRepresentation identityProvider) { + String otherFlowAlias = searchTemporaryCreatedTopLevelFlowForReplacement(); + + resetPostBrokerLoginFlow.put(identityProvider.getAlias(), identityProvider + .getPostBrokerLoginFlowAlias()); + + identityProvider.setPostBrokerLoginFlowAlias(otherFlowAlias); + identityProviderRepository.update(realmName, identityProvider); + } + + private String searchTemporaryCreatedTopLevelFlowForReplacement() { + AuthenticationFlowRepresentation otherFlow; + + Optional maybeTemporaryCreatedFlow = searchForTemporaryCreatedFlow(); + + if (maybeTemporaryCreatedFlow.isPresent()) { + otherFlow = maybeTemporaryCreatedFlow.get(); + } else { + logger.debug( + "Create top-level-flow '{}' in realm '{}' to be used temporarily", + realmImport.getRealm(), TEMPORARY_CREATED_AUTH_FLOW + ); + + AuthenticationFlowRepresentation temporaryCreatedFlow = setupTemporaryCreatedFlow(); + authenticationFlowRepository.createTopLevel(realmImport.getRealm(), temporaryCreatedFlow); + + otherFlow = temporaryCreatedFlow; + } + + return otherFlow.getAlias(); + } + + private Optional searchForTemporaryCreatedFlow() { + List existingTopLevelFlows = authenticationFlowRepository + .getTopLevelFlows(realmImport.getRealm()); + + return existingTopLevelFlows.stream() + .filter(f -> Objects.equals(f.getAlias(), TEMPORARY_CREATED_AUTH_FLOW)) + .findFirst(); + } + + public void resetFlowIfNeeded() { + if (hasToResetFlows()) { + RealmRepresentation existingRealm = realmRepository.get(realmImport.getRealm()); + + resetFlows(existingRealm); + realmRepository.update(existingRealm); + + if (!flowInUse()) { + deleteTemporaryCreatedFlow(); + } + } + } + + private boolean flowInUse() { + RealmRepresentation existingRealm = realmRepository.get(realmImport.getRealm()); + return existingRealm.getBrowserFlow().equals(TEMPORARY_CREATED_AUTH_FLOW) + || existingRealm.getDirectGrantFlow().equals(TEMPORARY_CREATED_AUTH_FLOW) + || existingRealm.getClientAuthenticationFlow().equals(TEMPORARY_CREATED_AUTH_FLOW) + || existingRealm.getDockerAuthenticationFlow().equals(TEMPORARY_CREATED_AUTH_FLOW) + || existingRealm.getRegistrationFlow().equals(TEMPORARY_CREATED_AUTH_FLOW) + || existingRealm.getResetCredentialsFlow().equals(TEMPORARY_CREATED_AUTH_FLOW); + } + + private boolean hasToResetFlows() { + return Strings.isNotBlank(browserFlow) + || Strings.isNotBlank(directGrantFlow) + || Strings.isNotBlank(clientAuthenticationFlow) + || Strings.isNotBlank(dockerAuthenticationFlow) + || Strings.isNotBlank(registrationFlow) + || Strings.isNotBlank(resetCredentialsFlow) + || !resetFirstBrokerLoginFlow.isEmpty() + || !resetPostBrokerLoginFlow.isEmpty(); + } + + private void resetFlows(RealmRepresentation existingRealm) { + resetBrowserFlowIfNeeded(existingRealm); + resetDirectGrantFlowIfNeeded(existingRealm); + resetClientAuthenticationFlowIfNeeded(existingRealm); + resetDockerAuthenticationFlowIfNeeded(existingRealm); + resetRegistrationFlowIfNeeded(existingRealm); + resetCredentialsFlowIfNeeded(existingRealm); + resetFirstBrokerLoginFlowsIfNeeded(existingRealm); + resetPostBrokerLoginFlowsIfNeeded(existingRealm); + } + + private void resetBrowserFlowIfNeeded(RealmRepresentation existingRealm) { + if (Strings.isNotBlank(browserFlow)) { + logger.debug( + "Reset browser-flow in realm '{}' to '{}'", + realmImport.getRealm(), browserFlow + ); + + existingRealm.setBrowserFlow(browserFlow); + } + } + + private void resetDirectGrantFlowIfNeeded(RealmRepresentation existingRealm) { + if (Strings.isNotBlank(directGrantFlow)) { + logger.debug( + "Reset direct-grant-flow in realm '{}' to '{}'", + realmImport.getRealm(), directGrantFlow + ); + + existingRealm.setDirectGrantFlow(directGrantFlow); + } + } + + private void resetClientAuthenticationFlowIfNeeded(RealmRepresentation existingRealm) { + if (Strings.isNotBlank(clientAuthenticationFlow)) { + logger.debug( + "Reset client-authentication-flow in realm '{}' to '{}'", + realmImport.getRealm(), clientAuthenticationFlow + ); + + existingRealm.setClientAuthenticationFlow(clientAuthenticationFlow); + } + } + + private void resetDockerAuthenticationFlowIfNeeded(RealmRepresentation existingRealm) { + if (Strings.isNotBlank(dockerAuthenticationFlow)) { + logger.debug( + "Reset docker-authentication-flow in realm '{}' to '{}'", + realmImport.getRealm(), dockerAuthenticationFlow + ); + + existingRealm.setDockerAuthenticationFlow(dockerAuthenticationFlow); + } + } + + private void resetRegistrationFlowIfNeeded(RealmRepresentation existingRealm) { + if (Strings.isNotBlank(registrationFlow)) { + logger.debug( + "Reset registration-flow in realm '{}' to '{}'", + realmImport.getRealm(), registrationFlow + ); + + existingRealm.setRegistrationFlow(registrationFlow); + } + } + + private void resetCredentialsFlowIfNeeded(RealmRepresentation existingRealm) { + if (Strings.isNotBlank(resetCredentialsFlow)) { + logger.debug( + "Reset reset-credentials-flow in realm '{}' to '{}'", + realmImport.getRealm(), resetCredentialsFlow + ); + + existingRealm.setResetCredentialsFlow(resetCredentialsFlow); + } + } + + private void resetFirstBrokerLoginFlowsIfNeeded(RealmRepresentation existingRealm) { + for (Map.Entry entry : resetFirstBrokerLoginFlow.entrySet()) { + logger.debug( + "Reset first-broker-login-flow for identity-provider '{}' in realm '{}' to '{}'", + entry.getKey(), realmImport.getRealm(), resetCredentialsFlow + ); + + IdentityProviderRepresentation identityProviderRepresentation = identityProviderRepository + .getByAlias(existingRealm.getRealm(), entry.getKey()); + + identityProviderRepresentation.setFirstBrokerLoginFlowAlias(entry.getValue()); + identityProviderRepository.update(existingRealm.getRealm(), identityProviderRepresentation); + } + } + + private void resetPostBrokerLoginFlowsIfNeeded(RealmRepresentation existingRealm) { + for (Map.Entry entry : resetPostBrokerLoginFlow.entrySet()) { + logger.debug( + "Reset post-broker-login-flow for identity-provider '{}' in realm '{}' to '{}'", + entry.getKey(), realmImport.getRealm(), resetCredentialsFlow + ); + + IdentityProviderRepresentation identityProviderRepresentation = identityProviderRepository + .getByAlias(existingRealm.getRealm(), entry.getKey()); + + identityProviderRepresentation.setPostBrokerLoginFlowAlias(entry.getValue()); + identityProviderRepository.update(existingRealm.getRealm(), identityProviderRepresentation); + } + } + + private void deleteTemporaryCreatedFlow() { + logger.debug("Delete temporary created top-level-flow '{}' in realm '{}'", + TEMPORARY_CREATED_AUTH_FLOW, realmImport.getRealm()); + + AuthenticationFlowRepresentation existingTemporaryCreatedFlow = authenticationFlowRepository + .getByAlias(realmImport.getRealm(), TEMPORARY_CREATED_AUTH_FLOW); + + authenticationFlowRepository.delete(realmImport.getRealm(), existingTemporaryCreatedFlow.getId()); + } + + private AuthenticationFlowRepresentation setupTemporaryCreatedFlow() { + AuthenticationFlowRepresentation tempFlow = new AuthenticationFlowRepresentation(); + + tempFlow.setAlias(TEMPORARY_CREATED_AUTH_FLOW); + tempFlow.setTopLevel(true); + tempFlow.setBuiltIn(false); + tempFlow.setProviderId(TEMPORARY_CREATED_AUTH_FLOW); + + return tempFlow; + } + } +} diff --git a/src/main/java/de/adorsys/keycloak/config/service/AuthenticationFlowsImportService.java b/src/main/java/de/adorsys/keycloak/config/service/AuthenticationFlowsImportService.java index 52fabee73..57bdb1a0c 100644 --- a/src/main/java/de/adorsys/keycloak/config/service/AuthenticationFlowsImportService.java +++ b/src/main/java/de/adorsys/keycloak/config/service/AuthenticationFlowsImportService.java @@ -110,6 +110,7 @@ private void setupFlowsInRealm(RealmImport realmImport) { realm.setDockerAuthenticationFlow(realmImport.getDockerAuthenticationFlow()); realm.setRegistrationFlow(realmImport.getRegistrationFlow()); realm.setResetCredentialsFlow(realmImport.getResetCredentialsFlow()); + realm.setFirstBrokerLoginFlow(realmImport.getFirstBrokerLoginFlow()); realmRepository.update(realm); } diff --git a/src/main/java/de/adorsys/keycloak/config/service/AuthenticationFlowsImportService.java.legacy b/src/main/java/de/adorsys/keycloak/config/service/AuthenticationFlowsImportService.java.legacy new file mode 100644 index 000000000..52fabee73 --- /dev/null +++ b/src/main/java/de/adorsys/keycloak/config/service/AuthenticationFlowsImportService.java.legacy @@ -0,0 +1,347 @@ +/*- + * ---license-start + * keycloak-config-cli + * --- + * Copyright (C) 2017 - 2021 adorsys GmbH & Co. KG @ https://adorsys.com + * --- + * 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.service; + +import de.adorsys.keycloak.config.exception.InvalidImportException; +import de.adorsys.keycloak.config.factory.UsedAuthenticationFlowWorkaroundFactory; +import de.adorsys.keycloak.config.model.RealmImport; +import de.adorsys.keycloak.config.properties.ImportConfigProperties; +import de.adorsys.keycloak.config.properties.ImportConfigProperties.ImportManagedProperties.ImportManagedPropertiesValues; +import de.adorsys.keycloak.config.repository.AuthenticationFlowRepository; +import de.adorsys.keycloak.config.repository.RealmRepository; +import de.adorsys.keycloak.config.util.AuthenticationFlowUtil; +import de.adorsys.keycloak.config.util.CloneUtil; +import org.keycloak.representations.idm.AuthenticationExecutionInfoRepresentation; +import org.keycloak.representations.idm.AuthenticationFlowRepresentation; +import org.keycloak.representations.idm.RealmRepresentation; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * We have to import authentication-flows separately because in case of an existing realmName, keycloak is ignoring or + * not supporting embedded objects in realm-import's property called "authenticationFlows" + *

+ * Glossar: + * topLevel-flow: any flow which has the property 'topLevel' set to 'true'. Can contain execution-flows and executions + * sub-flow: any flow which has the property 'topLevel' set to 'false' and which are related to execution-flows within topLevel-flows + */ +@Service +public class AuthenticationFlowsImportService { + private static final Logger logger = LoggerFactory.getLogger(AuthenticationFlowsImportService.class); + + private final RealmRepository realmRepository; + private final AuthenticationFlowRepository authenticationFlowRepository; + private final ExecutionFlowsImportService executionFlowsImportService; + private final AuthenticatorConfigImportService authenticatorConfigImportService; + private final UsedAuthenticationFlowWorkaroundFactory workaroundFactory; + + private final ImportConfigProperties importConfigProperties; + + @Autowired + public AuthenticationFlowsImportService( + RealmRepository realmRepository, + AuthenticationFlowRepository authenticationFlowRepository, + ExecutionFlowsImportService executionFlowsImportService, + AuthenticatorConfigImportService authenticatorConfigImportService, UsedAuthenticationFlowWorkaroundFactory workaroundFactory, + ImportConfigProperties importConfigProperties + ) { + this.realmRepository = realmRepository; + this.authenticationFlowRepository = authenticationFlowRepository; + this.executionFlowsImportService = executionFlowsImportService; + this.authenticatorConfigImportService = authenticatorConfigImportService; + this.workaroundFactory = workaroundFactory; + this.importConfigProperties = importConfigProperties; + } + + /** + * How the import works: + * - check the authentication flows: + * -- if the flow is not present: create the authentication flow + * -- if the flow is present, check: + * --- if the flow contains any changes: update the authentication flow, which means: delete and recreate the authentication flow + * --- if nothing of above: do nothing + */ + public void doImport(RealmImport realmImport) { + List authenticationFlows = realmImport.getAuthenticationFlows(); + if (authenticationFlows == null) return; + + List topLevelFlowsToImport = AuthenticationFlowUtil.getTopLevelFlows(realmImport); + createOrUpdateTopLevelFlows(realmImport, topLevelFlowsToImport); + updateBuiltInFlows(realmImport, authenticationFlows); + setupFlowsInRealm(realmImport); + + if (importConfigProperties.getManaged().getAuthenticationFlow() == ImportManagedPropertiesValues.FULL) { + deleteTopLevelFlowsMissingInImport(realmImport, topLevelFlowsToImport); + } + } + + private void setupFlowsInRealm(RealmImport realmImport) { + RealmRepresentation realm = realmRepository.get(realmImport.getRealm()); + + realm.setBrowserFlow(realmImport.getBrowserFlow()); + realm.setDirectGrantFlow(realmImport.getDirectGrantFlow()); + realm.setClientAuthenticationFlow(realmImport.getClientAuthenticationFlow()); + realm.setDockerAuthenticationFlow(realmImport.getDockerAuthenticationFlow()); + realm.setRegistrationFlow(realmImport.getRegistrationFlow()); + realm.setResetCredentialsFlow(realmImport.getResetCredentialsFlow()); + + realmRepository.update(realm); + } + + /** + * creates or updates only the top-level flows and its executions or execution-flows + */ + private void createOrUpdateTopLevelFlows(RealmImport realmImport, List topLevelFlowsToImport) { + for (AuthenticationFlowRepresentation topLevelFlowToImport : topLevelFlowsToImport) { + if (!topLevelFlowToImport.isBuiltIn()) { + createOrUpdateTopLevelFlow(realmImport, topLevelFlowToImport); + } + } + } + + /** + * creates or updates only the top-level flow and its executions or execution-flows + */ + private void createOrUpdateTopLevelFlow( + RealmImport realmImport, + AuthenticationFlowRepresentation topLevelFlowToImport + ) { + String alias = topLevelFlowToImport.getAlias(); + + Optional maybeTopLevelFlow = authenticationFlowRepository.searchByAlias(realmImport.getRealm(), alias); + + if (maybeTopLevelFlow.isPresent()) { + AuthenticationFlowRepresentation existingTopLevelFlow = maybeTopLevelFlow.get(); + updateTopLevelFlowIfNeeded(realmImport, topLevelFlowToImport, existingTopLevelFlow); + } else { + createTopLevelFlow(realmImport, topLevelFlowToImport); + } + } + + private void createTopLevelFlow(RealmImport realmImport, AuthenticationFlowRepresentation topLevelFlowToImport) { + logger.debug("Creating top-level flow: {}", topLevelFlowToImport.getAlias()); + authenticationFlowRepository.createTopLevel(realmImport.getRealm(), topLevelFlowToImport); + + AuthenticationFlowRepresentation createdTopLevelFlow = authenticationFlowRepository.getByAlias( + realmImport.getRealm(), topLevelFlowToImport.getAlias() + ); + executionFlowsImportService.createExecutionsAndExecutionFlows(realmImport, topLevelFlowToImport, createdTopLevelFlow); + } + + private void updateTopLevelFlowIfNeeded( + RealmImport realmName, + AuthenticationFlowRepresentation topLevelFlowToImport, + AuthenticationFlowRepresentation existingAuthenticationFlow + ) { + boolean hasToBeUpdated = hasAuthenticationFlowToBeUpdated(topLevelFlowToImport, existingAuthenticationFlow) + || hasAnySubFlowToBeUpdated(realmName, topLevelFlowToImport); + + if (hasToBeUpdated) { + logger.debug("Recreate top-level flow: {}", topLevelFlowToImport.getAlias()); + recreateTopLevelFlow(realmName, topLevelFlowToImport, existingAuthenticationFlow); + } else { + logger.debug("No need to update flow: {}", topLevelFlowToImport.getAlias()); + } + } + + private boolean hasAnySubFlowToBeUpdated( + RealmImport realmImport, + AuthenticationFlowRepresentation topLevelFlowToImport + ) { + List subFlows = getAllSubFlows(realmImport, topLevelFlowToImport); + + for (AuthenticationFlowRepresentation subFlowToImport : subFlows) { + if (isSubFlowNotExistingOrHasToBeUpdated(realmImport, topLevelFlowToImport, subFlowToImport)) { + return true; + } + } + + return false; + } + + private List getAllSubFlows(RealmImport realmImport, + AuthenticationFlowRepresentation topLevelFlowToImport) { + + final List subFlows = AuthenticationFlowUtil.getSubFlowsForTopLevelFlow( + realmImport, topLevelFlowToImport); + final List allSubFlows = new ArrayList<>(subFlows); + + for (AuthenticationFlowRepresentation subflow : subFlows) { + allSubFlows.addAll(getAllSubFlows(realmImport, subflow)); + } + + return allSubFlows; + } + + private boolean isSubFlowNotExistingOrHasToBeUpdated( + RealmImport realmImport, + AuthenticationFlowRepresentation topLevelFlowToImport, + AuthenticationFlowRepresentation subFlowToImport + ) { + Optional maybeSubFlow = authenticationFlowRepository.searchSubFlow( + realmImport.getRealm(), topLevelFlowToImport.getAlias(), subFlowToImport.getAlias() + ); + + return maybeSubFlow + .map(authenticationExecutionInfoRepresentation -> hasExistingSubFlowToBeUpdated( + realmImport, subFlowToImport, authenticationExecutionInfoRepresentation + )) + .orElse(true); + } + + private boolean hasExistingSubFlowToBeUpdated( + RealmImport realmImport, + AuthenticationFlowRepresentation subFlowToImport, + AuthenticationExecutionInfoRepresentation existingSubExecutionFlow + ) { + AuthenticationFlowRepresentation existingSubFlow = authenticationFlowRepository.getFlowById( + realmImport.getRealm(), existingSubExecutionFlow.getFlowId() + ); + + return hasAuthenticationFlowToBeUpdated(subFlowToImport, existingSubFlow); + } + + /** + * Checks if the authentication flow to import and the existing representation differs in any property except "id" and: + * + * @param authenticationFlowToImport the top-level or non-top-level flow coming from import file + * @param existingAuthenticationFlow the existing top-level or non-top-level flow in keycloak + * @return true if there is any change, false if not + */ + private boolean hasAuthenticationFlowToBeUpdated( + AuthenticationFlowRepresentation authenticationFlowToImport, + AuthenticationFlowRepresentation existingAuthenticationFlow + ) { + return !CloneUtil.deepEquals( + authenticationFlowToImport, + existingAuthenticationFlow, + "id" + ); + } + + private void updateBuiltInFlows( + RealmImport realmImport, + List flowsToImport + ) { + for (AuthenticationFlowRepresentation flowToImport : flowsToImport) { + if (!flowToImport.isBuiltIn()) continue; + + String flowAlias = flowToImport.getAlias(); + Optional maybeFlow = authenticationFlowRepository + .searchByAlias(realmImport.getRealm(), flowAlias); + + if (maybeFlow.isEmpty()) { + throw new InvalidImportException(String.format( + "Cannot create flow '%s' in realm '%s': Unable to create built-in flows.", + flowToImport.getAlias(), realmImport.getRealm() + )); + } + + AuthenticationFlowRepresentation existingFlow = maybeFlow.get(); + if (hasAuthenticationFlowToBeUpdated(flowToImport, existingFlow)) { + logger.debug("Updating builtin flow: {}", flowToImport.getAlias()); + updateBuiltInFlow(realmImport, flowToImport, existingFlow); + } + } + } + + private void updateBuiltInFlow( + RealmImport realmImport, + AuthenticationFlowRepresentation topLevelFlowToImport, + AuthenticationFlowRepresentation existingAuthenticationFlow + ) { + if (!existingAuthenticationFlow.isBuiltIn()) { + throw new InvalidImportException(String.format( + "Unable to update flow '%s' in realm '%s': Change built-in flag is not possible", + topLevelFlowToImport.getAlias(), realmImport.getRealm() + )); + } + AuthenticationFlowRepresentation patchedAuthenticationFlow = CloneUtil.patch( + existingAuthenticationFlow, topLevelFlowToImport, "id" + ); + + authenticationFlowRepository.update(realmImport.getRealm(), patchedAuthenticationFlow); + + executionFlowsImportService.updateExecutionFlows(realmImport, topLevelFlowToImport); + } + + /** + * Deletes the top-level flow and all its executions and recreates them. + */ + private void recreateTopLevelFlow( + RealmImport realmImport, + AuthenticationFlowRepresentation topLevelFlowToImport, + AuthenticationFlowRepresentation existingAuthenticationFlow + ) { + AuthenticationFlowRepresentation patchedAuthenticationFlow = CloneUtil.patch( + existingAuthenticationFlow, topLevelFlowToImport, "id" + ); + + if (existingAuthenticationFlow.isBuiltIn()) { + throw new InvalidImportException(String.format( + "Unable to recreate flow '%s' in realm '%s': Deletion or creation of built-in flows is not possible", + patchedAuthenticationFlow.getAlias(), realmImport.getRealm() + )); + } + + UsedAuthenticationFlowWorkaroundFactory.UsedAuthenticationFlowWorkaround workaround = workaroundFactory.buildFor(realmImport); + workaround.disableTopLevelFlowIfNeeded(topLevelFlowToImport.getAlias()); + + authenticatorConfigImportService.deleteAuthenticationConfigs(realmImport, patchedAuthenticationFlow); + authenticationFlowRepository.delete(realmImport.getRealm(), patchedAuthenticationFlow.getId()); + authenticationFlowRepository.createTopLevel(realmImport.getRealm(), patchedAuthenticationFlow); + + AuthenticationFlowRepresentation createdTopLevelFlow = authenticationFlowRepository.getByAlias( + realmImport.getRealm(), topLevelFlowToImport.getAlias() + ); + executionFlowsImportService.createExecutionsAndExecutionFlows(realmImport, topLevelFlowToImport, createdTopLevelFlow); + + workaround.resetFlowIfNeeded(); + } + + private void deleteTopLevelFlowsMissingInImport( + RealmImport realmImport, + List importedTopLevelFlows + ) { + String realmName = realmImport.getRealm(); + List existingTopLevelFlows = authenticationFlowRepository.getTopLevelFlows(realmName) + .stream().filter(flow -> !flow.isBuiltIn()).toList(); + + Set topLevelFlowsToImportAliases = importedTopLevelFlows.stream() + .map(AuthenticationFlowRepresentation::getAlias) + .collect(Collectors.toSet()); + + for (AuthenticationFlowRepresentation existingTopLevelFlow : existingTopLevelFlows) { + if (topLevelFlowsToImportAliases.contains(existingTopLevelFlow.getAlias())) continue; + + logger.debug("Delete authentication flow: {}", existingTopLevelFlow.getAlias()); + authenticationFlowRepository.delete(realmName, existingTopLevelFlow.getId()); + } + } +} diff --git a/src/main/java/de/adorsys/keycloak/config/service/RealmImportService.java b/src/main/java/de/adorsys/keycloak/config/service/RealmImportService.java index 14b257063..46843101a 100644 --- a/src/main/java/de/adorsys/keycloak/config/service/RealmImportService.java +++ b/src/main/java/de/adorsys/keycloak/config/service/RealmImportService.java @@ -59,6 +59,7 @@ public class RealmImportService { "defaultOptionalClientScopes", "clientProfiles", "clientPolicies", + "firstBrokerLoginFlow", }; private static final Logger logger = LoggerFactory.getLogger(RealmImportService.class); diff --git a/src/test/java/de/adorsys/keycloak/config/service/ImportAuthenticationFlowsIT.java b/src/test/java/de/adorsys/keycloak/config/service/ImportAuthenticationFlowsIT.java index 2450d513f..2c9ed11ef 100644 --- a/src/test/java/de/adorsys/keycloak/config/service/ImportAuthenticationFlowsIT.java +++ b/src/test/java/de/adorsys/keycloak/config/service/ImportAuthenticationFlowsIT.java @@ -41,10 +41,12 @@ import static org.hamcrest.core.Is.is; import static org.hamcrest.core.IsNull.nullValue; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assumptions.assumeTrue; @SuppressWarnings({"java:S5961", "java:S5976", "deprecation"}) class ImportAuthenticationFlowsIT extends AbstractImportIT { private static final String REALM_NAME = "realmWithFlow"; + private static final String DEFAULT_FLOW_REALM_NAME = "realmWithDefaultFlow"; ImportAuthenticationFlowsIT() { this.resourcePath = "import-files/auth-flows"; @@ -1210,6 +1212,35 @@ void shouldChangeSubFlowOfFirstBrokerLoginFlow() throws IOException { assertThat(flow.getAuthenticationExecutions().get(1).getRequirement(), is("DISABLED")); } + @Test + void shouldSetCustomFirstBrokerLoginFlowAsDefaultFlow() throws IOException { + assumeTrue(VersionUtil.ge(KEYCLOAK_VERSION,"24")); // was introduced with KC 24 + + doImport("init_custom_default_first-broker-login-flow.json"); + + RealmRepresentation realm = keycloakProvider.getInstance().realm(DEFAULT_FLOW_REALM_NAME).partialExport(true, true); + + assertThat(realm.getRealm(), is(DEFAULT_FLOW_REALM_NAME)); + assertThat(realm.isEnabled(), is(true)); + assertThat(realm.getFirstBrokerLoginFlow(), is("my auth flow")); + } + + @Test + void shouldUpdateCustomFirstBrokerLoginFlowWhenSetAsDefault() throws IOException { + assumeTrue(VersionUtil.ge(KEYCLOAK_VERSION,"24")); // was introduced with KC 24 + + doImport("init_custom_default_first-broker-login-flow.json"); + doImport("updated_custom_default_first-broker-login-flow.json"); + + RealmRepresentation realm = keycloakProvider.getInstance().realm(DEFAULT_FLOW_REALM_NAME).partialExport(true, true); + AuthenticationFlowRepresentation flow = getAuthenticationFlow(realm, "my auth flow"); + + assertThat(realm.getRealm(), is(DEFAULT_FLOW_REALM_NAME)); + assertThat(realm.isEnabled(), is(true)); + assertThat(realm.getFirstBrokerLoginFlow(), is("my auth flow")); + assertThat(flow.getAuthenticationExecutions().getFirst().getAuthenticator(), is("idp-auto-link")); + } + private List getExecutionFromFlow(AuthenticationFlowRepresentation flow, String executionAuthenticator) { List executions = flow.getAuthenticationExecutions(); diff --git a/src/test/resources/import-files/auth-flows/init_custom_default_first-broker-login-flow.json b/src/test/resources/import-files/auth-flows/init_custom_default_first-broker-login-flow.json new file mode 100644 index 000000000..48f02a814 --- /dev/null +++ b/src/test/resources/import-files/auth-flows/init_custom_default_first-broker-login-flow.json @@ -0,0 +1,24 @@ +{ + "enabled": true, + "realm": "realmWithDefaultFlow", + "firstBrokerLoginFlow": "my auth flow", + "authenticationFlows": [ + { + "alias": "my auth flow", + "description": "My auth flow for testing", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": false, + "authenticationExecutions": [ + { + "authenticator": "docker-http-basic-authenticator", + "requirement": "REQUIRED", + "priority": 0, + "userSetupAllowed": true, + "autheticatorFlow": false, + "authenticatorFlow": false + } + ] + } + ] +} diff --git a/src/test/resources/import-files/auth-flows/updated_custom_default_first-broker-login-flow.json b/src/test/resources/import-files/auth-flows/updated_custom_default_first-broker-login-flow.json new file mode 100644 index 000000000..6aa379b6a --- /dev/null +++ b/src/test/resources/import-files/auth-flows/updated_custom_default_first-broker-login-flow.json @@ -0,0 +1,24 @@ +{ + "enabled": true, + "realm": "realmWithDefaultFlow", + "firstBrokerLoginFlow": "my auth flow", + "authenticationFlows": [ + { + "alias": "my auth flow", + "description": "My auth flow for testing", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": false, + "authenticationExecutions": [ + { + "authenticator": "idp-auto-link", + "requirement": "REQUIRED", + "priority": 0, + "userSetupAllowed": false, + "autheticatorFlow": false, + "authenticatorFlow": false + } + ] + } + ] +}