diff --git a/.gitignore b/.gitignore index 336b753a9..835791c49 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,4 @@ release.properties /*.json /test* +/exports diff --git a/docs/FEATURES.md b/docs/FEATURES.md index 1c5a7bb4e..7e1d30284 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -55,6 +55,7 @@ | Remove clientScopeMappings | 2.5.0 | Remove existing clientScopeMappings while creating or updating realms | | Synchronize user federation | 3.5.0 | Synchronize the user federation defined on the realm configuration | | Synchronize user profile | 5.4.0 | Synchronize the user profile configuration defined on the realm configuration | +| Normalize realm exports | x.x.x | Normalize a full realm export to be more minimal | # Specificities diff --git a/docs/NORMALIZE.md b/docs/NORMALIZE.md new file mode 100644 index 000000000..0df106179 --- /dev/null +++ b/docs/NORMALIZE.md @@ -0,0 +1,123 @@ +# Realm normalization + +Realm normalization is a feature that is supposed to aid users in migrating from an "unmanaged" Keycloak installation, +to an installation managed by keycloak-config-cli. +To achieve this, it uses a full [realm export](https://www.keycloak.org/server/importExport#_exporting_a_specific_realm) +as an input, and only retains things that deviate from the default + +## Usage + +To run the normalization, run keycloak-config-cli with the CLI option `--run.operation=NORMALIZE`. +The default value for this option is `IMPORT`, which will run the regular keycloak-config-cli import. + +### Configuration options + +| Configuration key | Purpose | Example | +|--------------------------------------|-----------------------------------------------------------------------------------------------------------------|---------------| +| run.operation | Tell keycloak-config-cli to normalize, rather than import | NORMALIZE | +| normalization.files.input-locations | Which realm files to import | See IMPORT.md | +| normalization.files.output-directory | Where to save output realm files | ./exports/out | +| normalization.output-format | Whether to output JSON or YAML. Default value is YAML | YAML | +| normalization.fallback-version | Use this version as a baseline of keycloak version in realm is not available as baseline in keycloak-config-cli | 19.0.3 | + +### Unimplemented Features +- Components: + - Currently, keycloak-config-cli will not yet look at the `components` section of the exported JSON + - Therefore, some things (like LDAP federation configs and Key providers) are missing from the normalized YAML +- Users + - Users are not currently considered by normalization. + +## Missing entries +keycloak-config-cli will WARN if components that are present in a realm by default are missing from an exported realm. +An example of such a message is: +``` +Default realm requiredAction 'webauthn-register-passwordless' was deleted in exported realm. It may be reintroduced during import +``` +Messages like these will often show up when using keycloak-config-cli to normalize an import from an older version of Keycloak, and compared to a newer baseline. +In the above case, the Keycloak version is 18.0.3, and the baseline for comparison was 19.0.3. +Since the webauthn feature was not present (or enabled by default) in the older version, this message is mostly informative. +If a message like this appears on a component that was *not* deleted or should generally be present, this may indicate a bug in keycloak-config-cli. + +## Cleaning of invalid data +Sometimes, realms of existing installations may contain invalid data, due to faulty migrations, or due to direct interaction with the database, +rather than the Keycloak API. +When such problems are found, we attempt to handle them in keycloak-config-cli, or at least notify the user about the existence of these problems. + +### SAML Attributes on clients +While not necessarily invalid, openid-connect clients that were created on older Keycloak versions, will sometimes contain +SAML-related attributes. These are filtered out by keycloak-config-cli. + +### Unused non-top-level Authentication Flows +Authentication flows in Keycloak are marked as top-level if they are supposed to be available for binding or overrides. +Authentication flows that are not marked as top-level are used as sub-flows in other authentication flows. +The normalization process recognizes recursively whether there are authentication flows that are not top level and not used +by any top level flow, and does not include them in the final result. + +A warning message is logged, and you can use the following SQL query to find any authentication flows that are not referenced. +Note that this query, unlike keycloak-config-cli, is not recursive. +That means that after deleting an unused flow, additional unused flows may appear after the query is performed again. + +```sql +select flow.alias +from authentication_flow flow + join realm r on flow.realm_id = r.id + left join authentication_execution execution on flow.id = execution.auth_flow_id +where r.name = 'mytest' + and execution.id is null + and not flow.top_level +``` + +### Unused and duplicate Authenticator Configs +Authenticator Configs are not useful if they are not referenced by at least one authentication execution. +Therefore, keycloak-config-cli detects unused configurations and does not include them in the resulting output. +Note that the check for unused configs runs *after* the check for unused flows. +That means a config will be detected as unused if it is referenced by an execution that is part of a flow that is unused. + +A warning message is logged on duplicate or unused configs, and you can use the following SQL query to find any configs +that are unused: + +```sql +select ac.alias, ac.id +from authenticator_config ac + left join authentication_execution ae on ac.id = ae.auth_config + left join authentication_flow af on ae.flow_id = af.id + join realm r on ac.realm_id = r.id +where r.name = 'master' and af.alias is null +order by ac.alias +``` + +And the following query to find duplicates: + +```sql +select alias, count(alias), r.name as realm_name +from authenticator_config + join realm r on realm_id = r.id +group by alias, r.name +having count(alias) > 1 +``` + +If the `af.id` and `af.alias` fields are `null`, the config in question is not in use. +Note that configs used by unused flows are not marked as unused in the SQL result, as these need to be deleted first +to become unused. +After the unused flows (and executions) are deleted, the configs will be marked as unused and can also be deleted. + +### Authentication Executions with invalid subflows +Some keycloak exports have invalid authentication executions that reference a subflow, while also setting an authenticator. +This is only a valid configuration if the subflow's type is `form-flow`. +If it is not, then keycloak-config-cli will not import the configuration. +This will be marked by an ERROR severity message in the log output. +You can use this SQL query to find offending entries and remediate the configuration errors before continuing. + +```sql +select parent.alias, + subflow.alias, + execution.alias +from authentication_execution execution + join realm r on execution.realm_id = r.id + join authentication_flow parent on execution.flow_id = parent.id + join authentication_flow subflow on execution.auth_flow_id = subflow.id +where execution.auth_flow_id is not null + and execution.authenticator is not null + and subflow.provider_id <> 'form-flow' + and r.name = 'REALMNAME'; +``` diff --git a/pom.xml b/pom.xml index cb12d887b..050d835de 100644 --- a/pom.xml +++ b/pom.xml @@ -59,7 +59,7 @@ UTF-8 UTF-8 - 21.0.1 + 18.0.2 3.2.0 10.0 @@ -153,6 +153,12 @@ failsafe ${failsafe.version} + + + org.javers + javers-core + 6.8.0 + @@ -230,6 +236,16 @@ jackson-databind + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + + + + org.javers + javers-core + + org.yaml snakeyaml diff --git a/src/main/java/de/adorsys/keycloak/config/KeycloakConfigApplication.java b/src/main/java/de/adorsys/keycloak/config/KeycloakConfigApplication.java index 87784b03c..f17777978 100644 --- a/src/main/java/de/adorsys/keycloak/config/KeycloakConfigApplication.java +++ b/src/main/java/de/adorsys/keycloak/config/KeycloakConfigApplication.java @@ -20,14 +20,13 @@ package de.adorsys.keycloak.config; -import de.adorsys.keycloak.config.properties.ImportConfigProperties; -import de.adorsys.keycloak.config.properties.KeycloakConfigProperties; +import de.adorsys.keycloak.config.properties.RunConfigProperties; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.properties.EnableConfigurationProperties; @SpringBootApplication(proxyBeanMethods = false) -@EnableConfigurationProperties({KeycloakConfigProperties.class, ImportConfigProperties.class}) +@EnableConfigurationProperties(RunConfigProperties.class) public class KeycloakConfigApplication { public static void main(String[] args) { // https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-application-exit diff --git a/src/main/java/de/adorsys/keycloak/config/KeycloakConfigNormalizationRunner.java b/src/main/java/de/adorsys/keycloak/config/KeycloakConfigNormalizationRunner.java new file mode 100644 index 000000000..44b410b79 --- /dev/null +++ b/src/main/java/de/adorsys/keycloak/config/KeycloakConfigNormalizationRunner.java @@ -0,0 +1,126 @@ +/*- + * ---license-start + * keycloak-config-cli + * --- + * Copyright (C) 2017 - 2022 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; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLMapper; +import de.adorsys.keycloak.config.properties.NormalizationConfigProperties; +import de.adorsys.keycloak.config.properties.NormalizationKeycloakConfigProperties; +import de.adorsys.keycloak.config.provider.KeycloakExportProvider; +import de.adorsys.keycloak.config.service.normalize.RealmNormalizationService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.ExitCodeGenerator; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.io.FileOutputStream; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.text.SimpleDateFormat; +import java.util.Date; + +import static de.adorsys.keycloak.config.properties.NormalizationConfigProperties.OutputFormat.YAML; + +@Component +@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "NORMALIZE") +@EnableConfigurationProperties({NormalizationConfigProperties.class, NormalizationKeycloakConfigProperties.class}) +public class KeycloakConfigNormalizationRunner implements CommandLineRunner, ExitCodeGenerator { + + private static final Logger logger = LoggerFactory.getLogger(KeycloakConfigNormalizationRunner.class); + private static final long START_TIME = System.currentTimeMillis(); + + private final RealmNormalizationService normalizationService; + private final KeycloakExportProvider exportProvider; + private final NormalizationConfigProperties normalizationConfigProperties; + private final YAMLMapper yamlMapper; + private final ObjectMapper objectMapper; + private int exitCode; + + @Autowired + public KeycloakConfigNormalizationRunner(RealmNormalizationService normalizationService, + KeycloakExportProvider exportProvider, + NormalizationConfigProperties normalizationConfigProperties, + YAMLMapper yamlMapper, + ObjectMapper objectMapper) { + this.normalizationService = normalizationService; + this.exportProvider = exportProvider; + this.normalizationConfigProperties = normalizationConfigProperties; + this.yamlMapper = yamlMapper; + this.objectMapper = objectMapper; + } + + @Override + public void run(String... args) throws Exception { + try { + var outputLocation = Paths.get(normalizationConfigProperties.getFiles().getOutputDirectory()); + if (!Files.exists(outputLocation)) { + logger.info("Creating output directory '{}'", outputLocation); + Files.createDirectories(outputLocation); + } + if (!Files.isDirectory(outputLocation)) { + logger.error("Output location '{}' is not a directory. Aborting", outputLocation); + exitCode = 1; + return; + } + + for (var exportLocations : exportProvider.readFromLocations().values()) { + for (var export : exportLocations.entrySet()) { + logger.info("Normalizing file '{}'", export.getKey()); + for (var realm : export.getValue()) { + var normalizedRealm = normalizationService.normalizeRealm(realm); + var suffix = normalizationConfigProperties.getOutputFormat() == YAML ? "yaml" : "json"; + var outputFile = outputLocation.resolve(String.format("%s.%s", normalizedRealm.getRealm(), suffix)); + try (var os = new FileOutputStream(outputFile.toFile())) { + if (normalizationConfigProperties.getOutputFormat() == YAML) { + yamlMapper.writeValue(os, normalizedRealm); + } else { + objectMapper.writeValue(os, normalizedRealm); + } + } + } + } + } + } catch (NullPointerException e) { + throw e; + } catch (Exception e) { + logger.error(e.getMessage()); + + exitCode = 1; + + if (logger.isDebugEnabled()) { + throw e; + } + } finally { + long totalTime = System.currentTimeMillis() - START_TIME; + String formattedTime = new SimpleDateFormat("mm:ss.SSS").format(new Date(totalTime)); + logger.info("keycloak-config-cli running in {}.", formattedTime); + } + } + + @Override + public int getExitCode() { + return exitCode; + } +} diff --git a/src/main/java/de/adorsys/keycloak/config/KeycloakConfigRunner.java b/src/main/java/de/adorsys/keycloak/config/KeycloakConfigRunner.java index ddda773c0..f482847ca 100644 --- a/src/main/java/de/adorsys/keycloak/config/KeycloakConfigRunner.java +++ b/src/main/java/de/adorsys/keycloak/config/KeycloakConfigRunner.java @@ -23,6 +23,7 @@ import de.adorsys.keycloak.config.model.KeycloakImport; import de.adorsys.keycloak.config.model.RealmImport; import de.adorsys.keycloak.config.properties.ImportConfigProperties; +import de.adorsys.keycloak.config.properties.KeycloakConfigProperties; import de.adorsys.keycloak.config.provider.KeycloakImportProvider; import de.adorsys.keycloak.config.service.RealmImportService; import org.slf4j.Logger; @@ -30,6 +31,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.ExitCodeGenerator; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.stereotype.Component; import java.text.SimpleDateFormat; @@ -39,6 +42,13 @@ import java.util.Map; @Component +/* + * Spring only considers actual properties set, not default values of @ConfigurationProperties classes. + * Therefore, we enable matchIfMissing here, so if there is *no* property set, we consider it an import + * for backwards compatibility + */ +@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "IMPORT", matchIfMissing = true) +@EnableConfigurationProperties({ImportConfigProperties.class, KeycloakConfigProperties.class}) public class KeycloakConfigRunner implements CommandLineRunner, ExitCodeGenerator { private static final Logger logger = LoggerFactory.getLogger(KeycloakConfigRunner.class); private static final long START_TIME = System.currentTimeMillis(); diff --git a/src/main/java/de/adorsys/keycloak/config/configuration/NormalizationConfiguration.java b/src/main/java/de/adorsys/keycloak/config/configuration/NormalizationConfiguration.java new file mode 100644 index 000000000..21a43888a --- /dev/null +++ b/src/main/java/de/adorsys/keycloak/config/configuration/NormalizationConfiguration.java @@ -0,0 +1,137 @@ +/*- + * ---license-start + * keycloak-config-cli + * --- + * Copyright (C) 2017 - 2022 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.configuration; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator; +import com.fasterxml.jackson.dataformat.yaml.YAMLMapper; +import org.javers.core.Javers; +import org.javers.core.JaversBuilder; +import org.javers.core.diff.ListCompareAlgorithm; +import org.javers.core.metamodel.clazz.EntityDefinition; +import org.javers.core.metamodel.clazz.EntityDefinitionBuilder; +import org.keycloak.representations.idm.AuthenticationFlowRepresentation; +import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.ClientScopeRepresentation; +import org.keycloak.representations.idm.ComponentExportRepresentation; +import org.keycloak.representations.idm.GroupRepresentation; +import org.keycloak.representations.idm.IdentityProviderMapperRepresentation; +import org.keycloak.representations.idm.IdentityProviderRepresentation; +import org.keycloak.representations.idm.ProtocolMapperRepresentation; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.representations.idm.RequiredActionProviderRepresentation; +import org.keycloak.representations.idm.RoleRepresentation; +import org.keycloak.representations.idm.UserFederationMapperRepresentation; +import org.keycloak.representations.idm.UserFederationProviderRepresentation; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.ArrayList; +import java.util.List; + +@Configuration +@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "NORMALIZE") +public class NormalizationConfiguration { + + @Bean + public Javers javers() { + return commonJavers() + .withListCompareAlgorithm(ListCompareAlgorithm.LEVENSHTEIN_DISTANCE) + .build(); + } + + @Bean + public Javers unOrderedJavers() { + return commonJavers() + .withListCompareAlgorithm(ListCompareAlgorithm.AS_SET) + .build(); + } + + @Bean + public YAMLMapper yamlMapper() { + var ym = new YAMLMapper(); + ym.setSerializationInclusion(JsonInclude.Include.NON_NULL); + ym.enable(SerializationFeature.INDENT_OUTPUT); + ym.enable(YAMLGenerator.Feature.INDENT_ARRAYS_WITH_INDICATOR); + return ym; + } + + @Bean + public ObjectMapper objectMapper() { + var om = new ObjectMapper(); + om.setSerializationInclusion(JsonInclude.Include.NON_NULL); + om.enable(SerializationFeature.INDENT_OUTPUT); + return om; + } + + private JaversBuilder commonJavers() { + var realmIgnoredProperties = new ArrayList(); + realmIgnoredProperties.add("id"); + realmIgnoredProperties.add("groups"); + realmIgnoredProperties.add("roles"); + realmIgnoredProperties.add("defaultRole"); + realmIgnoredProperties.add("clientProfiles"); // + realmIgnoredProperties.add("clientPolicies"); // + realmIgnoredProperties.add("users"); + realmIgnoredProperties.add("federatedUsers"); + realmIgnoredProperties.add("scopeMappings"); // + realmIgnoredProperties.add("clientScopeMappings"); // + realmIgnoredProperties.add("clients"); // + realmIgnoredProperties.add("clientScopes"); // + realmIgnoredProperties.add("userFederationProviders"); + realmIgnoredProperties.add("userFederationMappers"); + realmIgnoredProperties.add("identityProviders"); + realmIgnoredProperties.add("identityProviderMappers"); + realmIgnoredProperties.add("protocolMappers"); // + realmIgnoredProperties.add("components"); + realmIgnoredProperties.add("authenticationFlows"); + realmIgnoredProperties.add("authenticatorConfig"); + realmIgnoredProperties.add("requiredActions"); + realmIgnoredProperties.add("applicationScopeMappings"); + realmIgnoredProperties.add("applications"); + realmIgnoredProperties.add("oauthClients"); + realmIgnoredProperties.add("clientTemplates"); + realmIgnoredProperties.add("attributes"); + + return JaversBuilder.javers() + .registerEntity(new EntityDefinition(RealmRepresentation.class, "realm", realmIgnoredProperties)) + .registerEntity(new EntityDefinition(ClientRepresentation.class, "clientId", + List.of("id", "authorizationSettings", "protocolMappers"))) + .registerEntity(new EntityDefinition(ProtocolMapperRepresentation.class, "name", List.of("id"))) + .registerEntity(new EntityDefinition(ClientScopeRepresentation.class, "name", List.of("id", "protocolMappers"))) + .registerEntity(new EntityDefinition(RoleRepresentation.class, "name", List.of("id", "containerId", "composites", "attributes"))) + .registerEntity(new EntityDefinition(GroupRepresentation.class, "path", List.of("id", "subGroups", "attributes", "clientRoles"))) + .registerEntity(new EntityDefinition(AuthenticationFlowRepresentation.class, "alias", List.of("id", "authenticationExecutions"))) + .registerEntity(new EntityDefinition(IdentityProviderRepresentation.class, "alias", List.of("internalId"))) + .registerEntity(EntityDefinitionBuilder.entityDefinition(IdentityProviderMapperRepresentation.class) + .withIdPropertyNames("name", "identityProviderAlias") + .withIgnoredProperties("id").build()) + .registerEntity(new EntityDefinition(RequiredActionProviderRepresentation.class, "alias")) + .registerEntity(new EntityDefinition(UserFederationProviderRepresentation.class, "displayName", List.of("id"))) + .registerEntity(EntityDefinitionBuilder.entityDefinition(UserFederationMapperRepresentation.class) + .withIdPropertyNames("name", "federationProviderDisplayName") + .withIgnoredProperties("id").build()) + .registerEntity(new EntityDefinition(ComponentExportRepresentation.class, "name", List.of("id", "subComponents", "config"))); + } +} diff --git a/src/main/java/de/adorsys/keycloak/config/exception/NormalizationException.java b/src/main/java/de/adorsys/keycloak/config/exception/NormalizationException.java new file mode 100644 index 000000000..6261834b8 --- /dev/null +++ b/src/main/java/de/adorsys/keycloak/config/exception/NormalizationException.java @@ -0,0 +1,31 @@ +/*- + * ---license-start + * keycloak-config-cli + * --- + * Copyright (C) 2017 - 2022 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.exception; + +public class NormalizationException extends RuntimeException { + public NormalizationException(String message, Throwable cause) { + super(message, cause); + } + + public NormalizationException(String message) { + super(message); + } +} 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..911fc53a1 100644 --- a/src/main/java/de/adorsys/keycloak/config/factory/UsedAuthenticationFlowWorkaroundFactory.java +++ b/src/main/java/de/adorsys/keycloak/config/factory/UsedAuthenticationFlowWorkaroundFactory.java @@ -31,11 +31,13 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; import java.util.*; @Service +@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "IMPORT", matchIfMissing = true) public class UsedAuthenticationFlowWorkaroundFactory { private final RealmRepository realmRepository; diff --git a/src/main/java/de/adorsys/keycloak/config/model/AuthenticationFlowImport.java b/src/main/java/de/adorsys/keycloak/config/model/AuthenticationFlowImport.java index 2dba38141..9f4de757f 100644 --- a/src/main/java/de/adorsys/keycloak/config/model/AuthenticationFlowImport.java +++ b/src/main/java/de/adorsys/keycloak/config/model/AuthenticationFlowImport.java @@ -22,6 +22,7 @@ import org.keycloak.representations.idm.AuthenticationExecutionExportRepresentation; import org.keycloak.representations.idm.AuthenticationFlowRepresentation; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Component; import java.io.Serializable; @@ -33,6 +34,7 @@ */ @Component +@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "IMPORT", matchIfMissing = true) public class AuthenticationFlowImport extends AuthenticationFlowRepresentation { private static final Comparator COMPARATOR = new AuthenticationExecutionExportRepresentationComparator(); diff --git a/src/main/java/de/adorsys/keycloak/config/model/RealmImport.java b/src/main/java/de/adorsys/keycloak/config/model/RealmImport.java index 2d4666b35..1b718572e 100644 --- a/src/main/java/de/adorsys/keycloak/config/model/RealmImport.java +++ b/src/main/java/de/adorsys/keycloak/config/model/RealmImport.java @@ -24,6 +24,7 @@ import com.fasterxml.jackson.annotation.JsonSetter; import org.keycloak.representations.idm.AuthenticationFlowRepresentation; import org.keycloak.representations.idm.RealmRepresentation; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Component; import java.util.ArrayList; @@ -31,6 +32,7 @@ import java.util.List; @Component +@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "IMPORT", matchIfMissing = true) public class RealmImport extends RealmRepresentation { private List authenticationFlowImports; 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 6370776b8..b6346c6d0 100644 --- a/src/main/java/de/adorsys/keycloak/config/properties/ImportConfigProperties.java +++ b/src/main/java/de/adorsys/keycloak/config/properties/ImportConfigProperties.java @@ -22,6 +22,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConstructorBinding; +import org.springframework.boot.context.properties.bind.DefaultValue; import org.springframework.validation.annotation.Validated; import java.util.Collection; @@ -62,10 +63,14 @@ public class ImportConfigProperties { @Valid private final ImportRemoteStateProperties remoteState; - public ImportConfigProperties(boolean validate, boolean parallel, - ImportFilesProperties files, ImportVarSubstitutionProperties varSubstitution, - ImportBehaviorsProperties behaviors, ImportCacheProperties cache, ImportManagedProperties managed, - ImportRemoteStateProperties remoteState + public ImportConfigProperties(@DefaultValue("true") boolean validate, + @DefaultValue("false") boolean parallel, + @DefaultValue ImportFilesProperties files, + @DefaultValue ImportVarSubstitutionProperties varSubstitution, + @DefaultValue ImportBehaviorsProperties behaviors, + @DefaultValue ImportCacheProperties cache, + @DefaultValue ImportManagedProperties managed, + @DefaultValue ImportRemoteStateProperties remoteState ) { this.validate = validate; this.parallel = parallel; @@ -150,13 +155,19 @@ public static class ImportManagedProperties { @NotNull private final ImportManagedPropertiesValues clientAuthorizationResources; - public ImportManagedProperties(ImportManagedPropertiesValues requiredAction, ImportManagedPropertiesValues group, - ImportManagedPropertiesValues clientScope, ImportManagedPropertiesValues scopeMapping, - ImportManagedPropertiesValues clientScopeMapping, ImportManagedPropertiesValues component, - ImportManagedPropertiesValues subComponent, ImportManagedPropertiesValues authenticationFlow, - ImportManagedPropertiesValues identityProvider, ImportManagedPropertiesValues identityProviderMapper, - ImportManagedPropertiesValues role, ImportManagedPropertiesValues client, - ImportManagedPropertiesValues clientAuthorizationResources) { + public ImportManagedProperties(@DefaultValue("FULL") ImportManagedPropertiesValues requiredAction, + @DefaultValue("FULL") ImportManagedPropertiesValues group, + @DefaultValue("FULL") ImportManagedPropertiesValues clientScope, + @DefaultValue("FULL") ImportManagedPropertiesValues scopeMapping, + @DefaultValue("FULL") ImportManagedPropertiesValues clientScopeMapping, + @DefaultValue("FULL") ImportManagedPropertiesValues component, + @DefaultValue("FULL") ImportManagedPropertiesValues subComponent, + @DefaultValue("FULL") ImportManagedPropertiesValues authenticationFlow, + @DefaultValue("FULL") ImportManagedPropertiesValues identityProvider, + @DefaultValue("FULL") ImportManagedPropertiesValues identityProviderMapper, + @DefaultValue("FULL") ImportManagedPropertiesValues role, + @DefaultValue("FULL") ImportManagedPropertiesValues client, + @DefaultValue("FULL") ImportManagedPropertiesValues clientAuthorizationResources) { this.requiredAction = requiredAction; this.group = group; this.clientScope = clientScope; @@ -240,7 +251,9 @@ public static class ImportFilesProperties { @NotNull private final boolean includeHiddenFiles; - public ImportFilesProperties(Collection locations, Collection excludes, boolean includeHiddenFiles) { + public ImportFilesProperties(Collection locations, + @DefaultValue Collection excludes, + @DefaultValue("false") boolean includeHiddenFiles) { this.locations = locations; this.excludes = excludes; this.includeHiddenFiles = includeHiddenFiles; @@ -276,7 +289,11 @@ public static class ImportVarSubstitutionProperties { @NotNull private final String suffix; - public ImportVarSubstitutionProperties(boolean enabled, boolean nested, boolean undefinedIsError, String prefix, String suffix) { + public ImportVarSubstitutionProperties(@DefaultValue("false") boolean enabled, + @DefaultValue("true") boolean nested, + @DefaultValue("true") boolean undefinedIsError, + @DefaultValue("$(") String prefix, + @DefaultValue(")") String suffix) { this.enabled = enabled; this.nested = nested; this.undefinedIsError = undefinedIsError; @@ -316,7 +333,9 @@ public static class ImportBehaviorsProperties { @NotNull private final boolean skipAttributesForFederatedUser; - public ImportBehaviorsProperties(boolean syncUserFederation, boolean removeDefaultRoleFromUser, boolean skipAttributesForFederatedUser) { + public ImportBehaviorsProperties(@DefaultValue("false") boolean syncUserFederation, + @DefaultValue("false") boolean removeDefaultRoleFromUser, + @DefaultValue("false") boolean skipAttributesForFederatedUser) { this.syncUserFederation = syncUserFederation; this.removeDefaultRoleFromUser = removeDefaultRoleFromUser; this.skipAttributesForFederatedUser = skipAttributesForFederatedUser; @@ -343,7 +362,8 @@ public static class ImportCacheProperties { @NotNull private final String key; - public ImportCacheProperties(boolean enabled, String key) { + public ImportCacheProperties(@DefaultValue("true") boolean enabled, + @DefaultValue("default") String key) { this.enabled = enabled; this.key = key; } @@ -367,7 +387,9 @@ public static class ImportRemoteStateProperties { @Pattern(regexp = "^[A-Fa-f0-9]+$") private final String encryptionSalt; - public ImportRemoteStateProperties(boolean enabled, String encryptionKey, String encryptionSalt) { + public ImportRemoteStateProperties(@DefaultValue("true") boolean enabled, + String encryptionKey, + @DefaultValue("2B521C795FBE2F2425DB150CD3700BA9") String encryptionSalt) { this.enabled = enabled; this.encryptionKey = encryptionKey; this.encryptionSalt = encryptionSalt; diff --git a/src/main/java/de/adorsys/keycloak/config/properties/KeycloakConfigProperties.java b/src/main/java/de/adorsys/keycloak/config/properties/KeycloakConfigProperties.java index bf9e907fa..60470b3f0 100644 --- a/src/main/java/de/adorsys/keycloak/config/properties/KeycloakConfigProperties.java +++ b/src/main/java/de/adorsys/keycloak/config/properties/KeycloakConfigProperties.java @@ -22,6 +22,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConstructorBinding; +import org.springframework.boot.context.properties.bind.DefaultValue; import org.springframework.validation.annotation.Validated; import java.net.URL; @@ -70,18 +71,19 @@ public class KeycloakConfigProperties { private final KeycloakAvailabilityCheck availabilityCheck; public KeycloakConfigProperties( - String loginRealm, - String clientId, - String version, URL url, - String user, + @DefaultValue("master") String loginRealm, + @DefaultValue("admin-cli") String clientId, + String version, + URL url, + @DefaultValue("admin") String user, String password, - String clientSecret, - String grantType, - boolean sslVerify, + @DefaultValue("") String clientSecret, + @DefaultValue("password") String grantType, + @DefaultValue("true") boolean sslVerify, URL httpProxy, - KeycloakAvailabilityCheck availabilityCheck, - Duration connectTimeout, - Duration readTimeout + @DefaultValue KeycloakAvailabilityCheck availabilityCheck, + @DefaultValue("10s") Duration connectTimeout, + @DefaultValue("10s") Duration readTimeout ) { this.loginRealm = loginRealm; this.clientId = clientId; @@ -161,7 +163,9 @@ public static class KeycloakAvailabilityCheck { private final Duration retryDelay; @SuppressWarnings("unused") - public KeycloakAvailabilityCheck(boolean enabled, Duration timeout, Duration retryDelay) { + public KeycloakAvailabilityCheck(@DefaultValue("false") boolean enabled, + @DefaultValue("120s") Duration timeout, + @DefaultValue("2s") Duration retryDelay) { this.enabled = enabled; this.timeout = timeout; this.retryDelay = retryDelay; diff --git a/src/main/java/de/adorsys/keycloak/config/properties/NormalizationConfigProperties.java b/src/main/java/de/adorsys/keycloak/config/properties/NormalizationConfigProperties.java new file mode 100644 index 000000000..a91e4b706 --- /dev/null +++ b/src/main/java/de/adorsys/keycloak/config/properties/NormalizationConfigProperties.java @@ -0,0 +1,108 @@ +/*- + * ---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.properties; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.ConstructorBinding; +import org.springframework.boot.context.properties.bind.DefaultValue; +import org.springframework.validation.annotation.Validated; + +import java.util.Collection; +import javax.validation.Valid; +import javax.validation.constraints.NotNull; + +@ConfigurationProperties(prefix = "normalization", ignoreUnknownFields = false) +@ConstructorBinding +@Validated +public class NormalizationConfigProperties { + + @Valid + private final NormalizationFilesProperties files; + + private final OutputFormat outputFormat; + + private final String fallbackVersion; + + public NormalizationConfigProperties(@DefaultValue NormalizationFilesProperties files, + @DefaultValue("yaml") OutputFormat outputFormat, + String fallbackVersion) { + this.files = files; + this.outputFormat = outputFormat; + this.fallbackVersion = fallbackVersion; + } + + public NormalizationFilesProperties getFiles() { + return files; + } + + public OutputFormat getOutputFormat() { + return outputFormat; + } + + public String getFallbackVersion() { + return fallbackVersion; + } + + public static class NormalizationFilesProperties { + + @NotNull + private final Collection inputLocations; + + @NotNull + private final Collection excludes; + + @NotNull + private final boolean includeHiddenFiles; + + @NotNull + private final String outputDirectory; + + public NormalizationFilesProperties(Collection inputLocations, + @DefaultValue Collection excludes, + @DefaultValue("false") boolean includeHiddenFiles, + String outputDirectory) { + this.inputLocations = inputLocations; + this.excludes = excludes; + this.includeHiddenFiles = includeHiddenFiles; + this.outputDirectory = outputDirectory; + } + + public Collection getInputLocations() { + return inputLocations; + } + + public Collection getExcludes() { + return excludes; + } + + public boolean isIncludeHiddenFiles() { + return includeHiddenFiles; + } + + public String getOutputDirectory() { + return outputDirectory; + } + } + + public enum OutputFormat { + JSON, YAML + } +} diff --git a/src/main/java/de/adorsys/keycloak/config/properties/NormalizationKeycloakConfigProperties.java b/src/main/java/de/adorsys/keycloak/config/properties/NormalizationKeycloakConfigProperties.java new file mode 100644 index 000000000..e618be0ec --- /dev/null +++ b/src/main/java/de/adorsys/keycloak/config/properties/NormalizationKeycloakConfigProperties.java @@ -0,0 +1,49 @@ +/*- + * ---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.properties; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.ConstructorBinding; +import org.springframework.validation.annotation.Validated; + +import javax.validation.constraints.NotNull; + +/* + * Duplicated prefix keycloak. Since only one of the two classes is loaded (depending on configuration) this is fine. + * This saves us from having to define a keycloak address for the normalization usage, since we don't actually need to + * talk to a keycloak instance, and we only need to know the version. + */ +@ConfigurationProperties(prefix = "keycloak", ignoreUnknownFields = false) +@ConstructorBinding +@Validated +public class NormalizationKeycloakConfigProperties { + + @NotNull + private final String version; + + public NormalizationKeycloakConfigProperties(String version) { + this.version = version; + } + + public String getVersion() { + return version; + } +} diff --git a/src/main/java/de/adorsys/keycloak/config/properties/RunConfigProperties.java b/src/main/java/de/adorsys/keycloak/config/properties/RunConfigProperties.java new file mode 100644 index 000000000..c7e07056e --- /dev/null +++ b/src/main/java/de/adorsys/keycloak/config/properties/RunConfigProperties.java @@ -0,0 +1,46 @@ +/*- + * ---license-start + * keycloak-config-cli + * --- + * Copyright (C) 2017 - 2022 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.properties; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.ConstructorBinding; +import org.springframework.boot.context.properties.bind.DefaultValue; +import org.springframework.validation.annotation.Validated; + +@ConfigurationProperties(prefix = "run", ignoreUnknownFields = false) +@ConstructorBinding +@Validated +public class RunConfigProperties { + + private final Operation operation; + + public RunConfigProperties(@DefaultValue("IMPORT") Operation operation) { + this.operation = operation; + } + + public Operation getOperation() { + return operation; + } + + public enum Operation { + IMPORT, NORMALIZE + } +} diff --git a/src/main/java/de/adorsys/keycloak/config/provider/BaselineProvider.java b/src/main/java/de/adorsys/keycloak/config/provider/BaselineProvider.java new file mode 100644 index 000000000..f2722a904 --- /dev/null +++ b/src/main/java/de/adorsys/keycloak/config/provider/BaselineProvider.java @@ -0,0 +1,111 @@ +/*- + * ---license-start + * keycloak-config-cli + * --- + * Copyright (C) 2017 - 2022 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.provider; + +import com.fasterxml.jackson.databind.ObjectMapper; +import de.adorsys.keycloak.config.exception.NormalizationException; +import de.adorsys.keycloak.config.properties.NormalizationConfigProperties; +import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.RealmRepresentation; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +@Component +@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "NORMALIZE") +public class BaselineProvider { + + private static final Logger logger = LoggerFactory.getLogger(BaselineProvider.class); + private static final String PLACEHOLDER = "REALM_NAME_PLACEHOLDER"; + + private final ObjectMapper objectMapper; + + private final String fallbackVersion; + + @Autowired + public BaselineProvider(ObjectMapper objectMapper, NormalizationConfigProperties normalizationConfigProperties) { + this.objectMapper = objectMapper; + this.fallbackVersion = normalizationConfigProperties.getFallbackVersion(); + } + + public RealmRepresentation getRealm(String version, String realmName) { + try (var inputStream = getRealmInputStream(version)) { + /* + * Replace the placeholder with the realm name to import. This sets some internal values like role names, + * baseUrls and redirectUrls so that they don't get picked up as "changes" + */ + var realmString = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8).replace(PLACEHOLDER, realmName); + return objectMapper.readValue(realmString, RealmRepresentation.class); + } catch (IOException ex) { + throw new NormalizationException(String.format("Failed to load baseline realm for version %s", version), ex); + } + } + + public ClientRepresentation getClient(String version, String clientId) { + try (var is = getClientInputStream(version)) { + var client = objectMapper.readValue(is, ClientRepresentation.class); + client.setClientId(clientId); + return client; + } catch (IOException ex) { + throw new NormalizationException(String.format("Failed to load baseline client for version %s", version), ex); + } + } + + public InputStream getRealmInputStream(String version) { + var inputStream = getClass().getResourceAsStream(String.format("/baseline/%s/realm/realm.json", version)); + if (inputStream == null) { + if (fallbackVersion != null) { + logger.warn("Reference realm not found for version {}. Using fallback version {}!", version, fallbackVersion); + inputStream = getClass().getResourceAsStream(String.format("/baseline/%s/realm/realm.json", fallbackVersion)); + if (inputStream == null) { + throw new NormalizationException(String.format("Reference realm for version %s does not exist, " + + "and fallback version %s does not exist either. Aborting!", version, fallbackVersion)); + } + } else { + throw new NormalizationException(String.format("Reference realm for version %s does not exist. Aborting!", version)); + } + } + return inputStream; + } + + public InputStream getClientInputStream(String version) { + var inputStream = getClass().getResourceAsStream(String.format("/baseline/%s/client/client.json", version)); + if (inputStream == null) { + if (fallbackVersion != null) { + logger.debug("Reference client not found for version {}. Using fallback version {}!", version, fallbackVersion); + inputStream = getClass().getResourceAsStream(String.format("/baseline/%s/client/client.json", fallbackVersion)); + if (inputStream == null) { + throw new NormalizationException(String.format("Reference client for version %s does not exist, " + + "and fallback version %s does not exist either. Aborting!", version, fallbackVersion)); + } + } else { + throw new NormalizationException(String.format("Reference client for version %s does not exist. Aborting!", version)); + } + } + return inputStream; + } +} diff --git a/src/main/java/de/adorsys/keycloak/config/provider/KeycloakExportProvider.java b/src/main/java/de/adorsys/keycloak/config/provider/KeycloakExportProvider.java new file mode 100644 index 000000000..c64c00a2e --- /dev/null +++ b/src/main/java/de/adorsys/keycloak/config/provider/KeycloakExportProvider.java @@ -0,0 +1,232 @@ +/*- + * ---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.provider; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import de.adorsys.keycloak.config.exception.InvalidImportException; +import de.adorsys.keycloak.config.model.ImportResource; +import de.adorsys.keycloak.config.properties.NormalizationConfigProperties; +import org.apache.commons.lang3.tuple.ImmutablePair; +import org.apache.commons.lang3.tuple.Pair; +import org.keycloak.representations.idm.RealmRepresentation; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.core.io.Resource; +import org.springframework.core.io.UrlResource; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.stereotype.Component; +import org.springframework.util.PathMatcher; +import org.yaml.snakeyaml.Yaml; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.Authenticator; +import java.net.PasswordAuthentication; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.stream.Collectors; + +/* + * This class heavily copy pastes code from KeycloakImportProvider. This can probably be reduced quite a bit by moving some code out to a shared class + */ +@Component +@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "NORMALIZE") +public class KeycloakExportProvider { + + private static final Logger logger = LoggerFactory.getLogger(KeycloakExportProvider.class); + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper() + .enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + + private final PathMatchingResourcePatternResolver patternResolver; + + private final NormalizationConfigProperties normalizationConfigProperties; + + @Autowired + public KeycloakExportProvider(PathMatchingResourcePatternResolver patternResolver, + NormalizationConfigProperties normalizationConfigProperties) { + this.patternResolver = patternResolver; + this.normalizationConfigProperties = normalizationConfigProperties; + } + + public Map>> readFromLocations() { + Map>> files = new LinkedHashMap<>(); + + for (String location : normalizationConfigProperties.getFiles().getInputLocations()) { + logger.debug("Loading file location '{}'", location); + String resourceLocation = prepareResourceLocation(location); + + Resource[] resources; + try { + resources = this.patternResolver.getResources(resourceLocation); + } catch (IOException e) { + throw new InvalidImportException("Unable to proceed location '" + location + "': " + e.getMessage(), e); + } + + resources = Arrays.stream(resources).filter(this::filterExcludedResources).toArray(Resource[]::new); + + if (resources.length == 0) { + throw new InvalidImportException("No files matching '" + location + "'!"); + } + + Map> exports = Arrays.stream(resources) + .map(this::readResource) + .filter(this::filterEmptyResources) + .sorted(Map.Entry.comparingByKey()) + .map(this::readRealms) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, + (oldValue, newValue) -> oldValue, LinkedHashMap::new)); + files.put(location, exports); + } + return files; + } + + private Pair> readRealms(ImportResource resource) { + String location = resource.getFilename(); + String content = resource.getValue(); + + if (logger.isTraceEnabled()) { + logger.trace(content); + } + + List realms; + try { + realms = readContent(content); + } catch (Exception e) { + throw new InvalidImportException("Unable to parse file '" + location + "': " + e.getMessage(), e); + } + return new ImmutablePair<>(location, realms); + } + + private List readContent(String content) { + List realms = new ArrayList<>(); + + Yaml yaml = new Yaml(); + Iterable yamlDocuments = yaml.loadAll(content); + + for (Object yamlDocument : yamlDocuments) { + realms.add(OBJECT_MAPPER.convertValue(yamlDocument, RealmRepresentation.class)); + } + return realms; + } + + private String prepareResourceLocation(String location) { + String importLocation = location; + + importLocation = importLocation.replaceFirst("^zip:", "jar:"); + + // backward compatibility to correct a possible missing prefix "file:" in path + if (!importLocation.contains(":")) { + importLocation = "file:" + importLocation; + } + return importLocation; + } + + private boolean filterExcludedResources(Resource resource) { + if (!resource.isFile()) { + return true; + } + + File file; + + try { + file = resource.getFile(); + } catch (IOException ignored) { + return true; + } + + if (file.isDirectory()) { + return false; + } + + if (!this.normalizationConfigProperties.getFiles().isIncludeHiddenFiles() + && (file.isHidden() || FileUtils.hasHiddenAncestorDirectory(file))) { + return false; + } + + PathMatcher pathMatcher = patternResolver.getPathMatcher(); + return normalizationConfigProperties.getFiles().getExcludes() + .stream() + .map(pattern -> pattern.startsWith("**") ? "/" + pattern : pattern) + .map(pattern -> !pattern.startsWith("/**") ? "/**" + pattern : pattern) + .map(pattern -> !pattern.startsWith("/") ? "/" + pattern : pattern) + .noneMatch(pattern -> { + boolean match = pathMatcher.match(pattern, file.getPath()); + if (match) { + logger.debug("Excluding resource file '{}' (match {})", file.getPath(), pattern); + return true; + } + return false; + }); + } + + private ImportResource readResource(Resource resource) { + logger.debug("Loading file '{}'", resource.getFilename()); + + try { + resource = setupAuthentication(resource); + try (InputStream inputStream = resource.getInputStream()) { + return new ImportResource(resource.getURI().toString(), new String(inputStream.readAllBytes(), StandardCharsets.UTF_8)); + } + } catch (IOException e) { + throw new InvalidImportException("Unable to proceed resource '" + resource + "': " + e.getMessage(), e); + } finally { + Authenticator.setDefault(null); + } + } + + private Resource setupAuthentication(Resource resource) throws IOException { + String userInfo; + + try { + userInfo = resource.getURL().getUserInfo(); + } catch (IOException e) { + return resource; + } + + if (userInfo == null) return resource; + + String[] userInfoSplit = userInfo.split(":"); + + if (userInfoSplit.length != 2) return resource; + + Authenticator.setDefault(new Authenticator() { + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(userInfoSplit[0], userInfoSplit[1].toCharArray()); + } + }); + + // Mask AuthInfo + String location = resource.getURI().toString().replace(userInfo + "@", "***@"); + return new UrlResource(location); + } + + private boolean filterEmptyResources(ImportResource resource) { + return !resource.getValue().isEmpty(); + } + + +} diff --git a/src/main/java/de/adorsys/keycloak/config/provider/KeycloakImportProvider.java b/src/main/java/de/adorsys/keycloak/config/provider/KeycloakImportProvider.java index 24dd458e1..d362eb382 100644 --- a/src/main/java/de/adorsys/keycloak/config/provider/KeycloakImportProvider.java +++ b/src/main/java/de/adorsys/keycloak/config/provider/KeycloakImportProvider.java @@ -36,6 +36,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.core.env.Environment; import org.springframework.core.io.Resource; import org.springframework.core.io.UrlResource; @@ -54,6 +55,7 @@ import java.util.stream.Collectors; @Component +@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "IMPORT", matchIfMissing = true) public class KeycloakImportProvider { private final PathMatchingResourcePatternResolver patternResolver; private final ImportConfigProperties importConfigProperties; diff --git a/src/main/java/de/adorsys/keycloak/config/provider/KeycloakProvider.java b/src/main/java/de/adorsys/keycloak/config/provider/KeycloakProvider.java index 13782648f..e6a008422 100644 --- a/src/main/java/de/adorsys/keycloak/config/provider/KeycloakProvider.java +++ b/src/main/java/de/adorsys/keycloak/config/provider/KeycloakProvider.java @@ -33,6 +33,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Component; import java.net.URI; @@ -50,6 +51,7 @@ * to avoid a deadlock. */ @Component +@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "IMPORT", matchIfMissing = true) public class KeycloakProvider implements AutoCloseable { private static final Logger logger = LoggerFactory.getLogger(KeycloakProvider.class); diff --git a/src/main/java/de/adorsys/keycloak/config/repository/AuthenticationFlowRepository.java b/src/main/java/de/adorsys/keycloak/config/repository/AuthenticationFlowRepository.java index 01eb48509..440acd8ac 100644 --- a/src/main/java/de/adorsys/keycloak/config/repository/AuthenticationFlowRepository.java +++ b/src/main/java/de/adorsys/keycloak/config/repository/AuthenticationFlowRepository.java @@ -32,6 +32,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; import org.springframework.util.Assert; @@ -44,6 +45,7 @@ import javax.ws.rs.core.Response; @Service +@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "IMPORT", matchIfMissing = true) public class AuthenticationFlowRepository { private static final Logger logger = LoggerFactory.getLogger(AuthenticationFlowRepository.class); diff --git a/src/main/java/de/adorsys/keycloak/config/repository/AuthenticatorConfigRepository.java b/src/main/java/de/adorsys/keycloak/config/repository/AuthenticatorConfigRepository.java index 7aeda05d1..c3b0921be 100644 --- a/src/main/java/de/adorsys/keycloak/config/repository/AuthenticatorConfigRepository.java +++ b/src/main/java/de/adorsys/keycloak/config/repository/AuthenticatorConfigRepository.java @@ -24,6 +24,7 @@ import org.keycloak.representations.idm.AuthenticatorConfigRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; import java.util.List; @@ -31,6 +32,7 @@ import java.util.stream.Collectors; @Service +@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "IMPORT", matchIfMissing = true) public class AuthenticatorConfigRepository { private final AuthenticationFlowRepository authenticationFlowRepository; private final RealmRepository realmRepository; diff --git a/src/main/java/de/adorsys/keycloak/config/repository/ClientRepository.java b/src/main/java/de/adorsys/keycloak/config/repository/ClientRepository.java index 2116a393f..db9857111 100644 --- a/src/main/java/de/adorsys/keycloak/config/repository/ClientRepository.java +++ b/src/main/java/de/adorsys/keycloak/config/repository/ClientRepository.java @@ -35,6 +35,7 @@ import org.keycloak.representations.idm.authorization.ResourceServerRepresentation; import org.keycloak.representations.idm.authorization.ScopeRepresentation; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; import java.util.List; @@ -46,6 +47,7 @@ import javax.ws.rs.core.Response; @Service +@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "IMPORT", matchIfMissing = true) public class ClientRepository { private final RealmRepository realmRepository; diff --git a/src/main/java/de/adorsys/keycloak/config/repository/ClientScopeRepository.java b/src/main/java/de/adorsys/keycloak/config/repository/ClientScopeRepository.java index e70a46100..6bf4b7d57 100644 --- a/src/main/java/de/adorsys/keycloak/config/repository/ClientScopeRepository.java +++ b/src/main/java/de/adorsys/keycloak/config/repository/ClientScopeRepository.java @@ -29,6 +29,7 @@ import org.keycloak.representations.idm.ClientScopeRepresentation; import org.keycloak.representations.idm.ProtocolMapperRepresentation; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; import java.util.ArrayList; @@ -40,6 +41,7 @@ import javax.ws.rs.core.Response; @Service +@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "IMPORT", matchIfMissing = true) public class ClientScopeRepository { private final RealmRepository realmRepository; diff --git a/src/main/java/de/adorsys/keycloak/config/repository/ComponentRepository.java b/src/main/java/de/adorsys/keycloak/config/repository/ComponentRepository.java index 25957dbe6..9efd9146b 100644 --- a/src/main/java/de/adorsys/keycloak/config/repository/ComponentRepository.java +++ b/src/main/java/de/adorsys/keycloak/config/repository/ComponentRepository.java @@ -28,6 +28,7 @@ import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.representations.idm.ComponentRepresentation; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; import java.util.Collections; @@ -38,6 +39,7 @@ import javax.ws.rs.core.Response; @Service +@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "IMPORT", matchIfMissing = true) public class ComponentRepository { private final RealmRepository realmRepository; diff --git a/src/main/java/de/adorsys/keycloak/config/repository/ExecutionFlowRepository.java b/src/main/java/de/adorsys/keycloak/config/repository/ExecutionFlowRepository.java index ad429c4e7..d28f0a679 100644 --- a/src/main/java/de/adorsys/keycloak/config/repository/ExecutionFlowRepository.java +++ b/src/main/java/de/adorsys/keycloak/config/repository/ExecutionFlowRepository.java @@ -31,6 +31,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; import java.util.List; @@ -41,6 +42,7 @@ import javax.ws.rs.core.Response; @Service +@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "IMPORT", matchIfMissing = true) public class ExecutionFlowRepository { private static final Logger logger = LoggerFactory.getLogger(ExecutionFlowRepository.class); diff --git a/src/main/java/de/adorsys/keycloak/config/repository/GroupRepository.java b/src/main/java/de/adorsys/keycloak/config/repository/GroupRepository.java index 88628176f..129ae35ae 100644 --- a/src/main/java/de/adorsys/keycloak/config/repository/GroupRepository.java +++ b/src/main/java/de/adorsys/keycloak/config/repository/GroupRepository.java @@ -32,6 +32,7 @@ import org.keycloak.representations.idm.ManagementPermissionRepresentation; import org.keycloak.representations.idm.RoleRepresentation; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; import java.util.ArrayList; @@ -42,6 +43,7 @@ import javax.ws.rs.core.Response; @Service +@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "IMPORT", matchIfMissing = true) public class GroupRepository { private final RealmRepository realmRepository; diff --git a/src/main/java/de/adorsys/keycloak/config/repository/IdentityProviderMapperRepository.java b/src/main/java/de/adorsys/keycloak/config/repository/IdentityProviderMapperRepository.java index ac71c2b65..03bb04743 100644 --- a/src/main/java/de/adorsys/keycloak/config/repository/IdentityProviderMapperRepository.java +++ b/src/main/java/de/adorsys/keycloak/config/repository/IdentityProviderMapperRepository.java @@ -26,6 +26,7 @@ import org.keycloak.representations.idm.IdentityProviderMapperRepresentation; import org.keycloak.representations.idm.IdentityProviderRepresentation; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; import java.util.ArrayList; @@ -35,6 +36,7 @@ import javax.ws.rs.core.Response; @Service +@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "IMPORT", matchIfMissing = true) public class IdentityProviderMapperRepository { private final RealmRepository realmRepository; 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 aa7301c3e..7363bb3d6 100644 --- a/src/main/java/de/adorsys/keycloak/config/repository/IdentityProviderRepository.java +++ b/src/main/java/de/adorsys/keycloak/config/repository/IdentityProviderRepository.java @@ -28,6 +28,7 @@ import org.keycloak.representations.idm.IdentityProviderRepresentation; import org.keycloak.representations.idm.ManagementPermissionRepresentation; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; import java.util.List; @@ -36,6 +37,7 @@ import javax.ws.rs.core.Response; @Service +@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "IMPORT", matchIfMissing = true) public class IdentityProviderRepository { private final RealmRepository realmRepository; diff --git a/src/main/java/de/adorsys/keycloak/config/repository/RealmRepository.java b/src/main/java/de/adorsys/keycloak/config/repository/RealmRepository.java index c71a6483a..82aa70681 100644 --- a/src/main/java/de/adorsys/keycloak/config/repository/RealmRepository.java +++ b/src/main/java/de/adorsys/keycloak/config/repository/RealmRepository.java @@ -28,11 +28,14 @@ import org.keycloak.admin.client.resource.RealmsResource; import org.keycloak.representations.idm.RealmRepresentation; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; +import java.util.List; import javax.ws.rs.WebApplicationException; @Service +@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "IMPORT", matchIfMissing = true) public class RealmRepository { private final KeycloakProvider keycloakProvider; @@ -105,4 +108,8 @@ public void removeDefaultDefaultClientScope(String realmName, String scopeId) { public void removeDefaultOptionalClientScope(String realmName, String scopeId) { getResource(realmName).removeDefaultOptionalClientScope(scopeId); } + + public List getRealms() { + return keycloakProvider.getInstance().realms().findAll(); + } } diff --git a/src/main/java/de/adorsys/keycloak/config/repository/RequiredActionRepository.java b/src/main/java/de/adorsys/keycloak/config/repository/RequiredActionRepository.java index 4b55fb683..a3ade2570 100644 --- a/src/main/java/de/adorsys/keycloak/config/repository/RequiredActionRepository.java +++ b/src/main/java/de/adorsys/keycloak/config/repository/RequiredActionRepository.java @@ -25,6 +25,7 @@ import org.keycloak.representations.idm.RequiredActionProviderRepresentation; import org.keycloak.representations.idm.RequiredActionProviderSimpleRepresentation; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; import java.util.List; @@ -34,6 +35,7 @@ * Provides methods to retrieve and store required-actions in your realm */ @Service +@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "IMPORT", matchIfMissing = true) public class RequiredActionRepository { private final AuthenticationFlowRepository authenticationFlowRepository; diff --git a/src/main/java/de/adorsys/keycloak/config/repository/RoleCompositeRepository.java b/src/main/java/de/adorsys/keycloak/config/repository/RoleCompositeRepository.java index aacbf2901..ac8165089 100644 --- a/src/main/java/de/adorsys/keycloak/config/repository/RoleCompositeRepository.java +++ b/src/main/java/de/adorsys/keycloak/config/repository/RoleCompositeRepository.java @@ -26,6 +26,7 @@ import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.RoleRepresentation; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; import java.util.*; @@ -33,6 +34,7 @@ import java.util.stream.Collectors; @Service +@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "IMPORT", matchIfMissing = true) public class RoleCompositeRepository { private final RoleRepository roleRepository; diff --git a/src/main/java/de/adorsys/keycloak/config/repository/RoleRepository.java b/src/main/java/de/adorsys/keycloak/config/repository/RoleRepository.java index 94833431c..253f9a58a 100644 --- a/src/main/java/de/adorsys/keycloak/config/repository/RoleRepository.java +++ b/src/main/java/de/adorsys/keycloak/config/repository/RoleRepository.java @@ -27,6 +27,7 @@ import org.keycloak.admin.client.resource.*; import org.keycloak.representations.idm.*; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.lang.Nullable; import org.springframework.stereotype.Service; @@ -34,6 +35,7 @@ import java.util.stream.Collectors; @Service +@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "IMPORT", matchIfMissing = true) public class RoleRepository { private final RealmRepository realmRepository; private final ClientRepository clientRepository; diff --git a/src/main/java/de/adorsys/keycloak/config/repository/ScopeMappingRepository.java b/src/main/java/de/adorsys/keycloak/config/repository/ScopeMappingRepository.java index a8e15154d..b420ae043 100644 --- a/src/main/java/de/adorsys/keycloak/config/repository/ScopeMappingRepository.java +++ b/src/main/java/de/adorsys/keycloak/config/repository/ScopeMappingRepository.java @@ -26,6 +26,7 @@ import org.keycloak.representations.idm.RoleRepresentation; import org.keycloak.representations.idm.ScopeMappingRepresentation; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; import java.util.Collection; @@ -34,6 +35,7 @@ import java.util.stream.Collectors; @Service +@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "IMPORT", matchIfMissing = true) public class ScopeMappingRepository { private final RealmRepository realmRepository; diff --git a/src/main/java/de/adorsys/keycloak/config/repository/StateRepository.java b/src/main/java/de/adorsys/keycloak/config/repository/StateRepository.java index 212ab611f..38679f63f 100644 --- a/src/main/java/de/adorsys/keycloak/config/repository/StateRepository.java +++ b/src/main/java/de/adorsys/keycloak/config/repository/StateRepository.java @@ -24,6 +24,7 @@ import de.adorsys.keycloak.config.properties.ImportConfigProperties; import de.adorsys.keycloak.config.util.CryptoUtil; import org.keycloak.representations.idm.RealmRepresentation; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Component; import java.text.MessageFormat; @@ -36,6 +37,7 @@ import static de.adorsys.keycloak.config.util.JsonUtil.toJson; @Component +@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "IMPORT", matchIfMissing = true) public class StateRepository { private static final int MAX_ATTRIBUTE_LENGTH = 250; diff --git a/src/main/java/de/adorsys/keycloak/config/repository/UserProfileRepository.java b/src/main/java/de/adorsys/keycloak/config/repository/UserProfileRepository.java index ad2302e52..c546829b4 100644 --- a/src/main/java/de/adorsys/keycloak/config/repository/UserProfileRepository.java +++ b/src/main/java/de/adorsys/keycloak/config/repository/UserProfileRepository.java @@ -27,12 +27,14 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Component; import java.util.Optional; import javax.ws.rs.core.Response; @Component +@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "IMPORT", matchIfMissing = true) public class UserProfileRepository { private static final Logger logger = LoggerFactory.getLogger(AuthenticationFlowRepository.class); diff --git a/src/main/java/de/adorsys/keycloak/config/repository/UserRepository.java b/src/main/java/de/adorsys/keycloak/config/repository/UserRepository.java index e778751a2..35b21cdc1 100644 --- a/src/main/java/de/adorsys/keycloak/config/repository/UserRepository.java +++ b/src/main/java/de/adorsys/keycloak/config/repository/UserRepository.java @@ -28,6 +28,7 @@ import org.keycloak.representations.idm.GroupRepresentation; import org.keycloak.representations.idm.UserRepresentation; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; import java.util.List; @@ -35,6 +36,7 @@ import javax.ws.rs.core.Response; @Service +@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "IMPORT", matchIfMissing = true) public class UserRepository { private final RealmRepository realmRepository; 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 0649b0bd7..42e8a416c 100644 --- a/src/main/java/de/adorsys/keycloak/config/service/AuthenticationFlowsImportService.java +++ b/src/main/java/de/adorsys/keycloak/config/service/AuthenticationFlowsImportService.java @@ -35,6 +35,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; import java.util.List; @@ -51,6 +52,7 @@ * sub-flow: any flow which has the property 'topLevel' set to 'false' and which are related to execution-flows within topLevel-flows */ @Service +@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "IMPORT", matchIfMissing = true) public class AuthenticationFlowsImportService { private static final Logger logger = LoggerFactory.getLogger(AuthenticationFlowsImportService.class); diff --git a/src/main/java/de/adorsys/keycloak/config/service/AuthenticatorConfigImportService.java b/src/main/java/de/adorsys/keycloak/config/service/AuthenticatorConfigImportService.java index e3fa9d846..e85812d18 100644 --- a/src/main/java/de/adorsys/keycloak/config/service/AuthenticatorConfigImportService.java +++ b/src/main/java/de/adorsys/keycloak/config/service/AuthenticatorConfigImportService.java @@ -29,6 +29,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; import java.util.ArrayList; @@ -40,6 +41,7 @@ import java.util.stream.Stream; @Service +@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "IMPORT", matchIfMissing = true) public class AuthenticatorConfigImportService { private static final Logger logger = LoggerFactory.getLogger(AuthenticatorConfigImportService.class); diff --git a/src/main/java/de/adorsys/keycloak/config/service/ClientAuthorizationImportService.java b/src/main/java/de/adorsys/keycloak/config/service/ClientAuthorizationImportService.java index 1663b2b7d..194188888 100644 --- a/src/main/java/de/adorsys/keycloak/config/service/ClientAuthorizationImportService.java +++ b/src/main/java/de/adorsys/keycloak/config/service/ClientAuthorizationImportService.java @@ -40,6 +40,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; import java.util.HashMap; @@ -54,6 +55,7 @@ @Service @SuppressWarnings({"java:S1192"}) +@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "IMPORT", matchIfMissing = true) public class ClientAuthorizationImportService { private static final Logger logger = LoggerFactory.getLogger(ClientAuthorizationImportService.class); diff --git a/src/main/java/de/adorsys/keycloak/config/service/ClientImportService.java b/src/main/java/de/adorsys/keycloak/config/service/ClientImportService.java index ea0c64b53..883f9eba6 100644 --- a/src/main/java/de/adorsys/keycloak/config/service/ClientImportService.java +++ b/src/main/java/de/adorsys/keycloak/config/service/ClientImportService.java @@ -35,6 +35,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; import java.util.*; @@ -47,6 +48,7 @@ @Service @SuppressWarnings({"java:S1192"}) +@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "IMPORT", matchIfMissing = true) public class ClientImportService { private static final Logger logger = LoggerFactory.getLogger(ClientImportService.class); diff --git a/src/main/java/de/adorsys/keycloak/config/service/ClientScopeImportService.java b/src/main/java/de/adorsys/keycloak/config/service/ClientScopeImportService.java index 98faafba4..145b781e6 100644 --- a/src/main/java/de/adorsys/keycloak/config/service/ClientScopeImportService.java +++ b/src/main/java/de/adorsys/keycloak/config/service/ClientScopeImportService.java @@ -32,6 +32,7 @@ import org.keycloak.representations.idm.RealmRepresentation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; import java.util.List; @@ -41,6 +42,7 @@ import java.util.stream.Collectors; @Service +@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "IMPORT", matchIfMissing = true) public class ClientScopeImportService { private static final Logger logger = LoggerFactory.getLogger(ClientScopeImportService.class); diff --git a/src/main/java/de/adorsys/keycloak/config/service/ClientScopeMappingImportService.java b/src/main/java/de/adorsys/keycloak/config/service/ClientScopeMappingImportService.java index 5d223e9f7..dbe029f2a 100644 --- a/src/main/java/de/adorsys/keycloak/config/service/ClientScopeMappingImportService.java +++ b/src/main/java/de/adorsys/keycloak/config/service/ClientScopeMappingImportService.java @@ -32,6 +32,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; import java.util.ArrayList; @@ -42,6 +43,7 @@ import java.util.stream.Collectors; @Service +@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "IMPORT", matchIfMissing = true) public class ClientScopeMappingImportService { private static final Logger logger = LoggerFactory.getLogger(ClientScopeMappingImportService.class); diff --git a/src/main/java/de/adorsys/keycloak/config/service/ComponentImportService.java b/src/main/java/de/adorsys/keycloak/config/service/ComponentImportService.java index 586283ab2..0d8bd3042 100644 --- a/src/main/java/de/adorsys/keycloak/config/service/ComponentImportService.java +++ b/src/main/java/de/adorsys/keycloak/config/service/ComponentImportService.java @@ -34,11 +34,13 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; import java.util.*; @Service +@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "IMPORT", matchIfMissing = true) public class ComponentImportService { private static final Logger logger = LoggerFactory.getLogger(ComponentImportService.class); diff --git a/src/main/java/de/adorsys/keycloak/config/service/DefaultGroupsImportService.java b/src/main/java/de/adorsys/keycloak/config/service/DefaultGroupsImportService.java index 8f7f0823b..43adbbbc5 100644 --- a/src/main/java/de/adorsys/keycloak/config/service/DefaultGroupsImportService.java +++ b/src/main/java/de/adorsys/keycloak/config/service/DefaultGroupsImportService.java @@ -26,11 +26,13 @@ import de.adorsys.keycloak.config.repository.RealmRepository; import org.keycloak.admin.client.resource.RealmResource; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; import java.util.List; @Service +@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "IMPORT", matchIfMissing = true) public class DefaultGroupsImportService { private final RealmRepository realmRepository; private final GroupRepository groupRepository; diff --git a/src/main/java/de/adorsys/keycloak/config/service/ExecutionFlowsImportService.java b/src/main/java/de/adorsys/keycloak/config/service/ExecutionFlowsImportService.java index ae885891f..6184f99f7 100644 --- a/src/main/java/de/adorsys/keycloak/config/service/ExecutionFlowsImportService.java +++ b/src/main/java/de/adorsys/keycloak/config/service/ExecutionFlowsImportService.java @@ -21,7 +21,6 @@ package de.adorsys.keycloak.config.service; import de.adorsys.keycloak.config.exception.ImportProcessingException; -import de.adorsys.keycloak.config.exception.InvalidImportException; import de.adorsys.keycloak.config.model.RealmImport; import de.adorsys.keycloak.config.repository.AuthenticatorConfigRepository; import de.adorsys.keycloak.config.repository.ExecutionFlowRepository; @@ -31,6 +30,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; import java.util.HashMap; @@ -44,6 +44,7 @@ * Imports executions and execution-flows of existing top-level flows */ @Service +@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "IMPORT", matchIfMissing = true) public class ExecutionFlowsImportService { private static final Logger logger = LoggerFactory.getLogger(ExecutionFlowsImportService.class); @@ -146,13 +147,6 @@ private void createSubFlowByExecutionFlow( executionToImport.getFlowAlias(), realmImport.getRealm() ); - if (!Objects.equals(executionToImport.getAuthenticator(), null) && !Objects.equals(subFlow.getProviderId(), "form-flow")) { - throw new InvalidImportException(String.format( - "Execution property authenticator '%s' can be only set if the sub-flow '%s' type is 'form-flow'.", - executionToImport.getAuthenticator(), subFlow.getAlias() - )); - } - HashMap executionFlow = new HashMap<>(); executionFlow.put("alias", executionToImport.getFlowAlias()); executionFlow.put("provider", executionToImport.getAuthenticator()); diff --git a/src/main/java/de/adorsys/keycloak/config/service/GroupImportService.java b/src/main/java/de/adorsys/keycloak/config/service/GroupImportService.java index c8334e8f8..df4ee4eaf 100644 --- a/src/main/java/de/adorsys/keycloak/config/service/GroupImportService.java +++ b/src/main/java/de/adorsys/keycloak/config/service/GroupImportService.java @@ -28,6 +28,7 @@ import org.keycloak.representations.idm.GroupRepresentation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; import java.util.*; @@ -35,6 +36,7 @@ import java.util.stream.Collectors; @Service +@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "IMPORT", matchIfMissing = true) public class GroupImportService { private static final Logger logger = LoggerFactory.getLogger(GroupImportService.class); 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 760340f6d..ebb93d5b7 100644 --- a/src/main/java/de/adorsys/keycloak/config/service/IdentityProviderImportService.java +++ b/src/main/java/de/adorsys/keycloak/config/service/IdentityProviderImportService.java @@ -30,6 +30,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; import java.util.List; @@ -39,6 +40,7 @@ import static de.adorsys.keycloak.config.properties.ImportConfigProperties.ImportManagedProperties.ImportManagedPropertiesValues; @Service +@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "IMPORT", matchIfMissing = true) public class IdentityProviderImportService { private static final Logger logger = LoggerFactory.getLogger(IdentityProviderImportService.class); 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 ebb0543d2..2f52d5eb1 100644 --- a/src/main/java/de/adorsys/keycloak/config/service/RealmImportService.java +++ b/src/main/java/de/adorsys/keycloak/config/service/RealmImportService.java @@ -31,9 +31,11 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; @Service +@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "IMPORT", matchIfMissing = true) public class RealmImportService { static final String[] ignoredPropertiesForRealmImport = new String[]{ "clients", diff --git a/src/main/java/de/adorsys/keycloak/config/service/RequiredActionsImportService.java b/src/main/java/de/adorsys/keycloak/config/service/RequiredActionsImportService.java index 4ddd12893..021a67b72 100644 --- a/src/main/java/de/adorsys/keycloak/config/service/RequiredActionsImportService.java +++ b/src/main/java/de/adorsys/keycloak/config/service/RequiredActionsImportService.java @@ -30,6 +30,7 @@ import org.keycloak.representations.idm.RequiredActionProviderSimpleRepresentation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; import java.util.List; @@ -40,6 +41,7 @@ * Creates and updates required-actions in your realm */ @Service +@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "IMPORT", matchIfMissing = true) public class RequiredActionsImportService { private static final Logger logger = LoggerFactory.getLogger(RequiredActionsImportService.class); diff --git a/src/main/java/de/adorsys/keycloak/config/service/RoleImportService.java b/src/main/java/de/adorsys/keycloak/config/service/RoleImportService.java index d3ddf1051..f6771f3f9 100644 --- a/src/main/java/de/adorsys/keycloak/config/service/RoleImportService.java +++ b/src/main/java/de/adorsys/keycloak/config/service/RoleImportService.java @@ -34,6 +34,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; import java.util.List; @@ -44,6 +45,7 @@ import java.util.stream.Collectors; @Service +@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "IMPORT", matchIfMissing = true) public class RoleImportService { private static final Logger logger = LoggerFactory.getLogger(RoleImportService.class); private static final String[] propertiesWithDependencies = new String[]{ diff --git a/src/main/java/de/adorsys/keycloak/config/service/ScopeMappingImportService.java b/src/main/java/de/adorsys/keycloak/config/service/ScopeMappingImportService.java index efe6396fb..c6ebb221a 100644 --- a/src/main/java/de/adorsys/keycloak/config/service/ScopeMappingImportService.java +++ b/src/main/java/de/adorsys/keycloak/config/service/ScopeMappingImportService.java @@ -30,6 +30,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; import java.util.List; @@ -39,6 +40,7 @@ import java.util.stream.Collectors; @Service +@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "IMPORT", matchIfMissing = true) public class ScopeMappingImportService { private static final Logger logger = LoggerFactory.getLogger(ScopeMappingImportService.class); diff --git a/src/main/java/de/adorsys/keycloak/config/service/UserImportService.java b/src/main/java/de/adorsys/keycloak/config/service/UserImportService.java index ecac23bd2..e80808a13 100644 --- a/src/main/java/de/adorsys/keycloak/config/service/UserImportService.java +++ b/src/main/java/de/adorsys/keycloak/config/service/UserImportService.java @@ -30,6 +30,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; @@ -38,6 +39,7 @@ import java.util.stream.Collectors; @Service +@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "IMPORT", matchIfMissing = true) public class UserImportService { private static final Logger logger = LoggerFactory.getLogger(UserImportService.class); diff --git a/src/main/java/de/adorsys/keycloak/config/service/UserProfileImportService.java b/src/main/java/de/adorsys/keycloak/config/service/UserProfileImportService.java index 8530dddbb..3f6197833 100644 --- a/src/main/java/de/adorsys/keycloak/config/service/UserProfileImportService.java +++ b/src/main/java/de/adorsys/keycloak/config/service/UserProfileImportService.java @@ -26,6 +26,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; import java.util.LinkedHashMap; @@ -33,6 +34,7 @@ import java.util.Map; @Service +@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "IMPORT", matchIfMissing = true) public class UserProfileImportService { private static final Logger logger = LoggerFactory.getLogger(UserProfileImportService.class); diff --git a/src/main/java/de/adorsys/keycloak/config/service/checksum/ChecksumService.java b/src/main/java/de/adorsys/keycloak/config/service/checksum/ChecksumService.java index ab4525508..20dee4d7a 100644 --- a/src/main/java/de/adorsys/keycloak/config/service/checksum/ChecksumService.java +++ b/src/main/java/de/adorsys/keycloak/config/service/checksum/ChecksumService.java @@ -27,6 +27,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; import java.text.MessageFormat; @@ -34,6 +35,7 @@ import java.util.Objects; @Service +@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "IMPORT", matchIfMissing = true) public class ChecksumService { private static final Logger logger = LoggerFactory.getLogger(ChecksumService.class); diff --git a/src/main/java/de/adorsys/keycloak/config/service/normalize/AttributeNormalizationService.java b/src/main/java/de/adorsys/keycloak/config/service/normalize/AttributeNormalizationService.java new file mode 100644 index 000000000..267faab0d --- /dev/null +++ b/src/main/java/de/adorsys/keycloak/config/service/normalize/AttributeNormalizationService.java @@ -0,0 +1,96 @@ +/*- + * ---license-start + * keycloak-config-cli + * --- + * Copyright (C) 2017 - 2022 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.normalize; + +import org.javers.core.Javers; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import static de.adorsys.keycloak.config.service.normalize.RealmNormalizationService.*; + +@Service +@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "NORMALIZE") +public class AttributeNormalizationService { + + private final Javers unOrderedJavers; + + public AttributeNormalizationService(Javers unOrderedJavers) { + this.unOrderedJavers = unOrderedJavers; + } + + public Map normalizeStringAttributes(Map exportedAttributes, Map baselineAttributes) { + var exportedOrEmpty = getNonNull(exportedAttributes); + var baselineOrEmpty = getNonNull(baselineAttributes); + var normalizedAttributes = new HashMap(); + for (var entry : baselineOrEmpty.entrySet()) { + var attributeName = entry.getKey(); + var baselineAttribute = entry.getValue(); + var exportedAttribute = exportedOrEmpty.remove(attributeName); + + if (!Objects.equals(baselineAttribute, exportedAttribute)) { + normalizedAttributes.put(attributeName, exportedAttribute); + } + } + normalizedAttributes.putAll(exportedOrEmpty); + return normalizedAttributes.isEmpty() ? null : normalizedAttributes; + } + + public Map> normalizeListAttributes(Map> exportedAttributes, + Map> baselineAttributes) { + var exportedOrEmpty = getNonNull(exportedAttributes); + var baselineOrEmpty = getNonNull(baselineAttributes); + var normalizedAttributes = new HashMap>(); + for (var entry : baselineOrEmpty.entrySet()) { + var attributeName = entry.getKey(); + var baselineAttribute = entry.getValue(); + var exportedAttribute = exportedOrEmpty.remove(attributeName); + + if (unOrderedJavers.compareCollections(baselineAttribute, exportedAttribute, String.class).hasChanges()) { + normalizedAttributes.put(attributeName, exportedAttribute); + } + } + normalizedAttributes.putAll(exportedOrEmpty); + return normalizedAttributes.isEmpty() ? null : normalizedAttributes; + } + + public boolean listAttributesChanged(Map> exportedAttributes, Map> baselineAttributes) { + var exportedOrEmpty = getNonNull(exportedAttributes); + var baselineOrEmpty = getNonNull(baselineAttributes); + + if (!Objects.equals(exportedOrEmpty.keySet(), baselineOrEmpty.keySet())) { + return true; + } + + for (var entry : baselineOrEmpty.entrySet()) { + if (unOrderedJavers.compareCollections(entry.getValue(), + exportedOrEmpty.get(entry.getKey()), String.class).hasChanges()) { + return true; + } + } + return false; + } + +} diff --git a/src/main/java/de/adorsys/keycloak/config/service/normalize/AuthFlowNormalizationService.java b/src/main/java/de/adorsys/keycloak/config/service/normalize/AuthFlowNormalizationService.java new file mode 100644 index 000000000..6f00051a4 --- /dev/null +++ b/src/main/java/de/adorsys/keycloak/config/service/normalize/AuthFlowNormalizationService.java @@ -0,0 +1,238 @@ +/*- + * ---license-start + * keycloak-config-cli + * --- + * Copyright (C) 2017 - 2022 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.normalize; + +import org.javers.core.Javers; +import org.keycloak.representations.idm.AbstractAuthenticationExecutionRepresentation; +import org.keycloak.representations.idm.AuthenticationExecutionExportRepresentation; +import org.keycloak.representations.idm.AuthenticationFlowRepresentation; +import org.keycloak.representations.idm.AuthenticatorConfigRepresentation; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static de.adorsys.keycloak.config.service.normalize.RealmNormalizationService.getNonNull; +import static java.util.function.Predicate.not; + +@Service +@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "NORMALIZE") +public class AuthFlowNormalizationService { + + private static final Logger logger = LoggerFactory.getLogger(AuthFlowNormalizationService.class); + + private final Javers unOrderedJavers; + + public AuthFlowNormalizationService(Javers unOrderedJavers) { + this.unOrderedJavers = unOrderedJavers; + } + + public List normalizeAuthFlows(List exportedAuthFlows, + List baselineAuthFlows) { + var exportedFiltered = filterBuiltIn(exportedAuthFlows); + var baselineFiltered = filterBuiltIn(baselineAuthFlows); + + Map exportedMap = exportedFiltered.stream() + .collect(Collectors.toMap(AuthenticationFlowRepresentation::getAlias, Function.identity())); + Map baselineMap = baselineFiltered.stream() + .collect(Collectors.toMap(AuthenticationFlowRepresentation::getAlias, Function.identity())); + + List normalizedFlows = new ArrayList<>(); + for (var entry : baselineMap.entrySet()) { + var alias = entry.getKey(); + var exportedFlow = exportedMap.remove(alias); + if (exportedFlow == null) { + logger.warn("Default realm authentication flow '{}' was deleted in exported realm. It may be reintroduced during import", alias); + continue; + } + var baselineFlow = entry.getValue(); + var diff = unOrderedJavers.compare(baselineFlow, exportedFlow); + + if (diff.hasChanges() || executionsChanged(exportedFlow.getAuthenticationExecutions(), baselineFlow.getAuthenticationExecutions())) { + normalizedFlows.add(exportedFlow); + } + } + normalizedFlows.addAll(exportedMap.values()); + for (var flow : normalizedFlows) { + flow.setId(null); + } + normalizedFlows = filterUnusedNonTopLevel(normalizedFlows); + detectBrokenAuthenticationFlows(normalizedFlows); + return normalizedFlows.isEmpty() ? null : normalizedFlows; + } + + public void detectBrokenAuthenticationFlows(List flows) { + var flowsByAlias = flows.stream().collect(Collectors.toMap(AuthenticationFlowRepresentation::getAlias, Function.identity())); + for (var flow : flows) { + for (var execution : flow.getAuthenticationExecutions()) { + var flowAlias = execution.getFlowAlias(); + var authenticator = execution.getAuthenticator(); + + if (flowAlias != null && authenticator != null) { + var referencedFlow = flowsByAlias.get(flowAlias); + if (!"form-flow".equals(referencedFlow.getProviderId())) { + logger.error("An execution of flow '{}' defines an authenticator and references the sub-flow '{}'." + + " This is only possible if the sub-flow is of type 'form-flow', but it is of type '{}'." + + " keycloak-config-cli will refuse to import this flow. See NORMALIZE.md for more information.", + flow.getAlias(), flowAlias, referencedFlow.getProviderId()); + } + } + } + } + + } + + public List normalizeAuthConfig(List configs, + List flows) { + var flowsOrEmpty = getNonNull(flows); + // Find out which configs are actually used by the normalized flows + var usedConfigs = flowsOrEmpty.stream() + .map(AuthenticationFlowRepresentation::getAuthenticationExecutions) + .map(l -> l.stream() + .map(AbstractAuthenticationExecutionRepresentation::getAuthenticatorConfig) + .collect(Collectors.toList())).flatMap(Collection::stream) + .collect(Collectors.toSet()); + + var configOrEmpty = getNonNull(configs); + // Only return configs that are used + var filteredConfigs = configOrEmpty.stream() + .filter(acr -> usedConfigs.contains(acr.getAlias())).collect(Collectors.toList()); + + var duplicates = new HashSet(); + var seen = new HashSet(); + for (var config : filteredConfigs) { + config.setId(null); + if (seen.contains(config.getAlias())) { + duplicates.add(config.getAlias()); + } else { + seen.add(config.getAlias()); + } + } + + if (!duplicates.isEmpty()) { + logger.warn("The following authenticator configs are duplicates: {}. " + + "Check NORMALIZE.md for an SQL query to find the offending entries in your database!", duplicates); + } + + if (configs.size() != filteredConfigs.size()) { + logger.warn("Some authenticator configs are unused. Check NORMALIZE.md for an SQL query to find the offending entries in your database!"); + } + return filteredConfigs.isEmpty() ? null : filteredConfigs; + } + + private List filterBuiltIn(List flows) { + if (flows == null) { + return new ArrayList<>(); + } + return flows.stream().filter(not(AuthenticationFlowRepresentation::isBuiltIn)).collect(Collectors.toList()); + } + + private List filterUnusedNonTopLevel(List flows) { + // Assume all top level flows are used + var usedFlows = flows.stream().filter(AuthenticationFlowRepresentation::isTopLevel).collect(Collectors.toList()); + var potentialUnused = flows.stream().filter(not(AuthenticationFlowRepresentation::isTopLevel)) + .collect(Collectors.toMap(AuthenticationFlowRepresentation::getAlias, Function.identity())); + var toCheck = new ArrayList<>(usedFlows); + while (!toCheck.isEmpty()) { + var toRemove = new ArrayList(); + for (var flow : toCheck) { + for (var execution : flow.getAuthenticationExecutions()) { + var alias = execution.getFlowAlias(); + if (alias != null && potentialUnused.containsKey(alias)) { + toRemove.add(alias); + } + } + } + toCheck.clear(); + for (var alias : toRemove) { + toCheck.add(potentialUnused.remove(alias)); + } + usedFlows.addAll(toCheck); + } + if (usedFlows.size() != flows.size()) { + logger.warn("The following authentication flows are unused: {}. " + + "Check NORMALIZE.md for an SQL query to find the offending entries in your database!", potentialUnused.keySet()); + } + return usedFlows; + } + + public boolean executionsChanged(List exportedExecutions, + List baselineExecutions) { + if (exportedExecutions == null && baselineExecutions != null) { + return true; + } + + if (exportedExecutions != null && baselineExecutions == null) { + return true; + } + + if (exportedExecutions == null) { + return false; + } + + if (exportedExecutions.size() != baselineExecutions.size()) { + return true; + } + + exportedExecutions.sort(Comparator.comparing(AbstractAuthenticationExecutionRepresentation::getPriority)); + baselineExecutions.sort(Comparator.comparing(AbstractAuthenticationExecutionRepresentation::getPriority)); + + for (int i = 0; i < exportedExecutions.size(); i++) { + if (executionChanged(exportedExecutions.get(i), baselineExecutions.get(i))) { + return true; + } + } + return false; + } + + public boolean executionChanged(AuthenticationExecutionExportRepresentation exportedExecution, + AuthenticationExecutionExportRepresentation baselineExecution) { + if (!Objects.equals(exportedExecution.getAuthenticatorConfig(), baselineExecution.getAuthenticatorConfig())) { + return true; + } + if (!Objects.equals(exportedExecution.getAuthenticator(), baselineExecution.getAuthenticator())) { + return true; + } + if (!Objects.equals(exportedExecution.isAuthenticatorFlow(), baselineExecution.isAuthenticatorFlow())) { + return true; + } + if (!Objects.equals(exportedExecution.getRequirement(), baselineExecution.getRequirement())) { + return true; + } + if (!Objects.equals(exportedExecution.getPriority(), baselineExecution.getPriority())) { + return true; + } + if (!Objects.equals(exportedExecution.getFlowAlias(), baselineExecution.getFlowAlias())) { + return true; + } + return !Objects.equals(exportedExecution.isUserSetupAllowed(), baselineExecution.isUserSetupAllowed()); + } +} diff --git a/src/main/java/de/adorsys/keycloak/config/service/normalize/ClientNormalizationService.java b/src/main/java/de/adorsys/keycloak/config/service/normalize/ClientNormalizationService.java new file mode 100644 index 000000000..e36b40063 --- /dev/null +++ b/src/main/java/de/adorsys/keycloak/config/service/normalize/ClientNormalizationService.java @@ -0,0 +1,169 @@ +/*- + * ---license-start + * keycloak-config-cli + * --- + * Copyright (C) 2017 - 2022 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.normalize; + +import de.adorsys.keycloak.config.provider.BaselineProvider; +import de.adorsys.keycloak.config.util.JaversUtil; +import org.javers.core.Javers; +import org.javers.core.diff.changetype.PropertyChange; +import org.keycloak.representations.idm.AuthenticationFlowRepresentation; +import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.ProtocolMapperRepresentation; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.representations.idm.authorization.ResourceServerRepresentation; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import static de.adorsys.keycloak.config.service.normalize.RealmNormalizationService.getNonNull; + +@Service +@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "NORMALIZE") +public class ClientNormalizationService { + + private static final Set SAML_ATTRIBUTES = Set.of("saml_signature_canonicalization_method", "saml.onetimeuse.condition", + "saml_name_id_format", "saml.authnstatement", "saml.server.signature.keyinfo$xmlSigKeyInfoKeyNameTransformer", + "saml_force_name_id_format", "saml.artifact.binding", "saml.artifact.binding.identifier", "saml.server.signature", "saml.encrypt", + "saml.assertion.signature", "saml.allow.ecp.flow", "saml.signing.private.key", "saml.force.name.id.format", "saml.client.signature", + "saml.signature.algorithm", "saml.signing.certificate", "saml.server.signature.keyinfo.ext", "saml.multivalued.roles", + "saml.force.post.binding"); + + private static final Logger logger = LoggerFactory.getLogger(ClientNormalizationService.class); + private final Javers unOrderedJavers; + private final BaselineProvider baselineProvider; + private final JaversUtil javersUtil; + + public ClientNormalizationService(Javers unOrderedJavers, + BaselineProvider baselineProvider, + JaversUtil javersUtil) { + this.unOrderedJavers = unOrderedJavers; + this.baselineProvider = baselineProvider; + this.javersUtil = javersUtil; + } + + public List normalizeClients(RealmRepresentation exportedRealm, RealmRepresentation baselineRealm) { + var exportedOrEmpty = getNonNull(exportedRealm.getClients()); + var baselineOrEmpty = getNonNull(baselineRealm.getClients()); + var exportedClientMap = new HashMap(); + for (var exportedClient : exportedOrEmpty) { + exportedClientMap.put(exportedClient.getClientId(), exportedClient); + } + + var baselineClientMap = new HashMap(); + var clients = new ArrayList(); + for (var baselineRealmClient : baselineOrEmpty) { + var clientId = baselineRealmClient.getClientId(); + baselineClientMap.put(clientId, baselineRealmClient); + var exportedClient = exportedClientMap.get(clientId); + if (exportedClient == null) { + logger.warn("Default realm client '{}' was deleted in exported realm. It may be reintroduced during import!", clientId); + /* + * Here we need to define a configuration parameter: If we want the import *not* to reintroduce default clients that were + * deleted, we need to add *all* clients, not just default clients to the dump. Then during import, set the mode that + * makes clients fully managed, so that *only* clients that are in the dump end up in the realm + */ + continue; + } + if (clientChanged(exportedClient, baselineRealmClient)) { + // We know the client has changed in some way. Now, compare it to a default client to minimize it + clients.add(normalizeClient(exportedClient, exportedRealm.getKeycloakVersion(), exportedRealm)); + } + } + + // Now iterate over all the clients that are *not* default clients + for (Map.Entry e : exportedClientMap.entrySet()) { + if (!baselineClientMap.containsKey(e.getKey())) { + clients.add(normalizeClient(e.getValue(), exportedRealm.getKeycloakVersion(), exportedRealm)); + } + } + return clients; + } + + public ClientRepresentation normalizeClient(ClientRepresentation client, String keycloakVersion, RealmRepresentation exportedRealm) { + var clientId = client.getClientId(); + var baselineClient = baselineProvider.getClient(keycloakVersion, clientId); + var diff = unOrderedJavers.compare(baselineClient, client); + var normalizedClient = new ClientRepresentation(); + for (var change : diff.getChangesByType(PropertyChange.class)) { + javersUtil.applyChange(normalizedClient, change); + } + + // Always include protocol, even if it's the default "openid-connect" + normalizedClient.setProtocol(client.getProtocol()); + var mappers = client.getProtocolMappers(); + normalizedClient.setProtocolMappers(mappers); + if (mappers != null) { + for (var mapper : mappers) { + mapper.setId(null); + } + } + normalizedClient.setAuthorizationSettings(client.getAuthorizationSettings()); + normalizedClient.setClientId(clientId); + + // Older versions of keycloak include SAML attributes even in OIDC clients. Ignore these. + if (normalizedClient.getProtocol().equals("openid-connect") && normalizedClient.getAttributes() != null) { + normalizedClient.getAttributes().keySet().removeIf(SAML_ATTRIBUTES::contains); + } + + if (normalizedClient.getAuthenticationFlowBindingOverrides() != null) { + var overrides = new HashMap(); + var flows = exportedRealm.getAuthenticationFlows().stream() + .collect(Collectors.toMap(AuthenticationFlowRepresentation::getId, AuthenticationFlowRepresentation::getAlias)); + for (var entry : normalizedClient.getAuthenticationFlowBindingOverrides().entrySet()) { + var id = entry.getValue(); + overrides.put(entry.getKey(), flows.get(id)); + } + normalizedClient.setAuthenticationFlowBindingOverrides(overrides); + } + normalizedClient.setPublicClient(client.isPublicClient()); + return normalizedClient; + } + + public boolean clientChanged(ClientRepresentation exportedClient, ClientRepresentation baselineClient) { + var diff = unOrderedJavers.compare(baselineClient, exportedClient); + if (diff.hasChanges()) { + return true; + } + if (protocolMappersChanged(exportedClient.getProtocolMappers(), baselineClient.getProtocolMappers())) { + return true; + } + return authorizationSettingsChanged(exportedClient.getAuthorizationSettings(), baselineClient.getAuthorizationSettings()); + } + + public boolean protocolMappersChanged(List exportedMappers, List baselineMappers) { + // CompareCollections doesn't handle nulls gracefully + return unOrderedJavers.compareCollections(getNonNull(baselineMappers), getNonNull(exportedMappers), ProtocolMapperRepresentation.class) + .hasChanges(); + } + + public boolean authorizationSettingsChanged(ResourceServerRepresentation exportedSettings, ResourceServerRepresentation baselineSettings) { + return unOrderedJavers.compare(baselineSettings, exportedSettings).hasChanges(); + } + +} diff --git a/src/main/java/de/adorsys/keycloak/config/service/normalize/ClientPolicyNormalizationService.java b/src/main/java/de/adorsys/keycloak/config/service/normalize/ClientPolicyNormalizationService.java new file mode 100644 index 000000000..823e13257 --- /dev/null +++ b/src/main/java/de/adorsys/keycloak/config/service/normalize/ClientPolicyNormalizationService.java @@ -0,0 +1,49 @@ +/*- + * ---license-start + * keycloak-config-cli + * --- + * Copyright (C) 2017 - 2023 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.normalize; + +import org.keycloak.representations.idm.ClientPoliciesRepresentation; +import org.keycloak.representations.idm.ClientProfilesRepresentation; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Service; + +@Service +@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "NORMALIZE") +public class ClientPolicyNormalizationService { + + public ClientPoliciesRepresentation normalizePolicies(ClientPoliciesRepresentation exportedPolicies, + ClientPoliciesRepresentation baselinePolicies) { + var policies = exportedPolicies.getPolicies(); + if (policies == null || policies.isEmpty()) { + return null; + } + return exportedPolicies; + } + + public ClientProfilesRepresentation normalizeProfiles(ClientProfilesRepresentation exportedProfiles, + ClientProfilesRepresentation baselineProfiles) { + var profiles = exportedProfiles.getProfiles(); + if (profiles == null || profiles.isEmpty()) { + return null; + } + return exportedProfiles; + } +} diff --git a/src/main/java/de/adorsys/keycloak/config/service/normalize/ClientScopeNormalizationService.java b/src/main/java/de/adorsys/keycloak/config/service/normalize/ClientScopeNormalizationService.java new file mode 100644 index 000000000..3825beee2 --- /dev/null +++ b/src/main/java/de/adorsys/keycloak/config/service/normalize/ClientScopeNormalizationService.java @@ -0,0 +1,107 @@ +/*- + * ---license-start + * keycloak-config-cli + * --- + * Copyright (C) 2017 - 2022 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.normalize; + +import org.javers.core.Javers; +import org.keycloak.representations.idm.ClientScopeRepresentation; +import org.keycloak.representations.idm.ProtocolMapperRepresentation; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static de.adorsys.keycloak.config.service.normalize.RealmNormalizationService.getNonNull; + +@Service +@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "NORMALIZE") +public class ClientScopeNormalizationService { + + private static final Logger logger = LoggerFactory.getLogger(ClientScopeNormalizationService.class); + + private final Javers unOrderedJavers; + + public ClientScopeNormalizationService(Javers unOrderedJavers) { + this.unOrderedJavers = unOrderedJavers; + } + + public List normalizeClientScopes(List exportedScopes, + List baselineScopes) { + var exportedOrEmpty = getNonNull(exportedScopes); + var baselineOrEmpty = getNonNull(baselineScopes); + + var exportedMap = exportedOrEmpty.stream().collect(Collectors.toMap(ClientScopeRepresentation::getName, + Function.identity())); + var baselineMap = baselineOrEmpty.stream().collect(Collectors.toMap(ClientScopeRepresentation::getName, + Function.identity())); + + var normalizedScopes = new ArrayList(); + for (var entry : baselineMap.entrySet()) { + var scopeName = entry.getKey(); + var baselineScope = entry.getValue(); + var exportedScope = exportedMap.remove(scopeName); + + if (exportedScope == null) { + logger.warn("Default realm clientScope '{}' was deleted in exported realm. It may be reintroduced during import!", scopeName); + continue; + } + + if (clientScopeChanged(exportedScope, baselineScope)) { + normalizedScopes.add(exportedScope); + } + } + normalizedScopes.addAll(exportedMap.values()); + + normalizeList(normalizedScopes); + return normalizedScopes.isEmpty() ? null : normalizedScopes; + } + + private static void normalizeList(ArrayList normalizedScopes) { + for (var scope : normalizedScopes) { + scope.setId(null); + if (scope.getProtocolMappers() != null) { + for (var mapper : scope.getProtocolMappers()) { + mapper.setId(null); + } + if (scope.getProtocolMappers().isEmpty()) { + scope.setProtocolMappers(null); + } + } + } + } + + public boolean clientScopeChanged(ClientScopeRepresentation exportedScope, ClientScopeRepresentation baselineScope) { + if (unOrderedJavers.compare(baselineScope, exportedScope).hasChanges()) { + return true; + } + + return protocolMappersChanged(exportedScope.getProtocolMappers(), baselineScope.getProtocolMappers()); + } + + public boolean protocolMappersChanged(List exportedMappers, List baselineMappers) { + return unOrderedJavers.compareCollections(getNonNull(baselineMappers), getNonNull(exportedMappers), + ProtocolMapperRepresentation.class).hasChanges(); + } +} diff --git a/src/main/java/de/adorsys/keycloak/config/service/normalize/ComponentNormalizationService.java b/src/main/java/de/adorsys/keycloak/config/service/normalize/ComponentNormalizationService.java new file mode 100644 index 000000000..274fa709f --- /dev/null +++ b/src/main/java/de/adorsys/keycloak/config/service/normalize/ComponentNormalizationService.java @@ -0,0 +1,90 @@ +/*- + * ---license-start + * keycloak-config-cli + * --- + * Copyright (C) 2017 - 2022 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.normalize; + +import org.javers.core.Javers; +import org.keycloak.common.util.MultivaluedHashMap; +import org.keycloak.representations.idm.ComponentExportRepresentation; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Service; + +import java.util.List; + +import static de.adorsys.keycloak.config.service.normalize.RealmNormalizationService.getNonNull; + +@Service +@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "NORMALIZE") +public class ComponentNormalizationService { + + private static final Logger logger = LoggerFactory.getLogger(ComponentNormalizationService.class); + + private final Javers unOrderedJavers; + + public ComponentNormalizationService(Javers unOrderedJavers) { + this.unOrderedJavers = unOrderedJavers; + } + + public MultivaluedHashMap + normalizeComponents(MultivaluedHashMap exportedComponents, + MultivaluedHashMap baselineComponents) { + var exportedOrEmpty = getNonNull(exportedComponents); + var baselineOrEmpty = getNonNull(baselineComponents); + + var normalizedMap = new MultivaluedHashMap(); + for (var entry : baselineOrEmpty.entrySet()) { + var componentClass = entry.getKey(); + + var exportedList = exportedOrEmpty.remove(componentClass); + + if (exportedList == null) { + logger.warn("Default realm component '{}' was deleted in exported realm. It may be reintroduced during import!", componentClass); + continue; + } + var baselineList = entry.getValue(); + var normalizedList = normalizeList(exportedList, baselineList, componentClass); + normalizedMap.put(componentClass, normalizedList); + } + normalizedMap.putAll(exportedOrEmpty); + //var toRemove = new HashSet(); + for (var entry : normalizedMap.entrySet()) { + var componentList = entry.getValue(); + for (var component : componentList) { + normalizeEntry(component); + } + } + return new MultivaluedHashMap<>(); + } + + public List normalizeList(List exportedComponents, + List baselineComponents, + String componentClass) { + return List.of(); + } + + public void normalizeEntry(ComponentExportRepresentation component) { + component.setId(null); + if (component.getConfig() != null && component.getConfig().isEmpty()) { + component.setConfig(null); + } + } +} diff --git a/src/main/java/de/adorsys/keycloak/config/service/normalize/GroupNormalizationService.java b/src/main/java/de/adorsys/keycloak/config/service/normalize/GroupNormalizationService.java new file mode 100644 index 000000000..10cf28c6b --- /dev/null +++ b/src/main/java/de/adorsys/keycloak/config/service/normalize/GroupNormalizationService.java @@ -0,0 +1,141 @@ +/*- + * ---license-start + * keycloak-config-cli + * --- + * Copyright (C) 2017 - 2022 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.normalize; + +import org.javers.core.Javers; +import org.keycloak.representations.idm.GroupRepresentation; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static de.adorsys.keycloak.config.service.normalize.RealmNormalizationService.getNonNull; + +@Service +@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "NORMALIZE") +public class GroupNormalizationService { + + private static final Logger logger = LoggerFactory.getLogger(GroupNormalizationService.class); + + private final Javers unOrderedJavers; + private final AttributeNormalizationService attributeNormalizationService; + + public GroupNormalizationService(Javers unOrderedJavers, + AttributeNormalizationService attributeNormalizationService) { + this.unOrderedJavers = unOrderedJavers; + this.attributeNormalizationService = attributeNormalizationService; + } + + public List normalizeGroups(List exportedGroups, List baselineGroups) { + var exportedOrEmpty = getNonNull(exportedGroups); + var baselineOrEmpty = getNonNull(baselineGroups); + var exportedGroupsMap = exportedOrEmpty.stream() + .collect(Collectors.toMap(GroupRepresentation::getPath, Function.identity())); + var baselineGroupsMap = baselineOrEmpty.stream() + .collect(Collectors.toMap(GroupRepresentation::getPath, Function.identity())); + + var normalizedGroups = new ArrayList(); + for (var entry : baselineGroupsMap.entrySet()) { + var groupPath = entry.getKey(); + var exportedGroup = exportedGroupsMap.remove(groupPath); + if (exportedGroup == null) { + logger.warn("Default realm group '{}' was deleted in exported realm. It may be reintroduced during import", groupPath); + continue; + } + var baselineGroup = entry.getValue(); + var diff = unOrderedJavers.compare(baselineGroup, exportedGroup); + + if (diff.hasChanges() || subGroupsChanged(exportedGroup, baselineGroup) + || attributeNormalizationService.listAttributesChanged(exportedGroup.getAttributes(), baselineGroup.getAttributes()) + || attributeNormalizationService.listAttributesChanged(exportedGroup.getClientRoles(), baselineGroup.getClientRoles())) { + normalizedGroups.add(exportedGroup); + } + } + normalizedGroups.addAll(exportedGroupsMap.values()); + normalizeGroupList(normalizedGroups); + return normalizedGroups.isEmpty() ? null : normalizedGroups; + } + + public boolean subGroupsChanged(GroupRepresentation exportedGroup, GroupRepresentation baselineGroup) { + if (exportedGroup.getSubGroups() == null && baselineGroup.getSubGroups() != null) { + return true; + } + if (exportedGroup.getSubGroups() != null && baselineGroup.getSubGroups() == null) { + return true; + } + if (exportedGroup.getSubGroups() == null && baselineGroup.getSubGroups() == null) { + return false; + } + + Map exportedSubGroups = exportedGroup.getSubGroups().stream() + .collect(Collectors.toMap(GroupRepresentation::getPath, Function.identity())); + Map baselineSubGroups = baselineGroup.getSubGroups().stream() + .collect(Collectors.toMap(GroupRepresentation::getPath, Function.identity())); + + for (var entry : baselineSubGroups.entrySet()) { + var groupPath = entry.getKey(); + var exportedSubGroup = exportedSubGroups.remove(groupPath); + + if (exportedSubGroup == null) { + // There's a subgroup in the baseline that's gone in the export. This counts as a change. + return true; + } + var baselineSubGroup = entry.getValue(); + if (unOrderedJavers.compare(baselineSubGroup, exportedSubGroup).hasChanges()) { + return true; + } + if (subGroupsChanged(exportedSubGroup, baselineSubGroup)) { + return true; + } + } + + // There are subgroups in the export that are not in the baseline. This is a change. + return !exportedSubGroups.isEmpty(); + } + + public void normalizeGroupList(List groups) { + for (var group : groups) { + if (group.getAttributes() != null && group.getAttributes().isEmpty()) { + group.setAttributes(null); + } + if (group.getRealmRoles() != null && group.getRealmRoles().isEmpty()) { + group.setRealmRoles(null); + } + if (group.getClientRoles() != null && group.getClientRoles().isEmpty()) { + group.setClientRoles(null); + } + if (group.getSubGroups() != null) { + if (group.getSubGroups().isEmpty()) { + group.setSubGroups(null); + } else { + normalizeGroupList(group.getSubGroups()); + } + } + group.setId(null); + } + } +} diff --git a/src/main/java/de/adorsys/keycloak/config/service/normalize/IdentityProviderNormalizationService.java b/src/main/java/de/adorsys/keycloak/config/service/normalize/IdentityProviderNormalizationService.java new file mode 100644 index 000000000..a5d87cdcf --- /dev/null +++ b/src/main/java/de/adorsys/keycloak/config/service/normalize/IdentityProviderNormalizationService.java @@ -0,0 +1,149 @@ +/*- + * ---license-start + * keycloak-config-cli + * --- + * Copyright (C) 2017 - 2022 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.normalize; + +import org.javers.core.Javers; +import org.keycloak.representations.idm.IdentityProviderMapperRepresentation; +import org.keycloak.representations.idm.IdentityProviderRepresentation; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static de.adorsys.keycloak.config.service.normalize.RealmNormalizationService.getNonNull; + +@Service +@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "NORMALIZE") +public class IdentityProviderNormalizationService { + + private static final Logger logger = LoggerFactory.getLogger(IdentityProviderNormalizationService.class); + + private final Javers unOrderedJavers; + + public IdentityProviderNormalizationService(Javers unOrderedJavers) { + this.unOrderedJavers = unOrderedJavers; + } + + public List normalizeProviders(List exportedProviders, + List baselineProviders) { + var exportedOrEmpty = getNonNull(exportedProviders); + var baselineOrEmpty = getNonNull(baselineProviders); + + var exportedMap = exportedOrEmpty.stream() + .collect(Collectors.toMap(IdentityProviderRepresentation::getAlias, Function.identity())); + var baselineMap = baselineOrEmpty.stream() + .collect(Collectors.toMap(IdentityProviderRepresentation::getAlias, Function.identity())); + + var normalizedProviders = new ArrayList(); + for (var entry : baselineMap.entrySet()) { + var alias = entry.getKey(); + var exportedProvider = exportedMap.remove(alias); + if (exportedProvider == null) { + logger.warn("Default realm identityProvider '{}' was deleted in exported realm. It may be reintroduced during import!", alias); + continue; + } + var baselineProvider = entry.getValue(); + + var diff = unOrderedJavers.compare(baselineProvider, exportedProvider); + if (diff.hasChanges()) { + normalizedProviders.add(exportedProvider); + } + } + normalizedProviders.addAll(exportedMap.values()); + for (var provider : normalizedProviders) { + provider.setInternalId(null); + if (provider.getConfig() != null && provider.getConfig().isEmpty()) { + provider.setConfig(null); + } + } + return normalizedProviders.isEmpty() ? null : normalizedProviders; + } + + public List normalizeMappers(List exportedMappers, + List baselineMappers) { + var exportedOrEmpty = getNonNull(exportedMappers); + var baselineOrEmpty = getNonNull(baselineMappers); + + var exportedMap = exportedOrEmpty.stream() + .collect(Collectors.toMap(m -> new MapperKey(m.getName(), m.getIdentityProviderAlias()), Function.identity())); + var baselineMap = baselineOrEmpty.stream() + .collect(Collectors.toMap(m -> new MapperKey(m.getName(), m.getIdentityProviderAlias()), Function.identity())); + + var normalizedMappers = new ArrayList(); + for (var entry : baselineMap.entrySet()) { + var key = entry.getKey(); + var exportedMapper = exportedMap.remove(key); + if (exportedMapper == null) { + logger.warn("Default realm identityProviderMapper '{}' for idp '{}' was deleted in exported realm." + + "It may be reintroduced during import!", key.getName(), key.getIdentityProviderAlias()); + continue; + } + var baselineMapper = entry.getValue(); + + var diff = unOrderedJavers.compare(baselineMapper, exportedMapper); + if (diff.hasChanges()) { + normalizedMappers.add(exportedMapper); + } + } + normalizedMappers.addAll(exportedMap.values()); + for (var mapper : normalizedMappers) { + mapper.setId(null); + } + return normalizedMappers.isEmpty() ? null : normalizedMappers; + } + + private static class MapperKey { + private final String name; + private final String identityProviderAlias; + + public MapperKey(String name, String identityProviderAlias) { + this.name = name; + this.identityProviderAlias = identityProviderAlias; + } + + public String getName() { + return name; + } + + public String getIdentityProviderAlias() { + return identityProviderAlias; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + MapperKey mapperKey = (MapperKey) o; + return Objects.equals(name, mapperKey.name) && Objects.equals(identityProviderAlias, mapperKey.identityProviderAlias); + } + + @Override + public int hashCode() { + return Objects.hash(name, identityProviderAlias); + } + } +} diff --git a/src/main/java/de/adorsys/keycloak/config/service/normalize/ProtocolMapperNormalizationService.java b/src/main/java/de/adorsys/keycloak/config/service/normalize/ProtocolMapperNormalizationService.java new file mode 100644 index 000000000..1a65f85b6 --- /dev/null +++ b/src/main/java/de/adorsys/keycloak/config/service/normalize/ProtocolMapperNormalizationService.java @@ -0,0 +1,82 @@ +/*- + * ---license-start + * keycloak-config-cli + * --- + * Copyright (C) 2017 - 2022 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.normalize; + +import org.javers.core.Javers; +import org.keycloak.representations.idm.ProtocolMapperRepresentation; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static de.adorsys.keycloak.config.service.normalize.RealmNormalizationService.getNonNull; + +@Service +@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "NORMALIZE") +public class ProtocolMapperNormalizationService { + + private static final Logger logger = LoggerFactory.getLogger(IdentityProviderNormalizationService.class); + + private final Javers unOrderedJavers; + + public ProtocolMapperNormalizationService(Javers unOrderedJavers) { + this.unOrderedJavers = unOrderedJavers; + } + + public List normalizeProtocolMappers(List exportedMappers, + List baselineMappers) { + var exportedOrEmpty = getNonNull(exportedMappers); + var baselineOrEmpty = getNonNull(baselineMappers); + + var exportedMap = exportedOrEmpty.stream() + .collect(Collectors.toMap(ProtocolMapperRepresentation::getName, Function.identity())); + var baselineMap = baselineOrEmpty.stream() + .collect(Collectors.toMap(ProtocolMapperRepresentation::getName, Function.identity())); + var normalizedMappers = new ArrayList(); + + for (var entry : baselineMap.entrySet()) { + var name = entry.getKey(); + var exportedMapper = exportedMap.remove(name); + if (exportedMapper == null) { + logger.warn("Default realm protocolMapper '{}' was deleted in exported realm. It may be reintroduced during import!", name); + continue; + } + + var baselineMapper = entry.getValue(); + if (unOrderedJavers.compare(baselineMapper, exportedMapper).hasChanges()) { + normalizedMappers.add(exportedMapper); + } + } + normalizedMappers.addAll(exportedMap.values()); + for (var mapper : normalizedMappers) { + mapper.setId(null); + if (mapper.getConfig() != null && mapper.getConfig().isEmpty()) { + mapper.setConfig(null); + } + } + return normalizedMappers.isEmpty() ? null : normalizedMappers; + } +} diff --git a/src/main/java/de/adorsys/keycloak/config/service/normalize/RealmNormalizationService.java b/src/main/java/de/adorsys/keycloak/config/service/normalize/RealmNormalizationService.java new file mode 100644 index 000000000..cb9e1b16b --- /dev/null +++ b/src/main/java/de/adorsys/keycloak/config/service/normalize/RealmNormalizationService.java @@ -0,0 +1,211 @@ +/*- + * ---license-start + * keycloak-config-cli + * --- + * Copyright (C) 2017 - 2022 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.normalize; + +import de.adorsys.keycloak.config.properties.NormalizationKeycloakConfigProperties; +import de.adorsys.keycloak.config.provider.BaselineProvider; +import de.adorsys.keycloak.config.util.JaversUtil; +import org.javers.core.Javers; +import org.javers.core.diff.changetype.PropertyChange; +import org.keycloak.representations.idm.RealmRepresentation; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +@Service +@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "NORMALIZE") +public class RealmNormalizationService { + + private static final Logger logger = LoggerFactory.getLogger(RealmNormalizationService.class); + + private final NormalizationKeycloakConfigProperties keycloakConfigProperties; + private final Javers javers; + private final BaselineProvider baselineProvider; + private final ClientNormalizationService clientNormalizationService; + private final ScopeMappingNormalizationService scopeMappingNormalizationService; + private final ProtocolMapperNormalizationService protocolMapperNormalizationService; + private final ClientScopeNormalizationService clientScopeNormalizationService; + private final RoleNormalizationService roleNormalizationService; + private final AttributeNormalizationService attributeNormalizationService; + private final GroupNormalizationService groupNormalizationService; + private final AuthFlowNormalizationService authFlowNormalizationService; + private final IdentityProviderNormalizationService identityProviderNormalizationService; + private final RequiredActionNormalizationService requiredActionNormalizationService; + private final UserFederationNormalizationService userFederationNormalizationService; + private final ClientPolicyNormalizationService clientPolicyNormalizationService; + private final JaversUtil javersUtil; + + @Autowired + public RealmNormalizationService(NormalizationKeycloakConfigProperties keycloakConfigProperties, + Javers javers, + BaselineProvider baselineProvider, + ClientNormalizationService clientNormalizationService, + ScopeMappingNormalizationService scopeMappingNormalizationService, + ProtocolMapperNormalizationService protocolMapperNormalizationService, + ClientScopeNormalizationService clientScopeNormalizationService, + RoleNormalizationService roleNormalizationService, + AttributeNormalizationService attributeNormalizationService, + GroupNormalizationService groupNormalizationService, + AuthFlowNormalizationService authFlowNormalizationService, + IdentityProviderNormalizationService identityProviderNormalizationService, + RequiredActionNormalizationService requiredActionNormalizationService, + UserFederationNormalizationService userFederationNormalizationService, + ClientPolicyNormalizationService clientPolicyNormalizationService, + JaversUtil javersUtil) { + this.keycloakConfigProperties = keycloakConfigProperties; + this.javers = javers; + this.baselineProvider = baselineProvider; + this.clientNormalizationService = clientNormalizationService; + this.scopeMappingNormalizationService = scopeMappingNormalizationService; + this.protocolMapperNormalizationService = protocolMapperNormalizationService; + this.clientScopeNormalizationService = clientScopeNormalizationService; + this.roleNormalizationService = roleNormalizationService; + this.attributeNormalizationService = attributeNormalizationService; + this.groupNormalizationService = groupNormalizationService; + this.authFlowNormalizationService = authFlowNormalizationService; + this.identityProviderNormalizationService = identityProviderNormalizationService; + this.requiredActionNormalizationService = requiredActionNormalizationService; + this.userFederationNormalizationService = userFederationNormalizationService; + this.clientPolicyNormalizationService = clientPolicyNormalizationService; + this.javersUtil = javersUtil; + + // TODO allow extra "default" values to be ignored? + + // TODO Ignore clients by regex + } + + public RealmRepresentation normalizeRealm(RealmRepresentation exportedRealm) { + var keycloakConfigVersion = keycloakConfigProperties.getVersion(); + var exportVersion = exportedRealm.getKeycloakVersion(); + if (!exportVersion.equals(keycloakConfigVersion)) { + logger.warn("Keycloak-Config-CLI keycloak version {} and export keycloak version {} are not equal." + + " This may cause problems if the API changed." + + " Please compile keycloak-config-cli with a matching keycloak version!", + keycloakConfigVersion, exportVersion); + } + var exportedRealmRealm = exportedRealm.getRealm(); + logger.info("Exporting realm {}", exportedRealmRealm); + var baselineRealm = baselineProvider.getRealm(exportVersion, exportedRealmRealm); + + /* + * Trick javers into thinking this is the "same" object, by setting the ID on the reference realm + * to the ID of the current realm. That way we only get actual changes, not a full list of changes + * including the "object removed" and "object added" changes + */ + baselineRealm.setRealm(exportedRealm.getRealm()); + var minimizedRealm = new RealmRepresentation(); + + handleBaseRealm(exportedRealm, baselineRealm, minimizedRealm); + + var clients = clientNormalizationService.normalizeClients(exportedRealm, baselineRealm); + if (!clients.isEmpty()) { + minimizedRealm.setClients(clients); + } + + // No setter for some reason... + var minimizedScopeMappings = scopeMappingNormalizationService.normalizeScopeMappings(exportedRealm, baselineRealm); + if (!minimizedScopeMappings.isEmpty()) { + var scopeMappings = minimizedRealm.getScopeMappings(); + if (scopeMappings == null) { + minimizedRealm.clientScopeMapping("dummy"); + scopeMappings = minimizedRealm.getScopeMappings(); + scopeMappings.clear(); + } + scopeMappings.addAll(minimizedScopeMappings); + } + + var clientScopeMappings = scopeMappingNormalizationService.normalizeClientScopeMappings(exportedRealm, baselineRealm); + if (!clientScopeMappings.isEmpty()) { + minimizedRealm.setClientScopeMappings(clientScopeMappings); + } + + minimizedRealm.setAttributes(attributeNormalizationService.normalizeStringAttributes(exportedRealm.getAttributes(), + baselineRealm.getAttributes())); + + minimizedRealm.setProtocolMappers(protocolMapperNormalizationService.normalizeProtocolMappers(exportedRealm.getProtocolMappers(), + baselineRealm.getProtocolMappers())); + + minimizedRealm.setClientScopes(clientScopeNormalizationService.normalizeClientScopes(exportedRealm.getClientScopes(), + baselineRealm.getClientScopes())); + + minimizedRealm.setRoles(roleNormalizationService.normalizeRoles(exportedRealm.getRoles(), baselineRealm.getRoles())); + + minimizedRealm.setGroups(groupNormalizationService.normalizeGroups(exportedRealm.getGroups(), baselineRealm.getGroups())); + + var authFlows = authFlowNormalizationService.normalizeAuthFlows(exportedRealm.getAuthenticationFlows(), + baselineRealm.getAuthenticationFlows()); + minimizedRealm.setAuthenticationFlows(authFlows); + minimizedRealm.setAuthenticatorConfig(authFlowNormalizationService.normalizeAuthConfig(exportedRealm.getAuthenticatorConfig(), authFlows)); + + minimizedRealm.setIdentityProviders(identityProviderNormalizationService.normalizeProviders(exportedRealm.getIdentityProviders(), + baselineRealm.getIdentityProviders())); + minimizedRealm.setIdentityProviderMappers(identityProviderNormalizationService.normalizeMappers(exportedRealm.getIdentityProviderMappers(), + baselineRealm.getIdentityProviderMappers())); + + minimizedRealm.setRequiredActions(requiredActionNormalizationService.normalizeRequiredActions(exportedRealm.getRequiredActions(), + baselineRealm.getRequiredActions())); + minimizedRealm.setUserFederationProviders(userFederationNormalizationService.normalizeProviders(exportedRealm.getUserFederationProviders(), + baselineRealm.getUserFederationProviders())); + minimizedRealm.setUserFederationMappers(userFederationNormalizationService.normalizeMappers(exportedRealm.getUserFederationMappers(), + baselineRealm.getUserFederationMappers())); + + minimizedRealm.setParsedClientPolicies(clientPolicyNormalizationService.normalizePolicies(exportedRealm.getParsedClientPolicies(), + baselineRealm.getParsedClientPolicies())); + minimizedRealm.setParsedClientProfiles(clientPolicyNormalizationService.normalizeProfiles(exportedRealm.getParsedClientProfiles(), + baselineRealm.getParsedClientProfiles())); + return minimizedRealm; + } + + private void handleBaseRealm(RealmRepresentation exportedRealm, RealmRepresentation baselineRealm, RealmRepresentation minimizedRealm) { + var diff = javers.compare(baselineRealm, exportedRealm); + for (var change : diff.getChangesByType(PropertyChange.class)) { + javersUtil.applyChange(minimizedRealm, change); + } + + // Now that Javers is done, clean up a bit afterwards. We always need to set the realm and enabled fields + minimizedRealm.setRealm(exportedRealm.getRealm()); + minimizedRealm.setEnabled(exportedRealm.isEnabled()); + + // If the realm ID diverges from the name, include it in the dump, otherwise remove it + if (Objects.equals(exportedRealm.getRealm(), exportedRealm.getId())) { + minimizedRealm.setId(null); + } else { + minimizedRealm.setId(exportedRealm.getId()); + } + } + + + public static Map getNonNull(Map in) { + return in == null ? new HashMap<>() : in; + } + + public static List getNonNull(List in) { + return in == null ? new ArrayList<>() : in; + } +} diff --git a/src/main/java/de/adorsys/keycloak/config/service/normalize/RequiredActionNormalizationService.java b/src/main/java/de/adorsys/keycloak/config/service/normalize/RequiredActionNormalizationService.java new file mode 100644 index 000000000..361357d0d --- /dev/null +++ b/src/main/java/de/adorsys/keycloak/config/service/normalize/RequiredActionNormalizationService.java @@ -0,0 +1,77 @@ +/*- + * ---license-start + * keycloak-config-cli + * --- + * Copyright (C) 2017 - 2022 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.normalize; + +import org.javers.core.Javers; +import org.keycloak.representations.idm.RequiredActionProviderRepresentation; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static de.adorsys.keycloak.config.service.normalize.RealmNormalizationService.getNonNull; + +@Service +@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "NORMALIZE") +public class RequiredActionNormalizationService { + + private static final Logger logger = LoggerFactory.getLogger(RequiredActionNormalizationService.class); + + private final Javers javers; + + public RequiredActionNormalizationService(Javers javers) { + this.javers = javers; + } + + public List normalizeRequiredActions(List exportedActions, + List baselineActions) { + var exportedOrEmpty = getNonNull(exportedActions); + var baselineOrEmpty = getNonNull(baselineActions); + + var exportedMap = exportedOrEmpty.stream() + .collect(Collectors.toMap(RequiredActionProviderRepresentation::getAlias, Function.identity())); + var baselineMap = baselineOrEmpty.stream() + .collect(Collectors.toMap(RequiredActionProviderRepresentation::getAlias, Function.identity())); + + var normalizedActions = new ArrayList(); + for (var entry : baselineMap.entrySet()) { + var alias = entry.getKey(); + var exportedAction = exportedMap.remove(alias); + if (exportedAction == null) { + logger.warn("Default realm requiredAction '{}' was deleted in exported realm. It may be reintroduced during import", alias); + continue; + } + var baselineAction = entry.getValue(); + + var diff = javers.compare(baselineAction, exportedAction); + if (diff.hasChanges()) { + normalizedActions.add(exportedAction); + } + } + normalizedActions.addAll(exportedMap.values()); + return normalizedActions.isEmpty() ? null : normalizedActions; + } +} diff --git a/src/main/java/de/adorsys/keycloak/config/service/normalize/RoleNormalizationService.java b/src/main/java/de/adorsys/keycloak/config/service/normalize/RoleNormalizationService.java new file mode 100644 index 000000000..0952ca36d --- /dev/null +++ b/src/main/java/de/adorsys/keycloak/config/service/normalize/RoleNormalizationService.java @@ -0,0 +1,155 @@ +/*- + * ---license-start + * keycloak-config-cli + * --- + * Copyright (C) 2017 - 2022 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.normalize; + +import org.javers.core.Javers; +import org.keycloak.representations.idm.RoleRepresentation; +import org.keycloak.representations.idm.RolesRepresentation; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static de.adorsys.keycloak.config.service.normalize.RealmNormalizationService.getNonNull; + +@Service +@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "NORMALIZE") +public class RoleNormalizationService { + + private static final Logger logger = LoggerFactory.getLogger(RoleNormalizationService.class); + + private final Javers unOrderedJavers; + private final AttributeNormalizationService attributeNormalizationService; + + @Autowired + public RoleNormalizationService(Javers unOrderedJavers, AttributeNormalizationService attributeNormalizationService) { + this.unOrderedJavers = unOrderedJavers; + this.attributeNormalizationService = attributeNormalizationService; + } + + public RolesRepresentation normalizeRoles(RolesRepresentation exportedRoles, RolesRepresentation baselineRoles) { + var exportedOrEmpty = exportedRoles == null ? new RolesRepresentation() : exportedRoles; + var baselineOrEmpty = baselineRoles == null ? new RolesRepresentation() : baselineRoles; + var clientRoles = normalizeClientRoles(exportedOrEmpty.getClient(), baselineOrEmpty.getClient()); + var realmRoles = normalizeRealmRoles(exportedOrEmpty.getRealm(), baselineOrEmpty.getRealm()); + var normalizedRoles = new RolesRepresentation(); + if (!clientRoles.isEmpty()) { + normalizedRoles.setClient(clientRoles); + } + if (!realmRoles.isEmpty()) { + normalizedRoles.setRealm(realmRoles); + } + return normalizedRoles; + } + + public List normalizeRealmRoles(List exportedRoles, List baselineRoles) { + return normalizeRoleList(exportedRoles, baselineRoles, null); + } + + public Map> normalizeClientRoles(Map> exportedRoles, + Map> baselineRoles) { + var exportedOrEmpty = getNonNull(exportedRoles); + var baselineOrEmpty = getNonNull(baselineRoles); + + var normalizedRoles = new HashMap>(); + for (var entry : baselineOrEmpty.entrySet()) { + var clientId = entry.getKey(); + var baselineClientRoles = entry.getValue(); + var exportedClientRoles = exportedOrEmpty.remove(clientId); + exportedClientRoles = getNonNull(exportedClientRoles); + + var normalizedClientRoles = normalizeRoleList(exportedClientRoles, baselineClientRoles, clientId); + if (!normalizedClientRoles.isEmpty()) { + normalizedRoles.put(clientId, normalizedClientRoles); + } + } + + for (var entry : exportedOrEmpty.entrySet()) { + var clientId = entry.getKey(); + var roles = entry.getValue(); + + if (!roles.isEmpty()) { + normalizedRoles.put(clientId, normalizeList(roles)); + } + } + return normalizedRoles; + } + + public List normalizeRoleList(List exportedRoles, + List baselineRoles, String clientId) { + var exportedOrEmpty = getNonNull(exportedRoles); + var baselineOrEmpty = getNonNull(baselineRoles); + + var exportedMap = exportedOrEmpty.stream() + .collect(Collectors.toMap(RoleRepresentation::getName, Function.identity())); + var baselineMap = baselineOrEmpty.stream() + .collect(Collectors.toMap(RoleRepresentation::getName, Function.identity())); + var normalizedRoles = new ArrayList(); + for (var entry : baselineMap.entrySet()) { + var roleName = entry.getKey(); + var exportedRole = exportedMap.remove(roleName); + if (exportedRole == null) { + if (clientId == null) { + logger.warn("Default realm role '{}' was deleted in exported realm. It may be reintroduced during import!", roleName); + } else { + logger.warn("Default realm client-role '{}' for client '{}' was deleted in the exported realm. " + + "It may be reintroduced during import!", roleName, clientId); + } + continue; + } + + var baselineRole = entry.getValue(); + + var diff = unOrderedJavers.compare(baselineRole, exportedRole); + + if (diff.hasChanges() + || compositesChanged(exportedRole.getComposites(), baselineRole.getComposites()) + || attributeNormalizationService.listAttributesChanged(exportedRole.getAttributes(), baselineRole.getAttributes())) { + normalizedRoles.add(exportedRole); + } + } + normalizedRoles.addAll(exportedMap.values()); + return normalizeList(normalizedRoles); + } + + public List normalizeList(List roles) { + for (var role : roles) { + role.setId(null); + if (role.getAttributes() != null && role.getAttributes().isEmpty()) { + role.setAttributes(null); + } + } + return roles; + } + + public boolean compositesChanged(RoleRepresentation.Composites exportedComposites, RoleRepresentation.Composites baselineComposites) { + return unOrderedJavers.compare(baselineComposites, exportedComposites) + .hasChanges(); + } +} diff --git a/src/main/java/de/adorsys/keycloak/config/service/normalize/ScopeMappingNormalizationService.java b/src/main/java/de/adorsys/keycloak/config/service/normalize/ScopeMappingNormalizationService.java new file mode 100644 index 000000000..f713a2351 --- /dev/null +++ b/src/main/java/de/adorsys/keycloak/config/service/normalize/ScopeMappingNormalizationService.java @@ -0,0 +1,117 @@ +/*- + * ---license-start + * keycloak-config-cli + * --- + * Copyright (C) 2017 - 2022 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.normalize; + +import org.javers.core.Javers; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.representations.idm.ScopeMappingRepresentation; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static de.adorsys.keycloak.config.service.normalize.RealmNormalizationService.getNonNull; + +@Service +@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "NORMALIZE") +public class ScopeMappingNormalizationService { + + private static final Logger logger = LoggerFactory.getLogger(ScopeMappingNormalizationService.class); + + private final Javers javers; + + public ScopeMappingNormalizationService(Javers javers) { + this.javers = javers; + } + + public List normalizeScopeMappings(RealmRepresentation exportedRealm, RealmRepresentation baselineRealm) { + /* + * TODO: are the mappings in scopeMappings always clientScope/role? If not, this breaks + */ + // First handle the "default" scopeMappings present in the + var exportedMappingsMap = new HashMap(); + for (var exportedMapping : exportedRealm.getScopeMappings()) { + exportedMappingsMap.put(exportedMapping.getClientScope(), exportedMapping); + } + + var baselineMappingsMap = new HashMap(); + + var mappings = new ArrayList(); + for (var baselineRealmMapping : baselineRealm.getScopeMappings()) { + var clientScope = baselineRealmMapping.getClientScope(); + baselineMappingsMap.put(clientScope, baselineRealmMapping); + var exportedMapping = exportedMappingsMap.get(clientScope); + if (exportedMapping == null) { + logger.warn("Default realm scopeMapping '{}' was deleted in exported realm. It may be reintroduced during import!", clientScope); + continue; + } + // If the exported scopeMapping is different from the one that is present in the baseline realm, export it in the yml + if (scopeMappingChanged(exportedMapping, baselineRealmMapping)) { + mappings.add(exportedMapping); + } + } + + for (Map.Entry e : exportedMappingsMap.entrySet()) { + var clientScope = e.getKey(); + if (!baselineMappingsMap.containsKey(clientScope)) { + mappings.add(e.getValue()); + } + } + return mappings; + } + + public Map> normalizeClientScopeMappings(RealmRepresentation exportedRealm, + RealmRepresentation baselineRealm) { + var baselineOrEmpty = getNonNull(baselineRealm.getClientScopeMappings()); + var exportedOrEmpty = getNonNull(exportedRealm.getClientScopeMappings()); + + var mappings = new HashMap>(); + for (var e : baselineOrEmpty.entrySet()) { + var key = e.getKey(); + if (!exportedOrEmpty.containsKey(key)) { + logger.warn("Default realm clientScopeMapping '{}' was deleted in exported realm. It may be reintroduced during import!", key); + continue; + } + var scopeMappings = exportedOrEmpty.get(key); + if (javers.compareCollections(e.getValue(), scopeMappings, ScopeMappingRepresentation.class).hasChanges()) { + mappings.put(key, scopeMappings); + } + } + + for (var e : exportedOrEmpty.entrySet()) { + var key = e.getKey(); + if (!baselineOrEmpty.containsKey(key)) { + mappings.put(key, e.getValue()); + } + } + return mappings; + } + + public boolean scopeMappingChanged(ScopeMappingRepresentation exportedMapping, ScopeMappingRepresentation baselineRealmMapping) { + return javers.compare(baselineRealmMapping, exportedMapping).hasChanges(); + } + +} diff --git a/src/main/java/de/adorsys/keycloak/config/service/normalize/UserFederationNormalizationService.java b/src/main/java/de/adorsys/keycloak/config/service/normalize/UserFederationNormalizationService.java new file mode 100644 index 000000000..2821b7b39 --- /dev/null +++ b/src/main/java/de/adorsys/keycloak/config/service/normalize/UserFederationNormalizationService.java @@ -0,0 +1,154 @@ +/*- + * ---license-start + * keycloak-config-cli + * --- + * Copyright (C) 2017 - 2023 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.normalize; + +import org.javers.core.Javers; +import org.keycloak.representations.idm.UserFederationMapperRepresentation; +import org.keycloak.representations.idm.UserFederationProviderRepresentation; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static de.adorsys.keycloak.config.service.normalize.RealmNormalizationService.getNonNull; + +@Service +@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "NORMALIZE") +public class UserFederationNormalizationService { + + private static final Logger logger = LoggerFactory.getLogger(UserFederationNormalizationService.class); + + private final Javers unOrderedJavers; + + @Autowired + public UserFederationNormalizationService(Javers unOrderedJavers) { + this.unOrderedJavers = unOrderedJavers; + } + + public List normalizeProviders(List exportedProviders, + List baselineProviders) { + var exportedOrEmpty = getNonNull(exportedProviders); + var baselineOrEmpty = getNonNull(baselineProviders); + + var exportedMap = exportedOrEmpty.stream() + .collect(Collectors.toMap(UserFederationProviderRepresentation::getDisplayName, Function.identity())); + var baselineMap = baselineOrEmpty.stream() + .collect(Collectors.toMap(UserFederationProviderRepresentation::getDisplayName, Function.identity())); + + var normalizedProviders = new ArrayList(); + for (var entry : baselineMap.entrySet()) { + var displayName = entry.getKey(); + var exportedProvider = exportedMap.remove(displayName); + + if (exportedProvider == null) { + logger.warn("Default realm UserFederationProvider '{}' was deleted in exported realm. " + + "It may be reintroduced during import!", displayName); + continue; + } + + var baselineProvider = entry.getValue(); + if (unOrderedJavers.compare(baselineProvider, exportedProvider).hasChanges()) { + normalizedProviders.add(exportedProvider); + } + } + normalizedProviders.addAll(exportedMap.values()); + for (var provider : normalizedProviders) { + provider.setId(null); + if (provider.getConfig() != null && provider.getConfig().isEmpty()) { + provider.setConfig(null); + } + } + return normalizedProviders.isEmpty() ? null : normalizedProviders; + } + + public List normalizeMappers(List exportedMappers, + List baselineMappers) { + var exportedOrEmpty = getNonNull(exportedMappers); + var baselineOrEmpty = getNonNull(baselineMappers); + + var exportedMap = exportedOrEmpty.stream() + .collect(Collectors.toMap(m -> new MapperKey(m.getName(), m.getFederationProviderDisplayName()), Function.identity())); + var baselineMap = baselineOrEmpty.stream() + .collect(Collectors.toMap(m -> new MapperKey(m.getName(), m.getFederationProviderDisplayName()), Function.identity())); + + var normalizedMappers = new ArrayList(); + for (var entry : baselineMap.entrySet()) { + var key = entry.getKey(); + var exportedMapper = exportedMap.remove(key); + if (exportedMapper == null) { + logger.warn("Default realm UserFederationMapper '{}' for federation '{}' was deleted in exported realm. " + + "It may be reintroduced during import!", key.getName(), key.getFederationDisplayName()); + } + + var baselineMapper = entry.getValue(); + if (unOrderedJavers.compare(baselineMapper, exportedMapper).hasChanges()) { + normalizedMappers.add(exportedMapper); + } + } + normalizedMappers.addAll(exportedMap.values()); + for (var mapper : normalizedMappers) { + mapper.setId(null); + if (mapper.getConfig() != null && mapper.getConfig().isEmpty()) { + mapper.setConfig(null); + } + } + return normalizedMappers.isEmpty() ? null : normalizedMappers; + } + + private static class MapperKey { + private final String name; + private final String federationDisplayName; + + public MapperKey(String name, String federationDisplayName) { + this.name = name; + this.federationDisplayName = federationDisplayName; + } + + public String getName() { + return name; + } + + public String getFederationDisplayName() { + return federationDisplayName; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + UserFederationNormalizationService.MapperKey mapperKey = (UserFederationNormalizationService.MapperKey) o; + return Objects.equals(name, mapperKey.name) && Objects.equals(federationDisplayName, mapperKey.federationDisplayName); + } + + @Override + public int hashCode() { + return Objects.hash(name, federationDisplayName); + } + } + +} diff --git a/src/main/java/de/adorsys/keycloak/config/service/rolecomposites/client/ClientCompositeImport.java b/src/main/java/de/adorsys/keycloak/config/service/rolecomposites/client/ClientCompositeImport.java index 977b902b0..28d7ac2dd 100644 --- a/src/main/java/de/adorsys/keycloak/config/service/rolecomposites/client/ClientCompositeImport.java +++ b/src/main/java/de/adorsys/keycloak/config/service/rolecomposites/client/ClientCompositeImport.java @@ -26,12 +26,14 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; import java.util.*; import java.util.stream.Collectors; @Service("clientRoleClientCompositeImport") +@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "IMPORT", matchIfMissing = true) public class ClientCompositeImport { private static final Logger logger = LoggerFactory.getLogger(ClientCompositeImport.class); diff --git a/src/main/java/de/adorsys/keycloak/config/service/rolecomposites/client/ClientRoleCompositeImportService.java b/src/main/java/de/adorsys/keycloak/config/service/rolecomposites/client/ClientRoleCompositeImportService.java index 2130abc0c..05b56fa43 100644 --- a/src/main/java/de/adorsys/keycloak/config/service/rolecomposites/client/ClientRoleCompositeImportService.java +++ b/src/main/java/de/adorsys/keycloak/config/service/rolecomposites/client/ClientRoleCompositeImportService.java @@ -22,6 +22,7 @@ import org.keycloak.representations.idm.RoleRepresentation; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; import java.util.List; @@ -32,6 +33,7 @@ * Implements the update mechanism for role composites of client-level roles */ @Service +@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "IMPORT", matchIfMissing = true) public class ClientRoleCompositeImportService { private final RealmCompositeImport realmCompositeImport; diff --git a/src/main/java/de/adorsys/keycloak/config/service/rolecomposites/client/RealmCompositeImport.java b/src/main/java/de/adorsys/keycloak/config/service/rolecomposites/client/RealmCompositeImport.java index 1fe734179..126fd06eb 100644 --- a/src/main/java/de/adorsys/keycloak/config/service/rolecomposites/client/RealmCompositeImport.java +++ b/src/main/java/de/adorsys/keycloak/config/service/rolecomposites/client/RealmCompositeImport.java @@ -25,6 +25,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; import java.util.Objects; @@ -32,6 +33,7 @@ import java.util.stream.Collectors; @Service("clientRoleRealmCompositeImport") +@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "IMPORT", matchIfMissing = true) public class RealmCompositeImport { private static final Logger logger = LoggerFactory.getLogger(RealmCompositeImport.class); diff --git a/src/main/java/de/adorsys/keycloak/config/service/rolecomposites/realm/ClientCompositeImport.java b/src/main/java/de/adorsys/keycloak/config/service/rolecomposites/realm/ClientCompositeImport.java index 71dc650b0..a2564ed3a 100644 --- a/src/main/java/de/adorsys/keycloak/config/service/rolecomposites/realm/ClientCompositeImport.java +++ b/src/main/java/de/adorsys/keycloak/config/service/rolecomposites/realm/ClientCompositeImport.java @@ -25,12 +25,14 @@ import org.keycloak.representations.idm.RoleRepresentation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; import java.util.*; import java.util.stream.Collectors; @Service("realmRoleClientCompositeImport") +@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "IMPORT", matchIfMissing = true) public class ClientCompositeImport { private static final Logger logger = LoggerFactory.getLogger(ClientCompositeImport.class); diff --git a/src/main/java/de/adorsys/keycloak/config/service/rolecomposites/realm/RealmCompositeImport.java b/src/main/java/de/adorsys/keycloak/config/service/rolecomposites/realm/RealmCompositeImport.java index e7eb4f7ac..1cb9aa31d 100644 --- a/src/main/java/de/adorsys/keycloak/config/service/rolecomposites/realm/RealmCompositeImport.java +++ b/src/main/java/de/adorsys/keycloak/config/service/rolecomposites/realm/RealmCompositeImport.java @@ -25,6 +25,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; import java.util.Objects; @@ -32,6 +33,7 @@ import java.util.stream.Collectors; @Service("realmRoleRealmCompositeImport") +@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "IMPORT", matchIfMissing = true) public class RealmCompositeImport { private static final Logger logger = LoggerFactory.getLogger(RealmCompositeImport.class); diff --git a/src/main/java/de/adorsys/keycloak/config/service/rolecomposites/realm/RealmRoleCompositeImportService.java b/src/main/java/de/adorsys/keycloak/config/service/rolecomposites/realm/RealmRoleCompositeImportService.java index 5292af18c..b3d484e86 100644 --- a/src/main/java/de/adorsys/keycloak/config/service/rolecomposites/realm/RealmRoleCompositeImportService.java +++ b/src/main/java/de/adorsys/keycloak/config/service/rolecomposites/realm/RealmRoleCompositeImportService.java @@ -22,6 +22,7 @@ import org.keycloak.representations.idm.RoleRepresentation; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; import java.util.List; @@ -31,6 +32,7 @@ * Implements the update mechanism for role composites of realm-level roles */ @Service +@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "IMPORT", matchIfMissing = true) public class RealmRoleCompositeImportService { private final RealmCompositeImport realmCompositeImport; diff --git a/src/main/java/de/adorsys/keycloak/config/service/state/StateService.java b/src/main/java/de/adorsys/keycloak/config/service/state/StateService.java index 6582e4adc..771b614dc 100644 --- a/src/main/java/de/adorsys/keycloak/config/service/state/StateService.java +++ b/src/main/java/de/adorsys/keycloak/config/service/state/StateService.java @@ -29,6 +29,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; import java.util.ArrayList; @@ -37,6 +38,7 @@ import java.util.stream.Collectors; @Service +@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "IMPORT", matchIfMissing = true) public class StateService { private static final Logger logger = LoggerFactory.getLogger(StateService.class); diff --git a/src/main/java/de/adorsys/keycloak/config/util/JaversUtil.java b/src/main/java/de/adorsys/keycloak/config/util/JaversUtil.java new file mode 100644 index 000000000..6ec32867c --- /dev/null +++ b/src/main/java/de/adorsys/keycloak/config/util/JaversUtil.java @@ -0,0 +1,42 @@ +/*- + * ---license-start + * keycloak-config-cli + * --- + * Copyright (C) 2017 - 2022 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.util; + +import de.adorsys.keycloak.config.exception.NormalizationException; +import org.javers.core.diff.changetype.PropertyChange; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Component; + +@Component +@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "NORMALIZE") +public class JaversUtil { + + public void applyChange(Object object, PropertyChange change) { + try { + var field = object.getClass().getDeclaredField(change.getPropertyName()); + field.setAccessible(true); + field.set(object, change.getRight()); + } catch (NoSuchFieldException | IllegalAccessException ex) { + throw new NormalizationException(String.format("Failed to set property %s on object of type %s", + change.getPropertyName(), object.getClass().getName()), ex); + } + } +} diff --git a/src/main/resources/application-normalize-dev.properties b/src/main/resources/application-normalize-dev.properties new file mode 100644 index 000000000..ada43bd0e --- /dev/null +++ b/src/main/resources/application-normalize-dev.properties @@ -0,0 +1,6 @@ +spring.output.ansi.enabled=ALWAYS +spring.config.import=classpath:application-debug.properties + +run.operation=NORMALIZE +normalization.files.input-locations=./exports/in/*.json +normalization.files.output-directory=./exports/out diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 8378d75aa..57a0c6fb3 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -6,47 +6,7 @@ spring.main.lazy-initialization=true spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.availability.ApplicationAvailabilityAutoConfiguration keycloak.version=@keycloak.version@ -keycloak.login-realm=master -keycloak.user=admin -keycloak.client-secret= -keycloak.grant-type=password -keycloak.client-id=admin-cli -keycloak.ssl-verify=true -keycloak.connect-timeout=10s -keycloak.read-timeout=10s -keycloak.availability-check.enabled=false -keycloak.availability-check.timeout=120s -keycloak.availability-check.retry-delay=2s -import.validate=true -import.parallel=false -import.files.excludes="" -import.files.include-hidden-files=false -import.cache.enabled=true -import.cache.key=default -import.var-substitution.enabled=false -import.var-substitution.nested=true -import.var-substitution.undefined-is-error=true -import.var-substitution.prefix=$( -import.var-substitution.suffix=) -import.remote-state.enabled=true -# For security reasons, change this value if you want to encrypt the state -import.remote-state.encryption-salt=2B521C795FBE2F2425DB150CD3700BA9 -import.behaviors.remove-default-role-from-user=false -import.behaviors.skip-attributes-for-federated-user=false -import.behaviors.sync-user-federation=false -import.managed.authentication-flow=full -import.managed.group=full -import.managed.required-action=full -import.managed.client-scope=full -import.managed.scope-mapping=full -import.managed.client-scope-mapping=full -import.managed.component=full -import.managed.sub-component=full -import.managed.identity-provider=full -import.managed.identity-provider-mapper=full -import.managed.role=full -import.managed.client=full -import.managed.client-authorization-resources=full + logging.group.http=org.apache.http.wire logging.group.realm-config=de.adorsys.keycloak.config.provider.KeycloakImportProvider logging.group.keycloak-config-cli=de.adorsys.keycloak.config.service,de.adorsys.keycloak.config.KeycloakConfigRunner,de.adorsys.keycloak.config.provider.KeycloakProvider diff --git a/src/main/resources/baseline/19.0.3/client/client.json b/src/main/resources/baseline/19.0.3/client/client.json new file mode 100644 index 000000000..858b1115b --- /dev/null +++ b/src/main/resources/baseline/19.0.3/client/client.json @@ -0,0 +1,45 @@ +{ + "id": "caa6606a-6056-475c-af05-8b0365bb8164", + "clientId": "reference-client", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "q0t682YLCCk2qd5dntjtcniGozLXIZ7h", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "client.secret.creation.time": "1667920370" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": -1, + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ], + "access": { + "view": true, + "configure": true, + "manage": true + } +} diff --git a/src/main/resources/baseline/19.0.3/realm/realm.json b/src/main/resources/baseline/19.0.3/realm/realm.json new file mode 100644 index 000000000..bf14005cb --- /dev/null +++ b/src/main/resources/baseline/19.0.3/realm/realm.json @@ -0,0 +1,2176 @@ +{ + "id": "REALM_NAME_PLACEHOLDER", + "realm": "REALM_NAME_PLACEHOLDER", + "notBefore": 0, + "defaultSignatureAlgorithm": "RS256", + "revokeRefreshToken": false, + "refreshTokenMaxReuse": 0, + "accessTokenLifespan": 300, + "accessTokenLifespanForImplicitFlow": 900, + "ssoSessionIdleTimeout": 1800, + "ssoSessionMaxLifespan": 36000, + "ssoSessionIdleTimeoutRememberMe": 0, + "ssoSessionMaxLifespanRememberMe": 0, + "offlineSessionIdleTimeout": 2592000, + "offlineSessionMaxLifespanEnabled": false, + "offlineSessionMaxLifespan": 5184000, + "clientSessionIdleTimeout": 0, + "clientSessionMaxLifespan": 0, + "clientOfflineSessionIdleTimeout": 0, + "clientOfflineSessionMaxLifespan": 0, + "accessCodeLifespan": 60, + "accessCodeLifespanUserAction": 300, + "accessCodeLifespanLogin": 1800, + "actionTokenGeneratedByAdminLifespan": 43200, + "actionTokenGeneratedByUserLifespan": 300, + "oauth2DeviceCodeLifespan": 600, + "oauth2DevicePollingInterval": 5, + "enabled": true, + "sslRequired": "external", + "registrationAllowed": false, + "registrationEmailAsUsername": false, + "rememberMe": false, + "verifyEmail": false, + "loginWithEmailAllowed": true, + "duplicateEmailsAllowed": false, + "resetPasswordAllowed": false, + "editUsernameAllowed": false, + "bruteForceProtected": false, + "permanentLockout": false, + "maxFailureWaitSeconds": 900, + "minimumQuickLoginWaitSeconds": 60, + "waitIncrementSeconds": 60, + "quickLoginCheckMilliSeconds": 1000, + "maxDeltaTimeSeconds": 43200, + "failureFactor": 30, + "roles": { + "realm": [ + { + "id": "0825e37b-b5bb-413d-9c1c-23457f17cb66", + "name": "default-roles-REALM_NAME_PLACEHOLDER", + "description": "${role_default-roles}", + "composite": true, + "composites": { + "realm": [ + "offline_access", + "uma_authorization" + ], + "client": { + "account": [ + "manage-account", + "view-profile" + ] + } + }, + "clientRole": false, + "containerId": "REALM_NAME_PLACEHOLDER", + "attributes": {} + }, + { + "id": "08b30ae4-52ba-45a9-b723-07e6ac57ae2f", + "name": "offline_access", + "description": "${role_offline-access}", + "composite": false, + "clientRole": false, + "containerId": "REALM_NAME_PLACEHOLDER", + "attributes": {} + }, + { + "id": "372ddfc2-cd6e-4ca3-a30f-d39cb92ac12d", + "name": "uma_authorization", + "description": "${role_uma_authorization}", + "composite": false, + "clientRole": false, + "containerId": "REALM_NAME_PLACEHOLDER", + "attributes": {} + } + ], + "client": { + "realm-management": [ + { + "id": "caf6ae68-6a16-4dfa-9a71-1149ceb8f79c", + "name": "manage-identity-providers", + "description": "${role_manage-identity-providers}", + "composite": false, + "clientRole": true, + "containerId": "631f9fd2-7539-4190-8983-0e94614c5b73", + "attributes": {} + }, + { + "id": "913aeddb-8be9-4060-86c7-30cf0fa892ee", + "name": "manage-users", + "description": "${role_manage-users}", + "composite": false, + "clientRole": true, + "containerId": "631f9fd2-7539-4190-8983-0e94614c5b73", + "attributes": {} + }, + { + "id": "15ee649c-ef5f-4fe8-a6c1-67a9db14b0c5", + "name": "view-clients", + "description": "${role_view-clients}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "query-clients" + ] + } + }, + "clientRole": true, + "containerId": "631f9fd2-7539-4190-8983-0e94614c5b73", + "attributes": {} + }, + { + "id": "5d56f881-d3ff-49ad-929c-214afd641f8f", + "name": "query-clients", + "description": "${role_query-clients}", + "composite": false, + "clientRole": true, + "containerId": "631f9fd2-7539-4190-8983-0e94614c5b73", + "attributes": {} + }, + { + "id": "0ba58546-8487-4508-b81f-0735432accf2", + "name": "view-events", + "description": "${role_view-events}", + "composite": false, + "clientRole": true, + "containerId": "631f9fd2-7539-4190-8983-0e94614c5b73", + "attributes": {} + }, + { + "id": "f6f1b3c0-5d29-41b4-b2ec-cfab60e28a3b", + "name": "query-users", + "description": "${role_query-users}", + "composite": false, + "clientRole": true, + "containerId": "631f9fd2-7539-4190-8983-0e94614c5b73", + "attributes": {} + }, + { + "id": "2c6ddc11-206e-4e82-b894-9fca7cc85866", + "name": "view-realm", + "description": "${role_view-realm}", + "composite": false, + "clientRole": true, + "containerId": "631f9fd2-7539-4190-8983-0e94614c5b73", + "attributes": {} + }, + { + "id": "f9917916-7aca-413b-b7bc-452d3fca7a48", + "name": "manage-events", + "description": "${role_manage-events}", + "composite": false, + "clientRole": true, + "containerId": "631f9fd2-7539-4190-8983-0e94614c5b73", + "attributes": {} + }, + { + "id": "ad1d551b-3f03-445d-8ac0-51b1196413f0", + "name": "view-users", + "description": "${role_view-users}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "query-groups", + "query-users" + ] + } + }, + "clientRole": true, + "containerId": "631f9fd2-7539-4190-8983-0e94614c5b73", + "attributes": {} + }, + { + "id": "f645cca6-9d25-40f0-8ce9-d988a04d9bec", + "name": "create-client", + "description": "${role_create-client}", + "composite": false, + "clientRole": true, + "containerId": "631f9fd2-7539-4190-8983-0e94614c5b73", + "attributes": {} + }, + { + "id": "ce56402a-8925-4750-9269-6b035cfa334f", + "name": "impersonation", + "description": "${role_impersonation}", + "composite": false, + "clientRole": true, + "containerId": "631f9fd2-7539-4190-8983-0e94614c5b73", + "attributes": {} + }, + { + "id": "1e70f63f-0883-4452-bb1b-f179bf3c8c30", + "name": "manage-authorization", + "description": "${role_manage-authorization}", + "composite": false, + "clientRole": true, + "containerId": "631f9fd2-7539-4190-8983-0e94614c5b73", + "attributes": {} + }, + { + "id": "5cef1f90-8af8-4445-b6a3-05d6eb60c46c", + "name": "manage-realm", + "description": "${role_manage-realm}", + "composite": false, + "clientRole": true, + "containerId": "631f9fd2-7539-4190-8983-0e94614c5b73", + "attributes": {} + }, + { + "id": "e87e4aa8-8c15-44ba-9622-eec4eeae1997", + "name": "query-groups", + "description": "${role_query-groups}", + "composite": false, + "clientRole": true, + "containerId": "631f9fd2-7539-4190-8983-0e94614c5b73", + "attributes": {} + }, + { + "id": "baa31234-e4ed-4821-a696-6ccd686b5e1f", + "name": "view-identity-providers", + "description": "${role_view-identity-providers}", + "composite": false, + "clientRole": true, + "containerId": "631f9fd2-7539-4190-8983-0e94614c5b73", + "attributes": {} + }, + { + "id": "0d7127a0-cd7b-478c-9156-629fc321bca4", + "name": "query-realms", + "description": "${role_query-realms}", + "composite": false, + "clientRole": true, + "containerId": "631f9fd2-7539-4190-8983-0e94614c5b73", + "attributes": {} + }, + { + "id": "99d07b51-633c-4d25-9dc1-f779a219c246", + "name": "realm-admin", + "description": "${role_realm-admin}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "manage-users", + "manage-identity-providers", + "view-clients", + "query-clients", + "view-events", + "query-users", + "view-realm", + "manage-events", + "view-users", + "create-client", + "manage-authorization", + "impersonation", + "manage-realm", + "view-identity-providers", + "query-groups", + "query-realms", + "manage-clients", + "view-authorization" + ] + } + }, + "clientRole": true, + "containerId": "631f9fd2-7539-4190-8983-0e94614c5b73", + "attributes": {} + }, + { + "id": "118c289f-0b71-4edf-ac2a-7016cc0c674d", + "name": "manage-clients", + "description": "${role_manage-clients}", + "composite": false, + "clientRole": true, + "containerId": "631f9fd2-7539-4190-8983-0e94614c5b73", + "attributes": {} + }, + { + "id": "30f43104-6914-4e4f-9f6d-219daf2f7991", + "name": "view-authorization", + "description": "${role_view-authorization}", + "composite": false, + "clientRole": true, + "containerId": "631f9fd2-7539-4190-8983-0e94614c5b73", + "attributes": {} + } + ], + "security-admin-console": [], + "admin-cli": [], + "account-console": [], + "broker": [ + { + "id": "6045a69a-aec3-4a11-b4c3-3cae53f7a914", + "name": "read-token", + "description": "${role_read-token}", + "composite": false, + "clientRole": true, + "containerId": "6eed48a3-64bd-48ed-ac7c-607f8e724258", + "attributes": {} + } + ], + "account": [ + { + "id": "4e27eb76-06ea-4547-ab5d-de8f9c745597", + "name": "manage-consent", + "description": "${role_manage-consent}", + "composite": true, + "composites": { + "client": { + "account": [ + "view-consent" + ] + } + }, + "clientRole": true, + "containerId": "6c7757f2-9ed7-4609-8e41-f4316ca8d31e", + "attributes": {} + }, + { + "id": "60ae83a6-6ed1-4b4f-81cc-701b9a95ceb9", + "name": "delete-account", + "description": "${role_delete-account}", + "composite": false, + "clientRole": true, + "containerId": "6c7757f2-9ed7-4609-8e41-f4316ca8d31e", + "attributes": {} + }, + { + "id": "f5f244ba-53de-45d7-9774-d4c915cd4ccb", + "name": "manage-account", + "description": "${role_manage-account}", + "composite": true, + "composites": { + "client": { + "account": [ + "manage-account-links" + ] + } + }, + "clientRole": true, + "containerId": "6c7757f2-9ed7-4609-8e41-f4316ca8d31e", + "attributes": {} + }, + { + "id": "2abb2ef2-3e4c-43d5-9ab7-b096e19f3a56", + "name": "view-consent", + "description": "${role_view-consent}", + "composite": false, + "clientRole": true, + "containerId": "6c7757f2-9ed7-4609-8e41-f4316ca8d31e", + "attributes": {} + }, + { + "id": "8eb01f07-3771-4b28-ab78-6f216079b508", + "name": "view-profile", + "description": "${role_view-profile}", + "composite": false, + "clientRole": true, + "containerId": "6c7757f2-9ed7-4609-8e41-f4316ca8d31e", + "attributes": {} + }, + { + "id": "23954dae-8150-4183-8733-bce1349fb0ec", + "name": "manage-account-links", + "description": "${role_manage-account-links}", + "composite": false, + "clientRole": true, + "containerId": "6c7757f2-9ed7-4609-8e41-f4316ca8d31e", + "attributes": {} + }, + { + "id": "734d080d-1f67-48e9-942f-9233f6eca901", + "name": "view-applications", + "description": "${role_view-applications}", + "composite": false, + "clientRole": true, + "containerId": "6c7757f2-9ed7-4609-8e41-f4316ca8d31e", + "attributes": {} + } + ] + } + }, + "groups": [], + "defaultRole": { + "id": "0825e37b-b5bb-413d-9c1c-23457f17cb66", + "name": "default-roles-REALM_NAME_PLACEHOLDER", + "description": "${role_default-roles}", + "composite": true, + "clientRole": false, + "containerId": "REALM_NAME_PLACEHOLDER" + }, + "requiredCredentials": [ + "password" + ], + "otpPolicyType": "totp", + "otpPolicyAlgorithm": "HmacSHA1", + "otpPolicyInitialCounter": 0, + "otpPolicyDigits": 6, + "otpPolicyLookAheadWindow": 1, + "otpPolicyPeriod": 30, + "otpSupportedApplications": [ + "FreeOTP", + "Google Authenticator" + ], + "webAuthnPolicyRpEntityName": "keycloak", + "webAuthnPolicySignatureAlgorithms": [ + "ES256" + ], + "webAuthnPolicyRpId": "", + "webAuthnPolicyAttestationConveyancePreference": "not specified", + "webAuthnPolicyAuthenticatorAttachment": "not specified", + "webAuthnPolicyRequireResidentKey": "not specified", + "webAuthnPolicyUserVerificationRequirement": "not specified", + "webAuthnPolicyCreateTimeout": 0, + "webAuthnPolicyAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyAcceptableAaguids": [], + "webAuthnPolicyPasswordlessRpEntityName": "keycloak", + "webAuthnPolicyPasswordlessSignatureAlgorithms": [ + "ES256" + ], + "webAuthnPolicyPasswordlessRpId": "", + "webAuthnPolicyPasswordlessAttestationConveyancePreference": "not specified", + "webAuthnPolicyPasswordlessAuthenticatorAttachment": "not specified", + "webAuthnPolicyPasswordlessRequireResidentKey": "not specified", + "webAuthnPolicyPasswordlessUserVerificationRequirement": "not specified", + "webAuthnPolicyPasswordlessCreateTimeout": 0, + "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyPasswordlessAcceptableAaguids": [], + "scopeMappings": [ + { + "clientScope": "offline_access", + "roles": [ + "offline_access" + ] + } + ], + "clientScopeMappings": { + "account": [ + { + "client": "account-console", + "roles": [ + "manage-account" + ] + } + ] + }, + "clients": [ + { + "id": "6c7757f2-9ed7-4609-8e41-f4316ca8d31e", + "clientId": "account", + "name": "${client_account}", + "rootUrl": "${authBaseUrl}", + "baseUrl": "/realms/REALM_NAME_PLACEHOLDER/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/realms/REALM_NAME_PLACEHOLDER/account/*" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "fe6ddaaf-3a90-4942-ae6a-1f170c87fc3b", + "clientId": "account-console", + "name": "${client_account-console}", + "rootUrl": "${authBaseUrl}", + "baseUrl": "/realms/REALM_NAME_PLACEHOLDER/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/realms/REALM_NAME_PLACEHOLDER/account/*" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+", + "pkce.code.challenge.method": "S256" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "9f385a11-7b42-4f92-ac9b-7d286590a392", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": {} + } + ], + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "61ecc83e-d3f3-4b4f-a821-845094b3d9d4", + "clientId": "admin-cli", + "name": "${client_admin-cli}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": false, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "6eed48a3-64bd-48ed-ac7c-607f8e724258", + "clientId": "broker", + "name": "${client_broker}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "631f9fd2-7539-4190-8983-0e94614c5b73", + "clientId": "realm-management", + "name": "${client_realm-management}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "05f3367b-492b-40e1-ba0c-f000aa3ad0ef", + "clientId": "security-admin-console", + "name": "${client_security-admin-console}", + "rootUrl": "${authAdminUrl}", + "baseUrl": "/admin/REALM_NAME_PLACEHOLDER/console/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/admin/REALM_NAME_PLACEHOLDER/console/*" + ], + "webOrigins": [ + "+" + ], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+", + "pkce.code.challenge.method": "S256" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "9d1f5814-fa63-4c36-ae10-747d30f47c69", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" + } + } + ], + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + } + ], + "clientScopes": [ + { + "id": "23fc5623-9366-478f-9924-801d71f32489", + "name": "role_list", + "description": "SAML role list", + "protocol": "saml", + "attributes": { + "consent.screen.text": "${samlRoleListScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "31541a2f-1c88-43d9-96fe-9f9efbd096d4", + "name": "role list", + "protocol": "saml", + "protocolMapper": "saml-role-list-mapper", + "consentRequired": false, + "config": { + "single": "false", + "attribute.nameformat": "Basic", + "attribute.name": "Role" + } + } + ] + }, + { + "id": "1859a4f3-d051-4800-963f-45b624cccd57", + "name": "web-origins", + "description": "OpenID Connect scope for add allowed web origins to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "false", + "consent.screen.text": "" + }, + "protocolMappers": [ + { + "id": "a7777bff-a046-4fe3-a5a9-a520d79865ec", + "name": "allowed web origins", + "protocol": "openid-connect", + "protocolMapper": "oidc-allowed-origins-mapper", + "consentRequired": false, + "config": {} + } + ] + }, + { + "id": "7d011629-d7f1-45ad-a09b-b4decb3d47ed", + "name": "acr", + "description": "OpenID Connect scope for add acr (authentication context class reference) to the token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "22a74f8c-4493-4a1a-bf97-f51466e336b3", + "name": "acr loa level", + "protocol": "openid-connect", + "protocolMapper": "oidc-acr-mapper", + "consentRequired": false, + "config": { + "id.token.claim": "true", + "access.token.claim": "true" + } + } + ] + }, + { + "id": "7fb8529d-cb6f-4f31-b4c3-f6be6f74b47d", + "name": "phone", + "description": "OpenID Connect built-in scope: phone", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${phoneScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "5bd8bfba-d8a3-4792-aaec-5f0513397193", + "name": "phone number verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "phoneNumberVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number_verified", + "jsonType.label": "boolean" + } + }, + { + "id": "89146d36-28d5-4750-933e-75014b203dcf", + "name": "phone number", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "phoneNumber", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "01025c61-c655-463d-967d-a45e88368472", + "name": "roles", + "description": "OpenID Connect scope for add user roles to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "true", + "consent.screen.text": "${rolesScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "ad80bcc7-66d4-44c0-b34e-65822a57359a", + "name": "realm roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "user.attribute": "foo", + "access.token.claim": "true", + "claim.name": "realm_access.roles", + "jsonType.label": "String", + "multivalued": "true" + } + }, + { + "id": "73346462-e569-4679-a57d-fe623bdc5a95", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": {} + }, + { + "id": "3842db0a-837f-4564-9e48-07c87f5d3258", + "name": "client roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-client-role-mapper", + "consentRequired": false, + "config": { + "user.attribute": "foo", + "access.token.claim": "true", + "claim.name": "resource_access.${client_id}.roles", + "jsonType.label": "String", + "multivalued": "true" + } + } + ] + }, + { + "id": "f79aab58-4d4a-40ee-b879-a586ff956f12", + "name": "microprofile-jwt", + "description": "Microprofile - JWT built-in scope", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "ccb6bedc-b921-4c83-abe6-13de8c0e9795", + "name": "groups", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "multivalued": "true", + "user.attribute": "foo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "groups", + "jsonType.label": "String" + } + }, + { + "id": "19ae8192-0189-4644-a362-d08a0bce5680", + "name": "upn", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "upn", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "a0d6d0c2-afd3-4b29-a99f-ff3002866519", + "name": "offline_access", + "description": "OpenID Connect built-in scope: offline_access", + "protocol": "openid-connect", + "attributes": { + "consent.screen.text": "${offlineAccessScopeConsentText}", + "display.on.consent.screen": "true" + } + }, + { + "id": "1dcbfe97-e4ab-4df7-8517-4eaaa26ea410", + "name": "profile", + "description": "OpenID Connect built-in scope: profile", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${profileScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "8448e164-b72e-406e-b14b-ae5d1e72393a", + "name": "gender", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "gender", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "gender", + "jsonType.label": "String" + } + }, + { + "id": "c573ae92-f94a-494e-9403-f875d22d3e8d", + "name": "profile", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "profile", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "profile", + "jsonType.label": "String" + } + }, + { + "id": "09f992cc-2d64-4d4d-88ca-59a1b63325e9", + "name": "website", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "website", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "website", + "jsonType.label": "String" + } + }, + { + "id": "04658d90-dc3d-4865-ac52-6f16570fcd76", + "name": "full name", + "protocol": "openid-connect", + "protocolMapper": "oidc-full-name-mapper", + "consentRequired": false, + "config": { + "id.token.claim": "true", + "access.token.claim": "true", + "userinfo.token.claim": "true" + } + }, + { + "id": "de199ca1-e720-419f-aeee-e112568f5cff", + "name": "family name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "lastName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "family_name", + "jsonType.label": "String" + } + }, + { + "id": "08daf912-8959-4ba5-9c38-ffcd395d6487", + "name": "given name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "firstName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "given_name", + "jsonType.label": "String" + } + }, + { + "id": "d76e56a2-6237-46d5-a56b-0ecd1f979e85", + "name": "username", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "preferred_username", + "jsonType.label": "String" + } + }, + { + "id": "6bc40007-ab16-48f6-ae81-a66a9062ad5c", + "name": "picture", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "picture", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "picture", + "jsonType.label": "String" + } + }, + { + "id": "0385dbe8-587c-49ab-a317-a15b5b52d456", + "name": "zoneinfo", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "zoneinfo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "zoneinfo", + "jsonType.label": "String" + } + }, + { + "id": "28946b3e-0eab-44a5-8eee-d42088072306", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" + } + }, + { + "id": "e61c48dc-d083-4029-92d1-3c7a6f3d8bb9", + "name": "birthdate", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "birthdate", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "birthdate", + "jsonType.label": "String" + } + }, + { + "id": "62545d7c-ef2e-48a6-bff8-e2e9a2b16c3d", + "name": "middle name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "middleName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "middle_name", + "jsonType.label": "String" + } + }, + { + "id": "f1c75501-26b2-4d94-82aa-851e4fa3dd7c", + "name": "nickname", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "nickname", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "nickname", + "jsonType.label": "String" + } + }, + { + "id": "046a0b0d-5256-48fe-9b8f-b5193d87ebdd", + "name": "updated at", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "updatedAt", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "updated_at", + "jsonType.label": "long" + } + } + ] + }, + { + "id": "d807e61e-3661-44ff-a8cd-285458e6f763", + "name": "address", + "description": "OpenID Connect built-in scope: address", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${addressScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "fede614b-c46e-484c-8dba-9590cd0205fe", + "name": "address", + "protocol": "openid-connect", + "protocolMapper": "oidc-address-mapper", + "consentRequired": false, + "config": { + "user.attribute.formatted": "formatted", + "user.attribute.country": "country", + "user.attribute.postal_code": "postal_code", + "userinfo.token.claim": "true", + "user.attribute.street": "street", + "id.token.claim": "true", + "user.attribute.region": "region", + "access.token.claim": "true", + "user.attribute.locality": "locality" + } + } + ] + }, + { + "id": "c8a4b7a9-c8ba-412b-a17d-e90a3c6393fd", + "name": "email", + "description": "OpenID Connect built-in scope: email", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${emailScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "de4bafae-e88c-4c2f-9001-311f5c141633", + "name": "email", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "email", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email", + "jsonType.label": "String" + } + }, + { + "id": "9aa11aac-4178-435b-83b3-2d85508e34bd", + "name": "email verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "emailVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email_verified", + "jsonType.label": "boolean" + } + } + ] + } + ], + "defaultDefaultClientScopes": [ + "role_list", + "profile", + "email", + "roles", + "web-origins", + "acr" + ], + "defaultOptionalClientScopes": [ + "offline_access", + "address", + "phone", + "microprofile-jwt" + ], + "browserSecurityHeaders": { + "contentSecurityPolicyReportOnly": "", + "xContentTypeOptions": "nosniff", + "xRobotsTag": "none", + "xFrameOptions": "SAMEORIGIN", + "contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", + "xXSSProtection": "1; mode=block", + "strictTransportSecurity": "max-age=31536000; includeSubDomains" + }, + "smtpServer": {}, + "eventsEnabled": false, + "eventsListeners": [ + "jboss-logging" + ], + "enabledEventTypes": [], + "adminEventsEnabled": false, + "adminEventsDetailsEnabled": false, + "identityProviders": [], + "identityProviderMappers": [], + "components": { + "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy": [ + { + "id": "808ef7c4-b5d1-491c-ade0-559f38287e68", + "name": "Full Scope Disabled", + "providerId": "scope", + "subType": "anonymous", + "subComponents": {}, + "config": {} + }, + { + "id": "2a0ad1a6-d6f9-43cb-8ec1-8913a23339d7", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "saml-role-list-mapper", + "oidc-full-name-mapper", + "oidc-usermodel-property-mapper", + "oidc-address-mapper", + "oidc-usermodel-attribute-mapper", + "saml-user-attribute-mapper", + "oidc-sha256-pairwise-sub-mapper", + "saml-user-property-mapper" + ] + } + }, + { + "id": "3695208c-32af-497b-9af9-5de878749899", + "name": "Consent Required", + "providerId": "consent-required", + "subType": "anonymous", + "subComponents": {}, + "config": {} + }, + { + "id": "b7317013-6157-4099-a1ee-194df4808b2d", + "name": "Allowed Client Scopes", + "providerId": "allowed-client-templates", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allow-default-scopes": [ + "true" + ] + } + }, + { + "id": "2464d485-980d-447a-94e2-34cf96aad1f1", + "name": "Allowed Client Scopes", + "providerId": "allowed-client-templates", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allow-default-scopes": [ + "true" + ] + } + }, + { + "id": "6460fb5a-bbb9-400b-aba7-85cbe8666341", + "name": "Trusted Hosts", + "providerId": "trusted-hosts", + "subType": "anonymous", + "subComponents": {}, + "config": { + "host-sending-registration-request-must-match": [ + "true" + ], + "client-uris-must-match": [ + "true" + ] + } + }, + { + "id": "c2093410-4c63-4436-9294-535c492912dc", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "oidc-usermodel-property-mapper", + "oidc-sha256-pairwise-sub-mapper", + "saml-role-list-mapper", + "saml-user-property-mapper", + "oidc-address-mapper", + "saml-user-attribute-mapper", + "oidc-usermodel-attribute-mapper", + "oidc-full-name-mapper" + ] + } + }, + { + "id": "f9ef3de5-1ad1-4c9c-92bd-62a070e13ba2", + "name": "Max Clients Limit", + "providerId": "max-clients", + "subType": "anonymous", + "subComponents": {}, + "config": { + "max-clients": [ + "200" + ] + } + } + ], + "org.keycloak.keys.KeyProvider": [ + { + "id": "d75363d0-aed5-4fea-a97f-d0c1adb4fa63", + "name": "hmac-generated", + "providerId": "hmac-generated", + "subComponents": {}, + "config": { + "kid": [ + "ea50f128-6340-4f3e-8050-1e79ba559121" + ], + "secret": [ + "QYJipEnEXtsqk2OqtX1oyyQrANj03UWaGGf1p4yD28-x4MMOSxXVpEVTwZZvpgJeB-4L2lztTaJxVhANSO1lDQ" + ], + "priority": [ + "100" + ], + "algorithm": [ + "HS256" + ] + } + }, + { + "id": "594b0771-b860-4aa8-8a81-4b29ee5ac384", + "name": "rsa-generated", + "providerId": "rsa-generated", + "subComponents": {}, + "config": { + "privateKey": [ + "MIIEowIBAAKCAQEAsZSMHIVGaorVvBfVYzTAHzXO7/ilxcjty/hayFAB3C1CkEnKeYIs2hUv8s3DFrG26GBFRWI+HVX8yCCuwksDkF4kB6ZxpNV/NCwt4+Hv1Fr/A58eVHkriXajlOE9LUkvrSd04bGGD+SplAgCweQzVcHF5Iy4bvsIJ8ow5WKJ7dGtJlnU2ddBEdja2KhlFWBdqn2YCdvpuLGEYNGr5/ANBM6evcv/bU3tVdq64TPSC13kw/QicKzAU6H+4k8reIAgaEhtudqb6sWI9G0JiSPtb98psTZMFjoGb9yphOFsoSmljA3Ozp2wDVltV/zyk8Aw7J2I8EOCdjcX8BdkkQc6oQIDAQABAoIBABEdhJ2RGNbW97+vumDb6jJ34LCPUgbslULF9pX85BkBAbvfaNTqP4FrblokC8wJp9vgv3xu+hagvYLaZ42RZlAJSsaz+5sL+r0gDvI6Sf+5H4ANW4J/xTr0BNMqHFfbiG1Tcrf4ALhSbSe31/AxGuOGkBi1mWcU6dXP7oOFSk7x79FVmirB0bKGGwd2TNbrmtBSiDU33vUPxwGnDSsmw8TVHyjISiM0BfvVkS9RrBGqEnNm0iKccukRgengrqCK8D4fq65YPrQQQ9o9I50eU0qHoCiVJyNC2+MBpiOniShLO8jyiLAuhDKDfoKis5C6Hqm1yyf2PzwB1rIjV5XVf2ECgYEA6JiWMHHKfyR3QNvGjRp3Jfz/WQvLGXZCwVDowhlNRtfvGC56tEC+QAWxM/4l12u1NMyYSDeF44tYpzq0dGtVkTWTzQKNczTkGhValNMgFEUa8pwumBVVfrfLllA0VGrMW6fEAr8ta2gFODOWoViLSD0s0Jmu6CpdKSUzafcnMjkCgYEAw3LQ2sLgGL8LajjPl++LgWcKRZF3M80IFF2fE78I80zVow4x6Ei+EyrvObmJ5necXKkRA7o7h/7Xb+ATy2h0ZCdr/OXpmR/yGASfUdNXDrtQ0nYF6TNz1Xqadi9cGv7YKy8SOgbB0SM5S6aRy+ouoaUwNCSsWDleAgEOQvpzq6kCgYBrPv/xMmaWHTBHXY69PPi3MWJjooZxJRA+ppnL9XKmOaZq1fOJ7VhLmNRODt9P5r/UqomEsuUvN+8WnIDcNSltHPEbVBP4jOioBjSP7pEaB4sXVmA9i4iyNvjORAj864lysXY1dgTxQzM06MSJfJQsKNjjDhmRvwbZk+eS8nzGMQKBgAFjAipbMZ3bVShmyMpKL9I2OfNuacsbTFBgra1FMLoRNH7Yre/4/ChEqLffIiRZeumJZY6CNsPrQfoQO/O4hQLk6LY9p1+nw176QWsiNb7sA1HK9pXGAK9mFEx8X4ntfvknd1ikDaH/PvvTbbtlqPkKpAHqtLJXjdwzx7cf8cwpAoGBALxwuNYA7NBV8qRzTsLfSGoZEfl6jQogD3GR+EmFswIEYzdp8Mp+VwcpBjP5D28v9kFKa8dCe7TU9aY1JZm5W+N3ZqfhLp6TNepKVpRsyKsF4Pjz9fDZu1Q9AU81ImSO4w6FkLqQc069w7M5O/tT4/WjYbfqEp16UkP77Gh1F4O6" + ], + "keyUse": [ + "SIG" + ], + "certificate": [ + "MIIClzCCAX8CBgGEV1OLDTANBgkqhkiG9w0BAQsFADAPMQ0wCwYDVQQDDARkZW1vMB4XDTIyMTEwODEyNTgyM1oXDTMyMTEwODEzMDAwM1owDzENMAsGA1UEAwwEZGVtbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALGUjByFRmqK1bwX1WM0wB81zu/4pcXI7cv4WshQAdwtQpBJynmCLNoVL/LNwxaxtuhgRUViPh1V/MggrsJLA5BeJAemcaTVfzQsLePh79Ra/wOfHlR5K4l2o5ThPS1JL60ndOGxhg/kqZQIAsHkM1XBxeSMuG77CCfKMOViie3RrSZZ1NnXQRHY2tioZRVgXap9mAnb6bixhGDRq+fwDQTOnr3L/21N7VXauuEz0gtd5MP0InCswFOh/uJPK3iAIGhIbbnam+rFiPRtCYkj7W/fKbE2TBY6Bm/cqYThbKEppYwNzs6dsA1ZbVf88pPAMOydiPBDgnY3F/AXZJEHOqECAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAdo+c58iEquMpcBiMQ44lhSnroyrRwHazMxQ/8fHUUh2QxNwlGynGPfhMNyUtn06sPZqabV1ixTDfb6NbEpJK/HN3meWUNl4I4i5Zabew5DuUCh/BLRUbgOApsoRyHabDlR68inuXIPaP4M8lOfQsZO2/xNuGnr/eedKIR1WotXtmm2WJv79A9tJQkplizS78HoCa+HlyP1UAuAUDO0IZsJwY8CbKq1wgrhs9by8amdzZRBVILuDnuqEqeRxSY4o4BOvtM7TG5aA0iBVQc473NT1IvY10ojW6zs/ahqs0yG44+W2aBG35DvoDNVqP9Lw1vaTAD7OHdzQGhREz07cemA==" + ], + "priority": [ + "100" + ] + } + }, + { + "id": "da244786-6bf6-40c4-b7c3-094f883d969e", + "name": "aes-generated", + "providerId": "aes-generated", + "subComponents": {}, + "config": { + "kid": [ + "6ee413ed-3b74-458f-acb7-a89d5e05f0b9" + ], + "secret": [ + "sANwaoZOUoMcaPlAr5U1sQ" + ], + "priority": [ + "100" + ] + } + }, + { + "id": "1eeba879-03e8-4aad-b91a-0159cf513d73", + "name": "rsa-enc-generated", + "providerId": "rsa-enc-generated", + "subComponents": {}, + "config": { + "privateKey": [ + "MIIEowIBAAKCAQEA4FmIA8jfFAMVm8BuINWDWCn//Xzlk48yoUly1kP8RXriGOYfXrQQTqae0nhBBqlaJFBXAjI41UGJE9+4j23/aGp8vcTTUy9M4qBl9jlt9Pqz178/KaMDuBkeb3TV8PbR6PXrCSuWOuT3LJaXWEitpeMM/+dFSOxD4M1LWOwoQltSlmiZblcb5NsRal60sHvFUA5ecp35jW7Rj+xAQKR7QxgOr8rf83P9NPWYww4+lbKdbgnD2IMfdzZ9soSiHX4WtRrl1dPAqnihfEeaOoDXXTa1m49Q4xORnMVs6E15A+Zviu0xhR1385GT7sHLhZbSh+oLH4nx9jFinry+AgGlUwIDAQABAoIBABbvIBfe82r8y7sxxzBFE1myZXBY0bEtbMwPEZW0unex0aYg9Cj+uEIKB2dVkrQnIMdgjRx03Nl0CxrEfn3vDTJz3E+b7Mxuo+nw4qtygHqQHE1cSA0uFGW/75wOMgahfKDXbtDvqzpXCKt+s3b7awDvvnb0geEsAd5bri2napApy7qEg8iD04NhJhPj+nnoZ/jWTs3N++Dy/a0Y4/TsmIJXjtOY4JUcbYSJDBgFW0hikU/JKRMYXRZNCOzgDlUO+ejxKR2/HJjNK3ynf8Z5seROvVLI6sDTAA20b/PQLW4yEC7BtjWor8zag5tN6ZORdf4tZrsWLAiSAU2NPZepRx0CgYEA9duql4nPIs9Vf7GBZeVJd8Vl5z4WuAbaLgZ0TxaUwU83b9oaw1JmQgZ7gOCDgBkdECJk7iZAgdCCDKyFZyq+aeE8jn2uWXXrh/nMw2rXtAu3K464oyN2kWd4d7vinEsm7B5Vz1tiT20uuACS8AWi63pdBsx7yIsoq/yTantSMN0CgYEA6Zq7A++W2vwCWf0bzuwZ8ozObHafr3awgnEzlD2CXUW0mBC3ptxbo9+ug9SnZC7UMVNEqTVArd2mFZYEnDZGvGbsiN7Qnfw3foKdNpvxMP+CfuQw7vxmCa1JOd5nLz6zKBEPGjIuCy0+wB6ASpUQNxUQQkQoC7L8Qt5cysUSM+8CgYAiSM6iMSp8bTM8ClHEFtRG6nUKaSMb6IC2WFoRyVFXH6fYZi7DPBNcc7D3SNetnlLqNBGlEBqAv8XS5J/5wgEpnKooKKiOex4sKQ5/1b9csSGK5m0i+sgHAMnQ0JeKOgSkepp2vwSXlN8l85aJ+A8/DSI513wPfDBgw2j/OVE91QKBgCnanTNBVAf8Kvewj7DtQGDitYFdZ5LqcwmL+q/OrXLEsGymYiE1Tf34b64TBcK/WSlVP/IJJoOAOOeZL05FszrCPhLvyPTlYZP7FuvX2MjsnpbZj6Lh+e416+7AWEBwvWyqUchhwTojayDE1juGpZcY4Qbea0ZdVTEt4fY6hN5lAoGBAIjrfcjMELZqlLEECwjvTgPmYqTgyJKgVw/SrYBvPpNNqVeIwb62FK19BjXuoIjecXBX8jvefGNa6qTw/OjZLk7JNNbbOdJ1PFp8GkD+e0PPUoISSY0+rAq77geLLfZbcZUwoE0UqKwSYcz0w549aUVKc/5x3tHOxEb7UcWWS5DY" + ], + "keyUse": [ + "ENC" + ], + "certificate": [ + "MIIClzCCAX8CBgGEV1OMBzANBgkqhkiG9w0BAQsFADAPMQ0wCwYDVQQDDARkZW1vMB4XDTIyMTEwODEyNTgyM1oXDTMyMTEwODEzMDAwM1owDzENMAsGA1UEAwwEZGVtbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOBZiAPI3xQDFZvAbiDVg1gp//185ZOPMqFJctZD/EV64hjmH160EE6mntJ4QQapWiRQVwIyONVBiRPfuI9t/2hqfL3E01MvTOKgZfY5bfT6s9e/PymjA7gZHm901fD20ej16wkrljrk9yyWl1hIraXjDP/nRUjsQ+DNS1jsKEJbUpZomW5XG+TbEWpetLB7xVAOXnKd+Y1u0Y/sQECke0MYDq/K3/Nz/TT1mMMOPpWynW4Jw9iDH3c2fbKEoh1+FrUa5dXTwKp4oXxHmjqA1102tZuPUOMTkZzFbOhNeQPmb4rtMYUdd/ORk+7By4WW0ofqCx+J8fYxYp68vgIBpVMCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAwPwABXVkSlRYqA9sMKJrw9piGH8tkwf6wQeAFhQsInbDzXLeuLt0A3gSuh5nL2zOGcxXddIK4IgUSqI+DlFlFsSkVqHFrQBAdIVRXsYFvGbARKhVuInHlpaOy6Y/VC6opL1BnqsmUOPEv7pk4Nhf/z7y5yZfTUaGiD+K1KA/mEf56NytOFJYsxiCZaAGX6BYIavRJp3YKPAcsNlJS//1G4meOlYcx5HiTA+qY/spc7vPeKuowSh3v26x4tgypLqoD0BAS5KrK4PEVaQM0IcBC3jrIY+7dGbE1Z374nm+1FDU7md48TJdI0c75r60cpBnHUH2bLJo2Z0ezrEojP6mvQ==" + ], + "priority": [ + "100" + ], + "algorithm": [ + "RSA-OAEP" + ] + } + } + ] + }, + "internationalizationEnabled": false, + "supportedLocales": [], + "authenticationFlows": [ + { + "id": "95570b1f-da9e-42a0-9532-aa9b690b04cb", + "alias": "Account verification options", + "description": "Method with which to verity the existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-email-verification", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Verify Existing Account by Re-authentication", + "userSetupAllowed": false + } + ] + }, + { + "id": "854f25b9-b37b-468a-b8ac-1e0da29133ba", + "alias": "Authentication Options", + "description": "Authentication options.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "basic-auth", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "basic-auth-otp", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-spnego", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 30, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "ebcd0954-f08f-4c69-970e-43569320bb07", + "alias": "Browser - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-otp-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "cd4b03ca-1a0f-4a9b-8bd9-388e0459d7ac", + "alias": "Direct Grant - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "direct-grant-validate-otp", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "692bd371-d43a-440b-886b-2a0884dd5b9f", + "alias": "First broker login - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-otp-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "dce38371-4f6b-45d1-94b0-5e91182c31fd", + "alias": "Handle Existing Account", + "description": "Handle what to do if there is existing account with same email/username like authenticated identity provider", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-confirm-link", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Account verification options", + "userSetupAllowed": false + } + ] + }, + { + "id": "357541a3-2499-42da-9ef0-db129f38f39b", + "alias": "Reset - Conditional OTP", + "description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-otp", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "9c37a124-af5e-44a2-b619-b3a88e1033c6", + "alias": "User creation or linking", + "description": "Flow for the existing/non-existing user alternatives", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "create unique user config", + "authenticator": "idp-create-user-if-unique", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Handle Existing Account", + "userSetupAllowed": false + } + ] + }, + { + "id": "94355d8f-b66f-405d-80e5-55ec030fb18c", + "alias": "Verify Existing Account by Re-authentication", + "description": "Reauthentication of existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-username-password-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "First broker login - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "8fcdac85-ec7e-4a3b-908a-4f00978d4724", + "alias": "browser", + "description": "browser based authentication", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-cookie", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-spnego", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "identity-provider-redirector", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 25, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 30, + "autheticatorFlow": true, + "flowAlias": "forms", + "userSetupAllowed": false + } + ] + }, + { + "id": "cd9ce79e-ba0b-4d58-8249-40f8b2139a56", + "alias": "clients", + "description": "Base authentication for clients", + "providerId": "client-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "client-secret", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-jwt", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-secret-jwt", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 30, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-x509", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 40, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "40f9ac10-edc0-46a6-b020-ba1785199911", + "alias": "direct grant", + "description": "OpenID Connect Resource Owner Grant", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "direct-grant-validate-username", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "direct-grant-validate-password", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 30, + "autheticatorFlow": true, + "flowAlias": "Direct Grant - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "0ac19480-8675-4679-b222-64508c62bcdb", + "alias": "docker auth", + "description": "Used by Docker clients to authenticate against the IDP", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "docker-http-basic-authenticator", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "9fd2aa99-79d7-435b-a852-5c18fad28546", + "alias": "first broker login", + "description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "review profile config", + "authenticator": "idp-review-profile", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "User creation or linking", + "userSetupAllowed": false + } + ] + }, + { + "id": "da0a8a3d-9643-4746-b42d-2e8c915f76e7", + "alias": "forms", + "description": "Username, password, otp and other auth forms.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-username-password-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Browser - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "5eea7962-6fe5-4b69-8401-313afd0f6559", + "alias": "http challenge", + "description": "An authentication flow based on challenge-response HTTP Authentication Schemes", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "no-cookie-redirect", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Authentication Options", + "userSetupAllowed": false + } + ] + }, + { + "id": "f80d7c69-9314-4f4b-a61a-28a73e2d0879", + "alias": "registration", + "description": "registration flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-page-form", + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": true, + "flowAlias": "registration form", + "userSetupAllowed": false + } + ] + }, + { + "id": "deffbcd6-aa05-40e2-9bca-2ed50be54bd6", + "alias": "registration form", + "description": "registration form", + "providerId": "form-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-user-creation", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "registration-profile-action", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 40, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "registration-password-action", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 50, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "registration-recaptcha-action", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 60, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "0b06e91d-77f8-4d8e-a55b-21caf478524a", + "alias": "reset credentials", + "description": "Reset credentials for a user if they forgot their password or something", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "reset-credentials-choose-user", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-credential-email", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-password", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 30, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 40, + "autheticatorFlow": true, + "flowAlias": "Reset - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "d51e394a-e9a8-495b-ac30-3adb1e66a369", + "alias": "saml ecp", + "description": "SAML ECP Profile Authentication Flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "http-basic-authenticator", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + } + ], + "authenticatorConfig": [ + { + "id": "3c1657ce-3153-4718-bbec-9d0c508eb8a8", + "alias": "create unique user config", + "config": { + "require.password.update.after.registration": "false" + } + }, + { + "id": "70a33e93-226e-492e-b119-f164e2cc457b", + "alias": "review profile config", + "config": { + "update.profile.on.first.login": "missing" + } + } + ], + "requiredActions": [ + { + "alias": "CONFIGURE_TOTP", + "name": "Configure OTP", + "providerId": "CONFIGURE_TOTP", + "enabled": true, + "defaultAction": false, + "priority": 10, + "config": {} + }, + { + "alias": "terms_and_conditions", + "name": "Terms and Conditions", + "providerId": "terms_and_conditions", + "enabled": false, + "defaultAction": false, + "priority": 20, + "config": {} + }, + { + "alias": "UPDATE_PASSWORD", + "name": "Update Password", + "providerId": "UPDATE_PASSWORD", + "enabled": true, + "defaultAction": false, + "priority": 30, + "config": {} + }, + { + "alias": "UPDATE_PROFILE", + "name": "Update Profile", + "providerId": "UPDATE_PROFILE", + "enabled": true, + "defaultAction": false, + "priority": 40, + "config": {} + }, + { + "alias": "VERIFY_EMAIL", + "name": "Verify Email", + "providerId": "VERIFY_EMAIL", + "enabled": true, + "defaultAction": false, + "priority": 50, + "config": {} + }, + { + "alias": "delete_account", + "name": "Delete Account", + "providerId": "delete_account", + "enabled": false, + "defaultAction": false, + "priority": 60, + "config": {} + }, + { + "alias": "webauthn-register", + "name": "Webauthn Register", + "providerId": "webauthn-register", + "enabled": true, + "defaultAction": false, + "priority": 70, + "config": {} + }, + { + "alias": "webauthn-register-passwordless", + "name": "Webauthn Register Passwordless", + "providerId": "webauthn-register-passwordless", + "enabled": true, + "defaultAction": false, + "priority": 80, + "config": {} + }, + { + "alias": "update_user_locale", + "name": "Update User Locale", + "providerId": "update_user_locale", + "enabled": true, + "defaultAction": false, + "priority": 1000, + "config": {} + } + ], + "browserFlow": "browser", + "registrationFlow": "registration", + "directGrantFlow": "direct grant", + "resetCredentialsFlow": "reset credentials", + "clientAuthenticationFlow": "clients", + "dockerAuthenticationFlow": "docker auth", + "attributes": { + "cibaBackchannelTokenDeliveryMode": "poll", + "cibaExpiresIn": "120", + "cibaAuthRequestedUserHint": "login_hint", + "oauth2DeviceCodeLifespan": "600", + "oauth2DevicePollingInterval": "5", + "parRequestUriLifespan": "60", + "cibaInterval": "5" + }, + "keycloakVersion": "19.0.3", + "userManagedAccessAllowed": false, + "clientProfiles": { + "profiles": [] + }, + "clientPolicies": { + "policies": [] + } +} diff --git a/src/main/resources/baseline/20.0.3/client/client.json b/src/main/resources/baseline/20.0.3/client/client.json new file mode 100644 index 000000000..d555d4f48 --- /dev/null +++ b/src/main/resources/baseline/20.0.3/client/client.json @@ -0,0 +1,45 @@ +{ + "id": "8a641514-bb92-4a5e-8ea4-27b90ef3e637", + "clientId": "reference-client", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "hzjJYnHVxMf3I3ugD4le0CgT1iI3rCx2", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "client.secret.creation.time": "1676457441" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": -1, + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ], + "access": { + "view": true, + "configure": true, + "manage": true + } +} diff --git a/src/main/resources/baseline/20.0.3/realm/realm.json b/src/main/resources/baseline/20.0.3/realm/realm.json new file mode 100644 index 000000000..ee28d9e47 --- /dev/null +++ b/src/main/resources/baseline/20.0.3/realm/realm.json @@ -0,0 +1,2188 @@ +{ + "id": "REALM_NAME_PLACEHOLDER", + "realm": "REALM_NAME_PLACEHOLDER", + "notBefore": 0, + "defaultSignatureAlgorithm": "RS256", + "revokeRefreshToken": false, + "refreshTokenMaxReuse": 0, + "accessTokenLifespan": 300, + "accessTokenLifespanForImplicitFlow": 900, + "ssoSessionIdleTimeout": 1800, + "ssoSessionMaxLifespan": 36000, + "ssoSessionIdleTimeoutRememberMe": 0, + "ssoSessionMaxLifespanRememberMe": 0, + "offlineSessionIdleTimeout": 2592000, + "offlineSessionMaxLifespanEnabled": false, + "offlineSessionMaxLifespan": 5184000, + "clientSessionIdleTimeout": 0, + "clientSessionMaxLifespan": 0, + "clientOfflineSessionIdleTimeout": 0, + "clientOfflineSessionMaxLifespan": 0, + "accessCodeLifespan": 60, + "accessCodeLifespanUserAction": 300, + "accessCodeLifespanLogin": 1800, + "actionTokenGeneratedByAdminLifespan": 43200, + "actionTokenGeneratedByUserLifespan": 300, + "oauth2DeviceCodeLifespan": 600, + "oauth2DevicePollingInterval": 5, + "enabled": true, + "sslRequired": "external", + "registrationAllowed": false, + "registrationEmailAsUsername": false, + "rememberMe": false, + "verifyEmail": false, + "loginWithEmailAllowed": true, + "duplicateEmailsAllowed": false, + "resetPasswordAllowed": false, + "editUsernameAllowed": false, + "bruteForceProtected": false, + "permanentLockout": false, + "maxFailureWaitSeconds": 900, + "minimumQuickLoginWaitSeconds": 60, + "waitIncrementSeconds": 60, + "quickLoginCheckMilliSeconds": 1000, + "maxDeltaTimeSeconds": 43200, + "failureFactor": 30, + "roles": { + "realm": [ + { + "id": "c1c757e3-1483-4a13-a650-57d13762063d", + "name": "offline_access", + "description": "${role_offline-access}", + "composite": false, + "clientRole": false, + "containerId": "a63b0d92-16b3-4110-8dd8-b25ed575035a", + "attributes": {} + }, + { + "id": "1df44d36-8c3e-47a9-8b37-28b31c9c5fd1", + "name": "uma_authorization", + "description": "${role_uma_authorization}", + "composite": false, + "clientRole": false, + "containerId": "a63b0d92-16b3-4110-8dd8-b25ed575035a", + "attributes": {} + }, + { + "id": "03ee0166-2480-429b-bc22-8f6fcd4f8126", + "name": "default-roles-REALM_NAME_PLACEHOLDER", + "description": "${role_default-roles}", + "composite": true, + "composites": { + "realm": [ + "offline_access", + "uma_authorization" + ], + "client": { + "account": [ + "view-profile", + "manage-account" + ] + } + }, + "clientRole": false, + "containerId": "a63b0d92-16b3-4110-8dd8-b25ed575035a", + "attributes": {} + } + ], + "client": { + "realm-management": [ + { + "id": "1305643f-47b2-471a-95b0-42f962443c0e", + "name": "view-authorization", + "description": "${role_view-authorization}", + "composite": false, + "clientRole": true, + "containerId": "61f1ed79-6efa-4109-9051-cd26de56f538", + "attributes": {} + }, + { + "id": "deb716da-2b9c-429d-a23a-21e3e40caf11", + "name": "view-realm", + "description": "${role_view-realm}", + "composite": false, + "clientRole": true, + "containerId": "61f1ed79-6efa-4109-9051-cd26de56f538", + "attributes": {} + }, + { + "id": "be59b49b-4a79-400f-a67b-0a17903155c9", + "name": "manage-realm", + "description": "${role_manage-realm}", + "composite": false, + "clientRole": true, + "containerId": "61f1ed79-6efa-4109-9051-cd26de56f538", + "attributes": {} + }, + { + "id": "9b2e7245-5859-49fd-a3b0-aeb215dc6e12", + "name": "view-users", + "description": "${role_view-users}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "query-groups", + "query-users" + ] + } + }, + "clientRole": true, + "containerId": "61f1ed79-6efa-4109-9051-cd26de56f538", + "attributes": {} + }, + { + "id": "7e010c9f-565b-450b-a222-0122ab71010d", + "name": "manage-clients", + "description": "${role_manage-clients}", + "composite": false, + "clientRole": true, + "containerId": "61f1ed79-6efa-4109-9051-cd26de56f538", + "attributes": {} + }, + { + "id": "92004b3a-9187-4f73-9169-319dbdec02bb", + "name": "query-groups", + "description": "${role_query-groups}", + "composite": false, + "clientRole": true, + "containerId": "61f1ed79-6efa-4109-9051-cd26de56f538", + "attributes": {} + }, + { + "id": "696b1e73-b5f0-4990-a00f-943a53ff4555", + "name": "manage-users", + "description": "${role_manage-users}", + "composite": false, + "clientRole": true, + "containerId": "61f1ed79-6efa-4109-9051-cd26de56f538", + "attributes": {} + }, + { + "id": "aae4db77-9a27-4cb7-b6f6-3f109f553502", + "name": "query-users", + "description": "${role_query-users}", + "composite": false, + "clientRole": true, + "containerId": "61f1ed79-6efa-4109-9051-cd26de56f538", + "attributes": {} + }, + { + "id": "e2fd690b-88be-4953-8bff-3225e40fdbd4", + "name": "view-events", + "description": "${role_view-events}", + "composite": false, + "clientRole": true, + "containerId": "61f1ed79-6efa-4109-9051-cd26de56f538", + "attributes": {} + }, + { + "id": "39b3b817-bea6-47f5-8b34-4fc32211e433", + "name": "manage-authorization", + "description": "${role_manage-authorization}", + "composite": false, + "clientRole": true, + "containerId": "61f1ed79-6efa-4109-9051-cd26de56f538", + "attributes": {} + }, + { + "id": "2615c6ce-7dc4-448b-940f-26905a99b25d", + "name": "manage-events", + "description": "${role_manage-events}", + "composite": false, + "clientRole": true, + "containerId": "61f1ed79-6efa-4109-9051-cd26de56f538", + "attributes": {} + }, + { + "id": "254a5871-81e9-4e96-b6a1-cf6c27d3ddb2", + "name": "query-realms", + "description": "${role_query-realms}", + "composite": false, + "clientRole": true, + "containerId": "61f1ed79-6efa-4109-9051-cd26de56f538", + "attributes": {} + }, + { + "id": "81582a5b-5be7-45be-9c0b-52582ede762a", + "name": "manage-identity-providers", + "description": "${role_manage-identity-providers}", + "composite": false, + "clientRole": true, + "containerId": "61f1ed79-6efa-4109-9051-cd26de56f538", + "attributes": {} + }, + { + "id": "322ce24f-42f5-40c6-a8bd-d31734ac9834", + "name": "impersonation", + "description": "${role_impersonation}", + "composite": false, + "clientRole": true, + "containerId": "61f1ed79-6efa-4109-9051-cd26de56f538", + "attributes": {} + }, + { + "id": "30e5d91d-ac17-4812-ba11-cba05e1add77", + "name": "realm-admin", + "description": "${role_realm-admin}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "view-authorization", + "view-realm", + "manage-realm", + "view-users", + "manage-clients", + "query-groups", + "manage-users", + "query-users", + "view-events", + "manage-authorization", + "query-realms", + "manage-events", + "manage-identity-providers", + "impersonation", + "view-clients", + "create-client", + "query-clients", + "view-identity-providers" + ] + } + }, + "clientRole": true, + "containerId": "61f1ed79-6efa-4109-9051-cd26de56f538", + "attributes": {} + }, + { + "id": "c21b3f56-ea00-4e69-905d-88fdb6e78dfa", + "name": "create-client", + "description": "${role_create-client}", + "composite": false, + "clientRole": true, + "containerId": "61f1ed79-6efa-4109-9051-cd26de56f538", + "attributes": {} + }, + { + "id": "93612d37-df0a-4898-9c9a-c01afb1249b8", + "name": "view-clients", + "description": "${role_view-clients}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "query-clients" + ] + } + }, + "clientRole": true, + "containerId": "61f1ed79-6efa-4109-9051-cd26de56f538", + "attributes": {} + }, + { + "id": "1e59ed3e-a91c-4ee3-8b29-a8da6c035549", + "name": "query-clients", + "description": "${role_query-clients}", + "composite": false, + "clientRole": true, + "containerId": "61f1ed79-6efa-4109-9051-cd26de56f538", + "attributes": {} + }, + { + "id": "15a8e4ef-f406-44e6-b4dc-21fdce6fd023", + "name": "view-identity-providers", + "description": "${role_view-identity-providers}", + "composite": false, + "clientRole": true, + "containerId": "61f1ed79-6efa-4109-9051-cd26de56f538", + "attributes": {} + } + ], + "security-admin-console": [], + "admin-cli": [], + "account-console": [], + "broker": [ + { + "id": "ed91f070-bce3-4b4c-a1bd-066bc96ff3e0", + "name": "read-token", + "description": "${role_read-token}", + "composite": false, + "clientRole": true, + "containerId": "db9b8810-7b16-47a9-8b9e-8de58449c206", + "attributes": {} + } + ], + "account": [ + { + "id": "f583ff69-51dc-4c6e-8a1d-addf142c3220", + "name": "delete-account", + "description": "${role_delete-account}", + "composite": false, + "clientRole": true, + "containerId": "28e61d0d-c4dc-4a91-8012-1a2f0325945c", + "attributes": {} + }, + { + "id": "9ac5bf5b-bb5d-4138-b397-04ea73622a60", + "name": "view-profile", + "description": "${role_view-profile}", + "composite": false, + "clientRole": true, + "containerId": "28e61d0d-c4dc-4a91-8012-1a2f0325945c", + "attributes": {} + }, + { + "id": "ab3ce8f1-ec74-4a35-b00a-259db0bc0878", + "name": "view-consent", + "description": "${role_view-consent}", + "composite": false, + "clientRole": true, + "containerId": "28e61d0d-c4dc-4a91-8012-1a2f0325945c", + "attributes": {} + }, + { + "id": "6907ab95-1c60-4f5b-912b-ee6d5c98346d", + "name": "manage-account", + "description": "${role_manage-account}", + "composite": true, + "composites": { + "client": { + "account": [ + "manage-account-links" + ] + } + }, + "clientRole": true, + "containerId": "28e61d0d-c4dc-4a91-8012-1a2f0325945c", + "attributes": {} + }, + { + "id": "073dfba9-b2b5-42de-a147-e0ac00b9cd76", + "name": "view-groups", + "description": "${role_view-groups}", + "composite": false, + "clientRole": true, + "containerId": "28e61d0d-c4dc-4a91-8012-1a2f0325945c", + "attributes": {} + }, + { + "id": "e9989640-ed85-40e5-86fc-51473fdafd4f", + "name": "view-applications", + "description": "${role_view-applications}", + "composite": false, + "clientRole": true, + "containerId": "28e61d0d-c4dc-4a91-8012-1a2f0325945c", + "attributes": {} + }, + { + "id": "d5b8032e-e9b2-44fb-8354-ac3e068fb6d3", + "name": "manage-account-links", + "description": "${role_manage-account-links}", + "composite": false, + "clientRole": true, + "containerId": "28e61d0d-c4dc-4a91-8012-1a2f0325945c", + "attributes": {} + }, + { + "id": "93560eab-3dd4-4ff4-b252-6f6c2103ff31", + "name": "manage-consent", + "description": "${role_manage-consent}", + "composite": true, + "composites": { + "client": { + "account": [ + "view-consent" + ] + } + }, + "clientRole": true, + "containerId": "28e61d0d-c4dc-4a91-8012-1a2f0325945c", + "attributes": {} + } + ] + } + }, + "groups": [], + "defaultRole": { + "id": "03ee0166-2480-429b-bc22-8f6fcd4f8126", + "name": "default-roles-REALM_NAME_PLACEHOLDER", + "description": "${role_default-roles}", + "composite": true, + "clientRole": false, + "containerId": "a63b0d92-16b3-4110-8dd8-b25ed575035a" + }, + "requiredCredentials": [ + "password" + ], + "otpPolicyType": "totp", + "otpPolicyAlgorithm": "HmacSHA1", + "otpPolicyInitialCounter": 0, + "otpPolicyDigits": 6, + "otpPolicyLookAheadWindow": 1, + "otpPolicyPeriod": 30, + "otpPolicyCodeReusable": false, + "otpSupportedApplications": [ + "totpAppGoogleName", + "totpAppFreeOTPName" + ], + "webAuthnPolicyRpEntityName": "keycloak", + "webAuthnPolicySignatureAlgorithms": [ + "ES256" + ], + "webAuthnPolicyRpId": "", + "webAuthnPolicyAttestationConveyancePreference": "not specified", + "webAuthnPolicyAuthenticatorAttachment": "not specified", + "webAuthnPolicyRequireResidentKey": "not specified", + "webAuthnPolicyUserVerificationRequirement": "not specified", + "webAuthnPolicyCreateTimeout": 0, + "webAuthnPolicyAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyAcceptableAaguids": [], + "webAuthnPolicyPasswordlessRpEntityName": "keycloak", + "webAuthnPolicyPasswordlessSignatureAlgorithms": [ + "ES256" + ], + "webAuthnPolicyPasswordlessRpId": "", + "webAuthnPolicyPasswordlessAttestationConveyancePreference": "not specified", + "webAuthnPolicyPasswordlessAuthenticatorAttachment": "not specified", + "webAuthnPolicyPasswordlessRequireResidentKey": "not specified", + "webAuthnPolicyPasswordlessUserVerificationRequirement": "not specified", + "webAuthnPolicyPasswordlessCreateTimeout": 0, + "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyPasswordlessAcceptableAaguids": [], + "scopeMappings": [ + { + "clientScope": "offline_access", + "roles": [ + "offline_access" + ] + } + ], + "clientScopeMappings": { + "account": [ + { + "client": "account-console", + "roles": [ + "manage-account", + "view-groups" + ] + } + ] + }, + "clients": [ + { + "id": "28e61d0d-c4dc-4a91-8012-1a2f0325945c", + "clientId": "account", + "name": "${client_account}", + "rootUrl": "${authBaseUrl}", + "baseUrl": "/realms/REALM_NAME_PLACEHOLDER/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/realms/REALM_NAME_PLACEHOLDER/account/*" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "a1797864-8592-4d75-b2c0-af15ca029abd", + "clientId": "account-console", + "name": "${client_account-console}", + "rootUrl": "${authBaseUrl}", + "baseUrl": "/realms/REALM_NAME_PLACEHOLDER/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/realms/REALM_NAME_PLACEHOLDER/account/*" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+", + "pkce.code.challenge.method": "S256" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "7e32f2c3-d9fb-40e0-a618-fb68c921f9d9", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": {} + } + ], + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "bb4d6126-c9c3-4c39-8016-805d04d55829", + "clientId": "admin-cli", + "name": "${client_admin-cli}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": false, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "db9b8810-7b16-47a9-8b9e-8de58449c206", + "clientId": "broker", + "name": "${client_broker}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "61f1ed79-6efa-4109-9051-cd26de56f538", + "clientId": "realm-management", + "name": "${client_realm-management}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "6c009e16-d012-43ac-9093-ce95786c2cb8", + "clientId": "security-admin-console", + "name": "${client_security-admin-console}", + "rootUrl": "${authAdminUrl}", + "baseUrl": "/admin/REALM_NAME_PLACEHOLDER/console/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/admin/REALM_NAME_PLACEHOLDER/console/*" + ], + "webOrigins": [ + "+" + ], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+", + "pkce.code.challenge.method": "S256" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "84f24df3-9b58-41fb-a05a-e591fc47e9d7", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" + } + } + ], + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + } + ], + "clientScopes": [ + { + "id": "aa08b21f-f33f-4079-be3d-1e925b44c935", + "name": "offline_access", + "description": "OpenID Connect built-in scope: offline_access", + "protocol": "openid-connect", + "attributes": { + "consent.screen.text": "${offlineAccessScopeConsentText}", + "display.on.consent.screen": "true" + } + }, + { + "id": "ab4f2643-cc27-463e-bfba-44712cffb45e", + "name": "acr", + "description": "OpenID Connect scope for add acr (authentication context class reference) to the token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "bcc3ec1d-d4bb-4171-82c3-e9b7f2de7371", + "name": "acr loa level", + "protocol": "openid-connect", + "protocolMapper": "oidc-acr-mapper", + "consentRequired": false, + "config": { + "id.token.claim": "true", + "access.token.claim": "true" + } + } + ] + }, + { + "id": "804093f4-8a08-4cca-a1a1-6008b7695e92", + "name": "web-origins", + "description": "OpenID Connect scope for add allowed web origins to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "false", + "consent.screen.text": "" + }, + "protocolMappers": [ + { + "id": "17a68f02-d8dc-4771-aeeb-d6e262e4fd07", + "name": "allowed web origins", + "protocol": "openid-connect", + "protocolMapper": "oidc-allowed-origins-mapper", + "consentRequired": false, + "config": {} + } + ] + }, + { + "id": "bb39a86e-661c-47ef-aa54-d3d2da043340", + "name": "phone", + "description": "OpenID Connect built-in scope: phone", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${phoneScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "17d26261-10f5-4f60-8cf5-c4106af3a3ee", + "name": "phone number", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "phoneNumber", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number", + "jsonType.label": "String" + } + }, + { + "id": "4a18f46f-63ac-4577-89c1-caa895a05255", + "name": "phone number verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "phoneNumberVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number_verified", + "jsonType.label": "boolean" + } + } + ] + }, + { + "id": "c91c9396-3cca-48c7-93b8-e93567f56f7e", + "name": "profile", + "description": "OpenID Connect built-in scope: profile", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${profileScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "888660e1-577c-4249-9703-e86241b9e714", + "name": "birthdate", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "birthdate", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "birthdate", + "jsonType.label": "String" + } + }, + { + "id": "3ddfe32a-e127-4808-8f59-a7ba97345258", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" + } + }, + { + "id": "ab023b96-89e4-41ed-b923-288bac047809", + "name": "middle name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "middleName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "middle_name", + "jsonType.label": "String" + } + }, + { + "id": "010b1c53-3403-476c-904f-81f51eaf297a", + "name": "username", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "preferred_username", + "jsonType.label": "String" + } + }, + { + "id": "558d8acf-56c5-4011-a366-f4370ebfaa4c", + "name": "zoneinfo", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "zoneinfo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "zoneinfo", + "jsonType.label": "String" + } + }, + { + "id": "5ca091f1-dd75-4f51-92cf-8fe52e1bd91a", + "name": "picture", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "picture", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "picture", + "jsonType.label": "String" + } + }, + { + "id": "c77ea1c7-af58-4fe4-8760-d5fc367a537d", + "name": "website", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "website", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "website", + "jsonType.label": "String" + } + }, + { + "id": "8aa68575-a903-46ae-adb7-43b4c5573f88", + "name": "nickname", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "nickname", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "nickname", + "jsonType.label": "String" + } + }, + { + "id": "319c76bc-93b5-4f78-b371-760f01143ffb", + "name": "profile", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "profile", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "profile", + "jsonType.label": "String" + } + }, + { + "id": "682141fe-c658-4a27-9c4f-d12cd6fd639d", + "name": "given name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "firstName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "given_name", + "jsonType.label": "String" + } + }, + { + "id": "31285f11-a0a0-4e95-a204-acda390961be", + "name": "gender", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "gender", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "gender", + "jsonType.label": "String" + } + }, + { + "id": "ee8c508a-f021-46c8-ba42-9f32b32aa504", + "name": "full name", + "protocol": "openid-connect", + "protocolMapper": "oidc-full-name-mapper", + "consentRequired": false, + "config": { + "id.token.claim": "true", + "access.token.claim": "true", + "userinfo.token.claim": "true" + } + }, + { + "id": "6aad901c-f1c7-46d8-bfc2-ddd17b37e73d", + "name": "family name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "lastName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "family_name", + "jsonType.label": "String" + } + }, + { + "id": "72f7e384-849a-475f-ad2d-0ea19319edd3", + "name": "updated at", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "updatedAt", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "updated_at", + "jsonType.label": "long" + } + } + ] + }, + { + "id": "204b6707-5432-43d6-928f-ed17669edc8e", + "name": "email", + "description": "OpenID Connect built-in scope: email", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${emailScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "6ffca153-999d-45dc-a98c-37ad1f79b04e", + "name": "email", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "email", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email", + "jsonType.label": "String" + } + }, + { + "id": "c8e75ea4-0006-4f7c-882c-8f1499bf14ca", + "name": "email verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "emailVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email_verified", + "jsonType.label": "boolean" + } + } + ] + }, + { + "id": "24c8b752-95fa-4b11-9c99-1da3d7028783", + "name": "address", + "description": "OpenID Connect built-in scope: address", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${addressScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "07b45364-f53c-453f-9d6f-1122586e8633", + "name": "address", + "protocol": "openid-connect", + "protocolMapper": "oidc-address-mapper", + "consentRequired": false, + "config": { + "user.attribute.formatted": "formatted", + "user.attribute.country": "country", + "user.attribute.postal_code": "postal_code", + "userinfo.token.claim": "true", + "user.attribute.street": "street", + "id.token.claim": "true", + "user.attribute.region": "region", + "access.token.claim": "true", + "user.attribute.locality": "locality" + } + } + ] + }, + { + "id": "e0c34249-b2e2-49cf-b414-9aac15acd82b", + "name": "microprofile-jwt", + "description": "Microprofile - JWT built-in scope", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "fcf43c71-170f-4c04-90b5-d6aefb16c7a5", + "name": "upn", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "upn", + "jsonType.label": "String" + } + }, + { + "id": "88097d77-681a-4b3a-a025-92fae6182a15", + "name": "groups", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "multivalued": "true", + "user.attribute": "foo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "groups", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "ac7c6a5d-9d21-4712-889f-951ba412ced0", + "name": "role_list", + "description": "SAML role list", + "protocol": "saml", + "attributes": { + "consent.screen.text": "${samlRoleListScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "9f08ee07-98a9-496d-b90c-897b38bd2dd3", + "name": "role list", + "protocol": "saml", + "protocolMapper": "saml-role-list-mapper", + "consentRequired": false, + "config": { + "single": "false", + "attribute.nameformat": "Basic", + "attribute.name": "Role" + } + } + ] + }, + { + "id": "6ba80bef-0a4f-4be1-9e34-89f1a9c8c976", + "name": "roles", + "description": "OpenID Connect scope for add user roles to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "true", + "consent.screen.text": "${rolesScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "9e774de8-10f9-4c1f-b70e-242f07532cf7", + "name": "realm roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "user.attribute": "foo", + "access.token.claim": "true", + "claim.name": "realm_access.roles", + "jsonType.label": "String", + "multivalued": "true" + } + }, + { + "id": "2086dce7-3707-4101-9174-7ca97082d228", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": {} + }, + { + "id": "7da3ae82-c74d-456e-a1f4-23f5d2ad6919", + "name": "client roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-client-role-mapper", + "consentRequired": false, + "config": { + "user.attribute": "foo", + "access.token.claim": "true", + "claim.name": "resource_access.${client_id}.roles", + "jsonType.label": "String", + "multivalued": "true" + } + } + ] + } + ], + "defaultDefaultClientScopes": [ + "role_list", + "profile", + "email", + "roles", + "web-origins", + "acr" + ], + "defaultOptionalClientScopes": [ + "offline_access", + "address", + "phone", + "microprofile-jwt" + ], + "browserSecurityHeaders": { + "contentSecurityPolicyReportOnly": "", + "xContentTypeOptions": "nosniff", + "xRobotsTag": "none", + "xFrameOptions": "SAMEORIGIN", + "contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", + "xXSSProtection": "1; mode=block", + "strictTransportSecurity": "max-age=31536000; includeSubDomains" + }, + "smtpServer": {}, + "eventsEnabled": false, + "eventsListeners": [ + "jboss-logging" + ], + "enabledEventTypes": [], + "adminEventsEnabled": false, + "adminEventsDetailsEnabled": false, + "identityProviders": [], + "identityProviderMappers": [], + "components": { + "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy": [ + { + "id": "fc6af199-36bb-4721-8805-65b15ac23677", + "name": "Full Scope Disabled", + "providerId": "scope", + "subType": "anonymous", + "subComponents": {}, + "config": {} + }, + { + "id": "4eca2c9d-3fd9-4798-a96f-66ab020f0999", + "name": "Allowed Client Scopes", + "providerId": "allowed-client-templates", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allow-default-scopes": [ + "true" + ] + } + }, + { + "id": "a6493944-7b42-47c9-8161-15bcce5e1dea", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "saml-user-attribute-mapper", + "oidc-full-name-mapper", + "saml-user-property-mapper", + "oidc-sha256-pairwise-sub-mapper", + "oidc-usermodel-property-mapper", + "oidc-usermodel-attribute-mapper", + "oidc-address-mapper", + "saml-role-list-mapper" + ] + } + }, + { + "id": "20f18ede-0082-4211-8193-97b3faf58832", + "name": "Trusted Hosts", + "providerId": "trusted-hosts", + "subType": "anonymous", + "subComponents": {}, + "config": { + "host-sending-registration-request-must-match": [ + "true" + ], + "client-uris-must-match": [ + "true" + ] + } + }, + { + "id": "59fd957f-035a-4381-ba69-b07befc54769", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "saml-user-attribute-mapper", + "oidc-usermodel-attribute-mapper", + "oidc-full-name-mapper", + "oidc-sha256-pairwise-sub-mapper", + "oidc-address-mapper", + "saml-role-list-mapper", + "saml-user-property-mapper", + "oidc-usermodel-property-mapper" + ] + } + }, + { + "id": "b5cf58aa-ebfe-4da0-adcf-ad3faee18502", + "name": "Allowed Client Scopes", + "providerId": "allowed-client-templates", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allow-default-scopes": [ + "true" + ] + } + }, + { + "id": "acec3143-8444-4d8f-9020-5fdd1992bfa8", + "name": "Consent Required", + "providerId": "consent-required", + "subType": "anonymous", + "subComponents": {}, + "config": {} + }, + { + "id": "6555ff94-661a-4659-9cde-0eb2147892ea", + "name": "Max Clients Limit", + "providerId": "max-clients", + "subType": "anonymous", + "subComponents": {}, + "config": { + "max-clients": [ + "200" + ] + } + } + ], + "org.keycloak.keys.KeyProvider": [ + { + "id": "f957405f-8f50-432a-8ade-8805d27e8b04", + "name": "rsa-generated", + "providerId": "rsa-generated", + "subComponents": {}, + "config": { + "privateKey": [ + "MIIEpAIBAAKCAQEA68PJqiC6cMTIaFvxsnhkyHXiOuwMxa3Udvu3lYJggl+3hMK9Pn1x7ojApUEch53qCIDLJADmr93aofdylZlLLdK/8vDViq8XWnodDfVDi6FttLhGhWiO7e37KswQ2wbosbQFbWIdU8brbsH1sm7snkwGTUuTXt3RDmWOlamomlbeumxNo9SaG+SSMdqwzB1KAu6IQwARvbA8LvzfMhlc6jh16o7CO1QTNFlbZ/8RDmu0T/pOF9TUccsw0hPiFbGzVy7+lANEVFXY9Un0sZTtb3xhJ6l9zsn07ZZEZ6D4COY0yGROWD5bvNUusZvDuzkg75myEHNDb4iZY0+DaiVRwQIDAQABAoIBABaWcUWOPCkzWTtSWm4tsFjaSRyPDUw1HW7G1VhgH4HuIgV4qNaKAIveEzg3QZX7+vY/QfccP6hoGhsRZit5/noQlRj1ROg0DTSQZXnhs8vMDALX8tMI6O9XsQwbyp5JY6rUI1RO+vNR/Vq6VMv4ak1iMupd4WvMgaEeTrz8/lfG6BxBcCGx8Wpvxsd9VvcAyPqXwI2C5y0JSnOJIHRxvLr2NmQeFdFppcVlucQV1SCzxnbMRcI5i0RKuOo/9yJJForwFJcFzkAxWgeUnJglNw/C/20s0aKwM+z+mBAFK/uZPjvEVcbTXj3KM1iuWs5l6FW03YyI5xO5EC2fRUmUKWcCgYEA+Yx2t6GSQ6YJ/v4DhaZv2rzEtn5G9Nk4J98w2GiQAjW4SBRp+vaWrmWhvkvrpX6lp741k9jK6CViayEE05orEHmVjHM885AiddQc88zxtCpGijMR1FuVdklswd0LnyB/PLWgr3f7Z1qAYyFCbxnpcg76jpQCpo36h40KEOO80BcCgYEA8dwZ1/Vwq6UDJBvM9QBvjOnW7Jeoico2m2Hsd74v9G1si+yywyvIMphVnlqoHsXqdxi79ZHJXiyfB/zmQWO9dEhcxq2OubaemDzeJLwXRcLW+Cg4VNIirDdZEWo/nhD7P+00IP/y8Z5neguWYVc01sLkCGB46z8Q/8R59xuZ++cCgYEA0/F9framD/h8MuqwORnDlEaQ1+HmB9xZOlvwE0yzSn0vl2BnJnO6REIjHglDCVrH/PCqdnhA1OuzbAMuIz2j56kr346cLMy0x9gwAsyEWB0zrfpz4SUrirwPt5MyZKLoDbrAz2aaygvuUMMVtmCOiYW5PdDtc2HQbsHV08RoP18CgYB09fi1fCcxioobUypprP1FCux529mQUO7Zc6CUQ7ATJzuf6yaDxc950DtPag31W8bIM3jqB8d2uGNrzHxZUO+UpU3gcpwb6VmGy6Ct6RvkC5ZDycd8FWbZG6cCCfyb5yBpyL812jDVccIevi3KAw81cGgwON8g/I2u8of83Sc5LwKBgQCtVDV1OHE24Y2DxRAakZ5fMp3NYIE8Y+5XrhwHdWRa72AaAz7DrU12b7yZJH5y/upV4Gv12Ia1o1pAx9mXSnVb0WSHp1A8UNtCVlivtnr66BGb5X5ucw53Wz+ciL3KgoHm5pU7Q/6UxttWyikvF2uBp6xMBWOcv3b1f6erxcw2aQ==" + ], + "keyUse": [ + "SIG" + ], + "certificate": [ + "MIICoTCCAYkCBgGGVKQ/mjANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDDAlkZW1vcmVhbG0wHhcNMjMwMjE1MTAzMzE1WhcNMzMwMjE1MTAzNDU1WjAUMRIwEAYDVQQDDAlkZW1vcmVhbG0wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDrw8mqILpwxMhoW/GyeGTIdeI67AzFrdR2+7eVgmCCX7eEwr0+fXHuiMClQRyHneoIgMskAOav3dqh93KVmUst0r/y8NWKrxdaeh0N9UOLoW20uEaFaI7t7fsqzBDbBuixtAVtYh1TxutuwfWybuyeTAZNS5Ne3dEOZY6VqaiaVt66bE2j1Job5JIx2rDMHUoC7ohDABG9sDwu/N8yGVzqOHXqjsI7VBM0WVtn/xEOa7RP+k4X1NRxyzDSE+IVsbNXLv6UA0RUVdj1SfSxlO1vfGEnqX3OyfTtlkRnoPgI5jTIZE5YPlu81S6xm8O7OSDvmbIQc0NviJljT4NqJVHBAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAHTSxIteVa2a1V4pqCHweJTRxoXOGE6048ZtEzOsO/5xElynszVZ1iMXlRq8uNTlc80Icx4hw17attP7KCColFIwZ0HCq1IfLQEquoHBaLDhIhVQ7ehCTkto/u8nLgfZL+NDkO+nT+i5eNctMQG74x6hTV3Urc5z4MnBtJNqtPvR+Lw0pbtH07f+K345Z06nxES0Hnb6TLsw8BuTTKPfSqVIAbsi4n3pUgULdeVRd0iYaSQdmwRaFEntWzUJlM4GjvaIPkaMS1CwjEBuutLzskVSiFlQ26W8KwXn7ySIBlec4ox3VD+Q7YynR64bB6Hs1MmFFuooTjn4c02uytLmK2o=" + ], + "priority": [ + "100" + ] + } + }, + { + "id": "d0d08daf-ba76-4e82-8183-37aa8f9a6af1", + "name": "rsa-enc-generated", + "providerId": "rsa-enc-generated", + "subComponents": {}, + "config": { + "privateKey": [ + "MIIEpAIBAAKCAQEA6wG/F8mo6eXBXSuShH9D2ybnMNUfh9dbRJ+Bporl/XNOSxBKaiyo+2W1VIdrjsedwmQuQoFjs2f229qzBS9Fl4ChLxlIjJEW1U+bPilRZUDlSM3q2HE6O4acP+nSIG8ZW9/KhXPgcanM+MixkM9cHzO7bqGKKcnCKD5Y8ia2Vc1PXPNguxmDwuj9XSlpdDOzRZiMUZzcqxzxWgyfUTD839NN16AM6tN4CBIuK779Zwah0ym4wLiVdX2UCJNVzVzuo16YoXYOlXC+skiWG+M4pjj3/IKkLQvzpO9Zh1x6NNMsstGQhPSRUq4KX4fquQVDwxDvIzsIWn2pcTmkOsdWSwIDAQABAoIBADCrTWx7PoimJOwLPI5FHwPxZBrIYH3M+2FUWVDo3iWlrha8mnSvqBVcZHfLjdple8YI4k2ypze99bFlcwLFXfe402jCJzS5TY3CrUdr3igGjxWLU7IcjO9L+ur/nR1LdOiCidome9p+TG1Pfvqa/xyVJaGNQeRSnOuhseEAZG2TMEidK0nWI1NQdm5Q6hsNI2UcPbq3+0ouHz2xBYGoKln76Ibqk5yfNty60jtxeQwg1P0I5PjiyLIiOQUKzaGEX6NENr/WI0qXLr5460ioCqI3Urrgml+lDij/Tg5BXs/Dd1awbup3/vUId/gd7Hy99tXDOK0iXyjZThfgBhsBiYUCgYEA/5zJhrn3g3YyFhPljP4xQBP3k4f/sPBihNwshsa/P8DBIFvxBE3jL+u8QZ/d1PU935gq4183HT8Nk7DFoQ0pntdLyJxfZ5oF07d0qYoHY++R5OIqX2VK2AXBGbwuCUct/XXjEUn83MSBgg1GGbY6qyty1UwBqQhN+qqiOxcUfS0CgYEA61z2IBNOs0T+ns4SsLZERM7uu2mV0IokaLQ60Rbk+7+sV+Mx0eapp4rXuxE33YFRrsvHonyfvbNZtqBnyw7fU8PQm5ozl9T1jeUAdgqm8l5cqpYswASL/NkWLkK3+D+ynLLjKygDZqMj3ZMO8c7nYw799QQaw4T9pzpJZjJLfFcCgYEArOk11kKEsdRJy2+IMBlf3ZXkO1ObXukt6+w43q8hfpH40tf/MUcy8R7JiacIW9/ODCwWjxrA4LLfj1HcTrblucKwTDOjwiSJ3o9ShsGNgEf2bFumCEQwHfO+jZcjmTkiXjvZ778aI4l2hjBOhGQmSdYpZyp0URECFxhIiCpzvL0CgYAYrtodCQlS4aR2URRCtgq40J7Wxr7wbNxeorAcZ3NCN5rCaNA7vB4EtRnkw2yBbWN8mmBoWPuDsIBzF6Vq9TdUmI+TEfvhK3NJG0AOIRXbCyxas38j8BYiQT4DQfn7Ler0ZgpO51Zb+DX1sct6boFzsQnPHUwVPyg+1m0GK7Yg5wKBgQDCIYvL6JhJidgP9ve3U0d0NUDAndRE5fuGqMcKvBiiRAE2D8RxHIWDre/NzrSLAst4UzmuPLG1LB1mIIfUHoPlHX5yEHbBRWtKeJUZ8tEU2z6uNZxP+ntf/K8fdkBS0jbowfG2G/nqqlyJuIrQduNvtT6aoZK0UjRtZ7iAdMjs5Q==" + ], + "keyUse": [ + "ENC" + ], + "certificate": [ + "MIICoTCCAYkCBgGGVKRAEDANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDDAlkZW1vcmVhbG0wHhcNMjMwMjE1MTAzMzE1WhcNMzMwMjE1MTAzNDU1WjAUMRIwEAYDVQQDDAlkZW1vcmVhbG0wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDrAb8Xyajp5cFdK5KEf0PbJucw1R+H11tEn4GmiuX9c05LEEpqLKj7ZbVUh2uOx53CZC5CgWOzZ/bb2rMFL0WXgKEvGUiMkRbVT5s+KVFlQOVIzerYcTo7hpw/6dIgbxlb38qFc+Bxqcz4yLGQz1wfM7tuoYopycIoPljyJrZVzU9c82C7GYPC6P1dKWl0M7NFmIxRnNyrHPFaDJ9RMPzf003XoAzq03gIEi4rvv1nBqHTKbjAuJV1fZQIk1XNXO6jXpihdg6VcL6ySJYb4zimOPf8gqQtC/Ok71mHXHo00yyy0ZCE9JFSrgpfh+q5BUPDEO8jOwhafalxOaQ6x1ZLAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAEkOE5zWi8x3NfPWsdZkLckRw9vQFDYhQuaRZfD1i3lKvYW9xERFjOICuSHUX9yxX/CpGWVDAomXXXYxvlPpkENO/GjUGcL5UH6AopPkOzUZwsVWEylR6nczlK9sXjfjqgDOFwV19q5bn6hiV6qLG4Ty747lCATaxCUDPwJvZE8ZLFTHrccFtAH8VXCZbJJMLXO42LgMeG5nF4Tf1K0iI+4sSDWshl+QNugmaRQSrG0IklH4+U9J9JTwAiibkspgVsc2ubhbA+Xvzndg0vIQ2cEVWLqK9tRAIuaoANTmJneBZdBoVL5gtY3jCyXj6ecrKr+4JzeXCwmrxQUk9061eDU=" + ], + "priority": [ + "100" + ], + "algorithm": [ + "RSA-OAEP" + ] + } + }, + { + "id": "0d0237f6-29a9-4e1d-8de0-0490ab1c9118", + "name": "hmac-generated", + "providerId": "hmac-generated", + "subComponents": {}, + "config": { + "kid": [ + "1523f166-af8b-4970-94ff-eabb0f391532" + ], + "secret": [ + "ByJpObMtnXpqZwygKnj_cgP-SkhVJPH_D-vgOS8yRSH9Km9idi4Ryh5w__zKGk4OeIJWQWRSz5VG_6kzINchKw" + ], + "priority": [ + "100" + ], + "algorithm": [ + "HS256" + ] + } + }, + { + "id": "53a26567-105e-4b11-bbd1-1516fe76104a", + "name": "aes-generated", + "providerId": "aes-generated", + "subComponents": {}, + "config": { + "kid": [ + "8d1e845c-f3bc-4b6c-86c5-acbc49442386" + ], + "secret": [ + "EWEleDkIM15zP8tyIr6nHw" + ], + "priority": [ + "100" + ] + } + } + ] + }, + "internationalizationEnabled": false, + "supportedLocales": [], + "authenticationFlows": [ + { + "id": "c3820085-85d3-4ff8-bd42-15464fa668b6", + "alias": "Account verification options", + "description": "Method with which to verity the existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-email-verification", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Verify Existing Account by Re-authentication", + "userSetupAllowed": false + } + ] + }, + { + "id": "815709b3-7421-44b3-bf18-4838cf354852", + "alias": "Authentication Options", + "description": "Authentication options.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "basic-auth", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "basic-auth-otp", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-spnego", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 30, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "6b97ee19-cc94-435d-92cd-c16cec1ce04e", + "alias": "Browser - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-otp-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "8ce4d49e-4ef9-4f99-ab46-3693b7b4a093", + "alias": "Direct Grant - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "direct-grant-validate-otp", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "2d7a3dd7-cfa2-427c-b1c6-aa91a4cd6c63", + "alias": "First broker login - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-otp-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "bf73abb4-36f2-4338-8578-86ab06bb1131", + "alias": "Handle Existing Account", + "description": "Handle what to do if there is existing account with same email/username like authenticated identity provider", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-confirm-link", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Account verification options", + "userSetupAllowed": false + } + ] + }, + { + "id": "65caf860-42ee-4bbe-aafe-468c838cf602", + "alias": "Reset - Conditional OTP", + "description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-otp", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "bf7960c0-0247-442b-b381-bea5646a4912", + "alias": "User creation or linking", + "description": "Flow for the existing/non-existing user alternatives", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "create unique user config", + "authenticator": "idp-create-user-if-unique", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Handle Existing Account", + "userSetupAllowed": false + } + ] + }, + { + "id": "1f2b94c0-0f10-4484-a224-0699141f7c02", + "alias": "Verify Existing Account by Re-authentication", + "description": "Reauthentication of existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-username-password-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "First broker login - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "a34173a7-4ad7-4817-ad11-9ddc459854aa", + "alias": "browser", + "description": "browser based authentication", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-cookie", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-spnego", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "identity-provider-redirector", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 25, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 30, + "autheticatorFlow": true, + "flowAlias": "forms", + "userSetupAllowed": false + } + ] + }, + { + "id": "a69e205f-6ae1-4aa1-9ed1-806dbc5f7168", + "alias": "clients", + "description": "Base authentication for clients", + "providerId": "client-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "client-secret", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-jwt", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-secret-jwt", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 30, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-x509", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 40, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "18b4d1ed-9fcd-41b2-8db0-cdc74d9ad155", + "alias": "direct grant", + "description": "OpenID Connect Resource Owner Grant", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "direct-grant-validate-username", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "direct-grant-validate-password", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 30, + "autheticatorFlow": true, + "flowAlias": "Direct Grant - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "7aeeddfa-01fa-43c2-80ad-3d7089b8d952", + "alias": "docker auth", + "description": "Used by Docker clients to authenticate against the IDP", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "docker-http-basic-authenticator", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "4e0d5e38-0c28-4ad3-b904-e1dcde038e7f", + "alias": "first broker login", + "description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "review profile config", + "authenticator": "idp-review-profile", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "User creation or linking", + "userSetupAllowed": false + } + ] + }, + { + "id": "c14d77a8-4c9f-429d-9d75-fd5d5046e140", + "alias": "forms", + "description": "Username, password, otp and other auth forms.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-username-password-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Browser - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "e58d48bb-d404-40d9-9cfb-5fa5d450fef1", + "alias": "http challenge", + "description": "An authentication flow based on challenge-response HTTP Authentication Schemes", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "no-cookie-redirect", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Authentication Options", + "userSetupAllowed": false + } + ] + }, + { + "id": "287e7e9a-1b9a-40b8-9d96-2718f336c12a", + "alias": "registration", + "description": "registration flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-page-form", + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": true, + "flowAlias": "registration form", + "userSetupAllowed": false + } + ] + }, + { + "id": "bad61d64-c8fb-4dad-b8fe-ce25da2de253", + "alias": "registration form", + "description": "registration form", + "providerId": "form-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-user-creation", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "registration-profile-action", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 40, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "registration-password-action", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 50, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "registration-recaptcha-action", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 60, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "68449dd8-7be7-4150-82fe-fb75b67c896e", + "alias": "reset credentials", + "description": "Reset credentials for a user if they forgot their password or something", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "reset-credentials-choose-user", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-credential-email", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-password", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 30, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 40, + "autheticatorFlow": true, + "flowAlias": "Reset - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "baded0d1-49db-4723-8924-19f1c6e4b0d3", + "alias": "saml ecp", + "description": "SAML ECP Profile Authentication Flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "http-basic-authenticator", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + } + ], + "authenticatorConfig": [ + { + "id": "c8e1e585-b8bb-417c-943d-079afbae72c5", + "alias": "create unique user config", + "config": { + "require.password.update.after.registration": "false" + } + }, + { + "id": "07e5d37a-e1ed-4957-9802-f02b8565c715", + "alias": "review profile config", + "config": { + "update.profile.on.first.login": "missing" + } + } + ], + "requiredActions": [ + { + "alias": "CONFIGURE_TOTP", + "name": "Configure OTP", + "providerId": "CONFIGURE_TOTP", + "enabled": true, + "defaultAction": false, + "priority": 10, + "config": {} + }, + { + "alias": "terms_and_conditions", + "name": "Terms and Conditions", + "providerId": "terms_and_conditions", + "enabled": false, + "defaultAction": false, + "priority": 20, + "config": {} + }, + { + "alias": "UPDATE_PASSWORD", + "name": "Update Password", + "providerId": "UPDATE_PASSWORD", + "enabled": true, + "defaultAction": false, + "priority": 30, + "config": {} + }, + { + "alias": "UPDATE_PROFILE", + "name": "Update Profile", + "providerId": "UPDATE_PROFILE", + "enabled": true, + "defaultAction": false, + "priority": 40, + "config": {} + }, + { + "alias": "VERIFY_EMAIL", + "name": "Verify Email", + "providerId": "VERIFY_EMAIL", + "enabled": true, + "defaultAction": false, + "priority": 50, + "config": {} + }, + { + "alias": "delete_account", + "name": "Delete Account", + "providerId": "delete_account", + "enabled": false, + "defaultAction": false, + "priority": 60, + "config": {} + }, + { + "alias": "webauthn-register", + "name": "Webauthn Register", + "providerId": "webauthn-register", + "enabled": true, + "defaultAction": false, + "priority": 70, + "config": {} + }, + { + "alias": "webauthn-register-passwordless", + "name": "Webauthn Register Passwordless", + "providerId": "webauthn-register-passwordless", + "enabled": true, + "defaultAction": false, + "priority": 80, + "config": {} + }, + { + "alias": "update_user_locale", + "name": "Update User Locale", + "providerId": "update_user_locale", + "enabled": true, + "defaultAction": false, + "priority": 1000, + "config": {} + } + ], + "browserFlow": "browser", + "registrationFlow": "registration", + "directGrantFlow": "direct grant", + "resetCredentialsFlow": "reset credentials", + "clientAuthenticationFlow": "clients", + "dockerAuthenticationFlow": "docker auth", + "attributes": { + "cibaBackchannelTokenDeliveryMode": "poll", + "cibaExpiresIn": "120", + "cibaAuthRequestedUserHint": "login_hint", + "oauth2DeviceCodeLifespan": "600", + "oauth2DevicePollingInterval": "5", + "parRequestUriLifespan": "60", + "cibaInterval": "5", + "realmReusableOtpCode": "false" + }, + "keycloakVersion": "20.0.3", + "userManagedAccessAllowed": false, + "clientProfiles": { + "profiles": [] + }, + "clientPolicies": { + "policies": [] + } +} diff --git a/src/test/java/de/adorsys/keycloak/config/configuration/NormalizeTestConfiguration.java b/src/test/java/de/adorsys/keycloak/config/configuration/NormalizeTestConfiguration.java new file mode 100644 index 000000000..a27f1e142 --- /dev/null +++ b/src/test/java/de/adorsys/keycloak/config/configuration/NormalizeTestConfiguration.java @@ -0,0 +1,35 @@ +/*- + * ---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.configuration; + +import de.adorsys.keycloak.config.properties.NormalizationConfigProperties; +import de.adorsys.keycloak.config.properties.NormalizationKeycloakConfigProperties; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ComponentScan(basePackages = {"de.adorsys.keycloak.config"}) +@EnableConfigurationProperties({NormalizationKeycloakConfigProperties.class, NormalizationConfigProperties.class}) +@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "NORMALIZE") +public class NormalizeTestConfiguration { +} diff --git a/src/test/java/de/adorsys/keycloak/config/configuration/TestConfiguration.java b/src/test/java/de/adorsys/keycloak/config/configuration/TestConfiguration.java index 27685c07a..258f4b868 100644 --- a/src/test/java/de/adorsys/keycloak/config/configuration/TestConfiguration.java +++ b/src/test/java/de/adorsys/keycloak/config/configuration/TestConfiguration.java @@ -22,6 +22,7 @@ import de.adorsys.keycloak.config.properties.ImportConfigProperties; import de.adorsys.keycloak.config.properties.KeycloakConfigProperties; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; @@ -29,6 +30,7 @@ @Configuration @ComponentScan(basePackages = {"de.adorsys.keycloak.config"}) @EnableConfigurationProperties({KeycloakConfigProperties.class, ImportConfigProperties.class}) +@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "IMPORT", matchIfMissing = true) public class TestConfiguration { } diff --git a/src/test/java/de/adorsys/keycloak/config/normalize/AbstractNormalizeTest.java b/src/test/java/de/adorsys/keycloak/config/normalize/AbstractNormalizeTest.java new file mode 100644 index 000000000..be0412dee --- /dev/null +++ b/src/test/java/de/adorsys/keycloak/config/normalize/AbstractNormalizeTest.java @@ -0,0 +1,47 @@ +/*- + * ---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.normalize; + +import de.adorsys.keycloak.config.configuration.NormalizeTestConfiguration; +import de.adorsys.keycloak.config.extensions.GithubActionsExtension; +import org.junit.jupiter.api.*; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.boot.test.context.ConfigDataApplicationContextInitializer; +import org.springframework.boot.test.system.OutputCaptureExtension; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static java.util.concurrent.TimeUnit.SECONDS; + +@ExtendWith(SpringExtension.class) +@ExtendWith(GithubActionsExtension.class) +@ExtendWith(OutputCaptureExtension.class) +@ContextConfiguration( + classes = {NormalizeTestConfiguration.class}, + initializers = {ConfigDataApplicationContextInitializer.class} +) +@ActiveProfiles("normalize-IT") +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +@TestClassOrder(ClassOrderer.OrderAnnotation.class) +@Timeout(value = 30, unit = SECONDS) +public abstract class AbstractNormalizeTest { +} diff --git a/src/test/java/de/adorsys/keycloak/config/normalize/DummyTest.java b/src/test/java/de/adorsys/keycloak/config/normalize/DummyTest.java new file mode 100644 index 000000000..2c3a7a520 --- /dev/null +++ b/src/test/java/de/adorsys/keycloak/config/normalize/DummyTest.java @@ -0,0 +1,31 @@ +/*- + * ---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.normalize; + +import org.junit.jupiter.api.Test; + +public class DummyTest extends AbstractNormalizeTest { + + @Test + void test() { + + } +} diff --git a/src/test/java/de/adorsys/keycloak/config/service/normalize/AuthFlowNormalizationServiceConfigIT.java b/src/test/java/de/adorsys/keycloak/config/service/normalize/AuthFlowNormalizationServiceConfigIT.java new file mode 100644 index 000000000..d9a2ef250 --- /dev/null +++ b/src/test/java/de/adorsys/keycloak/config/service/normalize/AuthFlowNormalizationServiceConfigIT.java @@ -0,0 +1,81 @@ +package de.adorsys.keycloak.config.service.normalize; + +import de.adorsys.keycloak.config.normalize.AbstractNormalizeTest; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.keycloak.representations.idm.AuthenticationExecutionExportRepresentation; +import org.keycloak.representations.idm.AuthenticationFlowRepresentation; +import org.keycloak.representations.idm.AuthenticatorConfigRepresentation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.system.CapturedOutput; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +class AuthFlowNormalizationServiceConfigIT extends AbstractNormalizeTest { + + @Autowired + AuthFlowNormalizationService service; + + @Test + public void testNormalizeAuthConfigsWithEmptyListsIsNull() { + var resultingAuthConfig = service.normalizeAuthConfig(new ArrayList<>(), new ArrayList<>()); + Assertions.assertThat(resultingAuthConfig).isNull(); + } + + @Test + public void testNormalizeAuthConfigsAreRemovedWithoutAliasReference(CapturedOutput output) { + AuthenticatorConfigRepresentation authenticatorConfigRepresentation = new AuthenticatorConfigRepresentation(); + authenticatorConfigRepresentation.setAlias("config2"); + + AuthenticationFlowRepresentation authenticationFlowRepresentation = new AuthenticationFlowRepresentation(); + + AuthenticationExecutionExportRepresentation authenticationExecutionExportRepresentation = new AuthenticationExecutionExportRepresentation(); + authenticationExecutionExportRepresentation.setAuthenticatorConfig("config1"); + authenticationFlowRepresentation.setAuthenticationExecutions(List.of(authenticationExecutionExportRepresentation)); + + var resultingAuthConfig = service.normalizeAuthConfig(List.of(authenticatorConfigRepresentation), List.of(authenticationFlowRepresentation)); + + Assertions.assertThat(resultingAuthConfig).isNull(); + Assertions.assertThat(output.getOut()).contains("Some authenticator configs are unused."); + } + + @Test + public void testNormalizeAuthConfigsRemainWithAliasReference() { + AuthenticatorConfigRepresentation authenticatorConfigRepresentation = new AuthenticatorConfigRepresentation(); + authenticatorConfigRepresentation.setAlias("config1"); + + AuthenticationFlowRepresentation authenticationFlowRepresentation = new AuthenticationFlowRepresentation(); + + AuthenticationExecutionExportRepresentation authenticationExecutionExportRepresentation = new AuthenticationExecutionExportRepresentation(); + authenticationExecutionExportRepresentation.setAuthenticatorConfig("config1"); + authenticationFlowRepresentation.setAuthenticationExecutions(List.of(authenticationExecutionExportRepresentation)); + + var resultingAuthConfig = service.normalizeAuthConfig(List.of(authenticatorConfigRepresentation), List.of(authenticationFlowRepresentation)); + + Assertions.assertThat(resultingAuthConfig).containsExactlyInAnyOrder(authenticatorConfigRepresentation); + } + + @Test + public void testNormalizeAuthConfigsCheckedForDuplicates(CapturedOutput output) { + AuthenticatorConfigRepresentation authenticatorConfigRepresentation1 = new AuthenticatorConfigRepresentation(); + authenticatorConfigRepresentation1.setId(UUID.randomUUID().toString()); + authenticatorConfigRepresentation1.setAlias("config1"); + + AuthenticatorConfigRepresentation authenticatorConfigRepresentation2 = new AuthenticatorConfigRepresentation(); + authenticatorConfigRepresentation2.setId(UUID.randomUUID().toString()); + authenticatorConfigRepresentation2.setAlias("config1"); + + AuthenticationFlowRepresentation authenticationFlowRepresentation = new AuthenticationFlowRepresentation(); + + AuthenticationExecutionExportRepresentation authenticationExecutionExportRepresentation = new AuthenticationExecutionExportRepresentation(); + authenticationExecutionExportRepresentation.setAuthenticatorConfig("config1"); + authenticationFlowRepresentation.setAuthenticationExecutions(List.of(authenticationExecutionExportRepresentation)); + + var resultingAuthConfig = service.normalizeAuthConfig(List.of(authenticatorConfigRepresentation1, authenticatorConfigRepresentation2), List.of(authenticationFlowRepresentation)); + + Assertions.assertThat(resultingAuthConfig).containsExactlyInAnyOrder(authenticatorConfigRepresentation1, authenticatorConfigRepresentation2); + Assertions.assertThat(output.getOut()).contains("The following authenticator configs are duplicates: [config1]"); + } +} diff --git a/src/test/java/de/adorsys/keycloak/config/service/normalize/AuthFlowNormalizationServiceFlowIT.java b/src/test/java/de/adorsys/keycloak/config/service/normalize/AuthFlowNormalizationServiceFlowIT.java new file mode 100644 index 000000000..a06f6a621 --- /dev/null +++ b/src/test/java/de/adorsys/keycloak/config/service/normalize/AuthFlowNormalizationServiceFlowIT.java @@ -0,0 +1,53 @@ +package de.adorsys.keycloak.config.service.normalize; + +import de.adorsys.keycloak.config.normalize.AbstractNormalizeTest; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.keycloak.representations.idm.AuthenticationFlowRepresentation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.system.CapturedOutput; + +import java.util.ArrayList; +import java.util.List; + +class AuthFlowNormalizationServiceFlowIT extends AbstractNormalizeTest { + + @Autowired + AuthFlowNormalizationService service; + + + @Test + public void testNormalizeAuthFlows() { + var resultingAuthFlows = service.normalizeAuthFlows(new ArrayList<>(), new ArrayList<>()); + Assertions.assertThat(resultingAuthFlows).isNull(); + } + + @Test + public void testNormalizeAuthFlowsIgnoreBuiltInTrue() { + AuthenticationFlowRepresentation authenticationFlowRepresentation = new AuthenticationFlowRepresentation(); + authenticationFlowRepresentation.setBuiltIn(true); + + AuthenticationFlowRepresentation authenticationFlowRepresentationBaseline = new AuthenticationFlowRepresentation(); + authenticationFlowRepresentationBaseline.setBuiltIn(true); + + var resultingAuthFlows = service.normalizeAuthFlows(List.of(authenticationFlowRepresentation), List.of(authenticationFlowRepresentationBaseline)); + + Assertions.assertThat(resultingAuthFlows).isNull(); + } + + @Test + public void testNormalizeAuthFlowsIgnoreBuiltInTrueButBaselineHasEntryCreatesRecreationWarning(CapturedOutput output) { + AuthenticationFlowRepresentation authenticationFlowRepresentation = new AuthenticationFlowRepresentation(); + authenticationFlowRepresentation.setBuiltIn(true); + + AuthenticationFlowRepresentation authenticationFlowRepresentationBaseline = new AuthenticationFlowRepresentation(); + authenticationFlowRepresentationBaseline.setBuiltIn(false); + authenticationFlowRepresentationBaseline.setAlias("flow1"); + + var resultingAuthFlows = service.normalizeAuthFlows(List.of(authenticationFlowRepresentation), List.of(authenticationFlowRepresentationBaseline)); + + Assertions.assertThat(resultingAuthFlows).isNull(); + Assertions.assertThat(output.getOut()).contains("Default realm authentication flow 'flow1' was deleted in exported realm. It may be reintroduced during import"); + + } +} diff --git a/src/test/java/de/adorsys/keycloak/config/test/util/KeycloakAuthentication.java b/src/test/java/de/adorsys/keycloak/config/test/util/KeycloakAuthentication.java index b29f4851c..52e26edc7 100644 --- a/src/test/java/de/adorsys/keycloak/config/test/util/KeycloakAuthentication.java +++ b/src/test/java/de/adorsys/keycloak/config/test/util/KeycloakAuthentication.java @@ -24,9 +24,11 @@ import org.keycloak.admin.client.Keycloak; import org.keycloak.representations.AccessTokenResponse; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Component; @Component +@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "IMPORT", matchIfMissing = true) public class KeycloakAuthentication { private final KeycloakConfigProperties keycloakConfigProperties; diff --git a/src/test/java/de/adorsys/keycloak/config/test/util/KeycloakRepository.java b/src/test/java/de/adorsys/keycloak/config/test/util/KeycloakRepository.java index 123bbefc7..d3ea4269c 100644 --- a/src/test/java/de/adorsys/keycloak/config/test/util/KeycloakRepository.java +++ b/src/test/java/de/adorsys/keycloak/config/test/util/KeycloakRepository.java @@ -24,6 +24,7 @@ import org.keycloak.admin.client.resource.UserResource; import org.keycloak.representations.idm.*; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Component; import java.util.List; @@ -35,6 +36,7 @@ import static org.hamcrest.Matchers.hasSize; @Component +@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "IMPORT", matchIfMissing = true) public class KeycloakRepository { private final KeycloakProvider keycloakProvider; diff --git a/src/test/resources/application-normalize-IT.properties b/src/test/resources/application-normalize-IT.properties new file mode 100644 index 000000000..77a36297e --- /dev/null +++ b/src/test/resources/application-normalize-IT.properties @@ -0,0 +1,3 @@ +run.operation=normalize +normalization.files.input-locations=default +normalization.files.output-directory=default-output