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