From d1e21bd81a2a42307595cdf8c174cb3399a805b0 Mon Sep 17 00:00:00 2001 From: Nkwenti-Severian-Ndongtsop Date: Wed, 25 Feb 2026 07:56:53 +0100 Subject: [PATCH 01/15] Fix YAML codePointLimit test --- .../keycloak/config/provider/KeycloakExportProviderTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/java/de/adorsys/keycloak/config/provider/KeycloakExportProviderTest.java b/src/test/java/de/adorsys/keycloak/config/provider/KeycloakExportProviderTest.java index 512b3a860..637e34dbf 100644 --- a/src/test/java/de/adorsys/keycloak/config/provider/KeycloakExportProviderTest.java +++ b/src/test/java/de/adorsys/keycloak/config/provider/KeycloakExportProviderTest.java @@ -77,6 +77,7 @@ void setUp() { lenient().when(patternResolver.getPathMatcher()).thenReturn(pathMatcher); lenient().when(filesProperties.getExcludes()).thenReturn(Collections.emptyList()); lenient().when(filesProperties.isIncludeHiddenFiles()).thenReturn(false); + lenient().when(filesProperties.getCodePointLimit()).thenReturn(500 * 1024 * 1024); exportProvider = new KeycloakExportProvider(patternResolver, normalizationConfigProperties); } From d5edfa22aa88e79d63ea42a7e7d5f933ce0a5003 Mon Sep 17 00:00:00 2001 From: Nkwenti-Severian-Ndongtsop Date: Wed, 25 Feb 2026 08:10:37 +0100 Subject: [PATCH 02/15] docs: update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4588efda1..e946f3d04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - Fix Keycloak compatibility by stripping `clientProfiles` and `clientPolicies` from top-level realm updates - Improve idempotency for OTP policy, state, and checksum updates to avoid redundant realm updates - Fix issue where empty or null composite realm roles were not being cleared during import +- Increase code point limit to 500MB for import and normalization processes ## [6.4.1] - 2026-01-28 From bd61793d096034fbc43a4c350a5761d81a9ff018 Mon Sep 17 00:00:00 2001 From: Nkwenti-Severian-Ndongtsop Date: Wed, 25 Feb 2026 12:55:28 +0100 Subject: [PATCH 03/15] test: add user update ignored props json --- .../00_create_realm_with_user.json | 54 ++++++++++++++++ ...01_update_user_roles_try_change_email.json | 63 +++++++++++++++++++ 2 files changed, 117 insertions(+) create mode 100644 src/test/resources/import-files/user-update-ignored-properties/00_create_realm_with_user.json create mode 100644 src/test/resources/import-files/user-update-ignored-properties/01_update_user_roles_try_change_email.json diff --git a/src/test/resources/import-files/user-update-ignored-properties/00_create_realm_with_user.json b/src/test/resources/import-files/user-update-ignored-properties/00_create_realm_with_user.json new file mode 100644 index 000000000..99428844e --- /dev/null +++ b/src/test/resources/import-files/user-update-ignored-properties/00_create_realm_with_user.json @@ -0,0 +1,54 @@ +{ + "enabled": true, + "realm": "realmUserUpdateIgnoredProps", + "roles": { + "realm": [ + { + "name": "role_a", + "description": "Role A", + "composite": false, + "clientRole": false + }, + { + "name": "role_b", + "description": "Role B", + "composite": false, + "clientRole": false + } + ], + "client": { + "test-client": [ + { + "name": "client_role_a", + "description": "Client Role A", + "composite": false, + "clientRole": true + } + ] + } + }, + "clients": [ + { + "clientId": "test-client", + "name": "test-client", + "enabled": true, + "clientAuthenticatorType": "client-secret", + "secret": "my-special-client-secret", + "redirectUris": [ + "*" + ], + "webOrigins": [ + "*" + ] + } + ], + "users": [ + { + "username": "ldapuser", + "email": "ldapuser@old.example", + "enabled": true, + "firstName": "LDAP", + "lastName": "User" + } + ] +} diff --git a/src/test/resources/import-files/user-update-ignored-properties/01_update_user_roles_try_change_email.json b/src/test/resources/import-files/user-update-ignored-properties/01_update_user_roles_try_change_email.json new file mode 100644 index 000000000..d598bbd35 --- /dev/null +++ b/src/test/resources/import-files/user-update-ignored-properties/01_update_user_roles_try_change_email.json @@ -0,0 +1,63 @@ +{ + "enabled": true, + "realm": "realmUserUpdateIgnoredProps", + "roles": { + "realm": [ + { + "name": "role_a", + "description": "Role A", + "composite": false, + "clientRole": false + }, + { + "name": "role_b", + "description": "Role B", + "composite": false, + "clientRole": false + } + ], + "client": { + "test-client": [ + { + "name": "client_role_a", + "description": "Client Role A", + "composite": false, + "clientRole": true + } + ] + } + }, + "clients": [ + { + "clientId": "test-client", + "name": "test-client", + "enabled": true, + "clientAuthenticatorType": "client-secret", + "secret": "my-special-client-secret", + "redirectUris": [ + "*" + ], + "webOrigins": [ + "*" + ] + } + ], + "users": [ + { + "username": "ldapuser", + "email": "ldapuser@new.example", + "enabled": true, + "firstName": "LDAP", + "lastName": "User", + "realmRoles": [ + "role_a", + "role_b" + ], + "clientRoles": { + "test-client": [ + "client_role_a" + ] + } + } + ] +} From 12e1967bfb951a4f74b084d6949491f4e2a9b560 Mon Sep 17 00:00:00 2001 From: Nkwenti-Severian-Ndongtsop Date: Wed, 25 Feb 2026 12:58:11 +0100 Subject: [PATCH 04/15] docs: link issue 910 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e946f3d04..345052a85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - Enhance getting all Clients to remove Flow Override by using pagination by 100 to avoid timeout [#1384](https://github.com/adorsys/keycloak-config-cli/issues/1384) - JavaScript variable substitution support in configuration files [#934](https://github.com/adorsys/keycloak-config-cli/issues/934) - Add support for Keycloak Workflows management +- Add option to configure ignored user properties during user update (`--import.behaviors.user-update-ignored-properties`) [#910](https://github.com/adorsys/keycloak-config-cli/issues/910) ### Fixed - Fix bug where `clientProfiles` and `clientPolicies` were erased when importing multiple realm configuration files From 9ba96446cdf5cc494e744af64bb2c445e362fd71 Mon Sep 17 00:00:00 2001 From: Nkwenti-Severian-Ndongtsop Date: Wed, 25 Feb 2026 12:58:36 +0100 Subject: [PATCH 05/15] feat: user update ignored props --- .../config/properties/ImportConfigProperties.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) 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 54784c3c1..fabc32fa6 100644 --- a/src/main/java/de/adorsys/keycloak/config/properties/ImportConfigProperties.java +++ b/src/main/java/de/adorsys/keycloak/config/properties/ImportConfigProperties.java @@ -25,6 +25,7 @@ import org.springframework.validation.annotation.Validated; import java.util.Collection; +import java.util.List; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; @@ -383,13 +384,18 @@ public static class ImportBehaviorsProperties { @NotNull private final ChecksumChangedOption checksumChanged; + @NotNull + private final Collection userUpdateIgnoredProperties; + public ImportBehaviorsProperties(boolean syncUserFederation, boolean removeDefaultRoleFromUser, boolean skipAttributesForFederatedUser, - boolean checksumWithCacheKey, ChecksumChangedOption checksumChanged) { + boolean checksumWithCacheKey, ChecksumChangedOption checksumChanged, + @DefaultValue("attributes") Collection userUpdateIgnoredProperties) { this.syncUserFederation = syncUserFederation; this.removeDefaultRoleFromUser = removeDefaultRoleFromUser; this.skipAttributesForFederatedUser = skipAttributesForFederatedUser; this.checksumWithCacheKey = checksumWithCacheKey; this.checksumChanged = checksumChanged; + this.userUpdateIgnoredProperties = userUpdateIgnoredProperties == null ? List.of("attributes") : userUpdateIgnoredProperties; } public boolean isSyncUserFederation() { @@ -412,6 +418,10 @@ public ChecksumChangedOption getChecksumChanged() { return checksumChanged; } + public Collection getUserUpdateIgnoredProperties() { + return userUpdateIgnoredProperties; + } + public enum ChecksumChangedOption { CONTINUE, FAIL } From 1db59818ffdafb5192b949191b262a88322fb85d Mon Sep 17 00:00:00 2001 From: Nkwenti-Severian-Ndongtsop Date: Wed, 25 Feb 2026 12:58:39 +0100 Subject: [PATCH 06/15] feat: configurable ignored user fields --- .../config/service/UserImportService.java | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) 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 de8d95655..f33b6ca0d 100644 --- a/src/main/java/de/adorsys/keycloak/config/service/UserImportService.java +++ b/src/main/java/de/adorsys/keycloak/config/service/UserImportService.java @@ -49,7 +49,7 @@ public class UserImportService { private static final Logger logger = LoggerFactory.getLogger(UserImportService.class); - private static final String[] IGNORED_PROPERTIES_FOR_UPDATE = {"realmRoles", "clientRoles", "serviceAccountClientId", "attributes"}; + private static final String[] ALWAYS_IGNORED_PROPERTIES_FOR_UPDATE = {"realmRoles", "clientRoles", "serviceAccountClientId", "attributes"}; private static final String USER_LABEL_FOR_INITIAL_CREDENTIAL = "initial"; private final RealmRepository realmRepository; @@ -172,7 +172,7 @@ public void importUser() { private void updateUser(UserRepresentation existingUser) { UserRepresentation patchedUser = CloneUtil - .patch(existingUser, userToImport, IGNORED_PROPERTIES_FOR_UPDATE); + .patch(existingUser, userToImport, getIgnoredPropertiesForUpdate()); if (importConfigProperties.getBehaviors().isSkipAttributesForFederatedUser() && patchedUser.getFederationLink() != null) { patchedUser.setAttributes(null); @@ -253,6 +253,27 @@ private boolean isPasswordHistoryViolation(String errorMessage) { || lowerCaseError.contains("passwordpolicynotmetexception"); } + private String[] getIgnoredPropertiesForUpdate() { + Set ignored = new LinkedHashSet<>(); + ignored.addAll(Arrays.asList(ALWAYS_IGNORED_PROPERTIES_FOR_UPDATE)); + + Collection configuredIgnored = null; + if (importConfigProperties != null && importConfigProperties.getBehaviors() != null) { + configuredIgnored = importConfigProperties.getBehaviors().getUserUpdateIgnoredProperties(); + } + + if (configuredIgnored != null) { + for (String prop : configuredIgnored) { + if (prop == null) continue; + String trimmed = prop.trim(); + if (trimmed.isEmpty()) continue; + ignored.add(trimmed); + } + } + + return ignored.toArray(new String[0]); + } + private void handleGroups() { List userGroupsToUpdate = userToImport.getGroups(); if (userGroupsToUpdate == null) { From a190e42a0e5bfc691db13a377a09eb6788737633 Mon Sep 17 00:00:00 2001 From: Nkwenti-Severian-Ndongtsop Date: Wed, 25 Feb 2026 12:58:52 +0100 Subject: [PATCH 07/15] test: default ignored props --- .../keycloak/config/properties/ImportConfigPropertiesTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/java/de/adorsys/keycloak/config/properties/ImportConfigPropertiesTest.java b/src/test/java/de/adorsys/keycloak/config/properties/ImportConfigPropertiesTest.java index b6adfaeba..2551ef627 100644 --- a/src/test/java/de/adorsys/keycloak/config/properties/ImportConfigPropertiesTest.java +++ b/src/test/java/de/adorsys/keycloak/config/properties/ImportConfigPropertiesTest.java @@ -117,6 +117,7 @@ void shouldPopulateConfigurationProperties() { assertThat(properties.getBehaviors().isSkipAttributesForFederatedUser(), is(true)); assertThat(properties.getBehaviors().isChecksumWithCacheKey(), is(true)); assertThat(properties.getBehaviors().getChecksumChanged(), is(ChecksumChangedOption.FAIL)); + assertThat(properties.getBehaviors().getUserUpdateIgnoredProperties(), contains("attributes")); } @EnableConfigurationProperties(ImportConfigProperties.class) From e6ef1d93b6e40ec8c25cfda774be0e085decac5c Mon Sep 17 00:00:00 2001 From: Nkwenti-Severian-Ndongtsop Date: Wed, 25 Feb 2026 12:58:54 +0100 Subject: [PATCH 08/15] test: ignore configured user fields --- .../service/UserImportServiceRetryTest.java | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/test/java/de/adorsys/keycloak/config/service/UserImportServiceRetryTest.java b/src/test/java/de/adorsys/keycloak/config/service/UserImportServiceRetryTest.java index 194ebacab..dcc626373 100644 --- a/src/test/java/de/adorsys/keycloak/config/service/UserImportServiceRetryTest.java +++ b/src/test/java/de/adorsys/keycloak/config/service/UserImportServiceRetryTest.java @@ -39,11 +39,13 @@ import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; +import java.util.List; import java.util.Collections; import java.util.Optional; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; @@ -130,6 +132,32 @@ void shouldUpdateFieldsWithoutChangingPassword() throws IOException { verify(userRepository, times(1)).updateUser(eq(realm), any(UserRepresentation.class)); } + @Test + void shouldIgnoreConfiguredUserUpdateProperties() throws IOException { + UserRepresentation userToImport = loadUserFromJson("import-files/users/61_create_realm_with_password_history_policy.json"); + String realm = "testIgnoredUserProperties"; + + UserRepresentation existingUser = loadUserFromJson("import-files/users/61_create_realm_with_password_history_policy.json"); + existingUser.setId("user1"); + + existingUser.setEmail("ldap@example.com"); + userToImport.setEmail("new@example.com"); + userToImport.setLastName("UpdatedLastName"); + + when(userRepository.search(realm, userToImport.getUsername())).thenReturn(Optional.of(existingUser)); + + RealmRepresentation realmRepresentation = new RealmRepresentation(); + realmRepresentation.setRegistrationEmailAsUsername(false); + when(realmRepository.get(realm)).thenReturn(realmRepresentation); + + when(importBehaviorsProperties.getUserUpdateIgnoredProperties()).thenReturn(List.of("attributes", "email")); + + assertDoesNotThrow(() -> userImportService.doImport(createRealmImport(realm, userToImport))); + + verify(userRepository).updateUser(eq(realm), any(UserRepresentation.class)); + verify(userRepository).updateUser(eq(realm), argThat(u -> "ldap@example.com".equals(u.getEmail()))); + } + private RealmImport createRealmImport(String realm, UserRepresentation user) { RealmImport realmImport = new RealmImport(); realmImport.setRealm(realm); From c9bc1d52d5e46a5c5b4dc6907fff198cc4060988 Mon Sep 17 00:00:00 2001 From: Nkwenti-Severian-Ndongtsop Date: Wed, 25 Feb 2026 12:58:56 +0100 Subject: [PATCH 09/15] test: it user update ignored props --- .../ImportUserUpdateIgnoredPropertiesIT.java | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 src/test/java/de/adorsys/keycloak/config/service/ImportUserUpdateIgnoredPropertiesIT.java diff --git a/src/test/java/de/adorsys/keycloak/config/service/ImportUserUpdateIgnoredPropertiesIT.java b/src/test/java/de/adorsys/keycloak/config/service/ImportUserUpdateIgnoredPropertiesIT.java new file mode 100644 index 000000000..ac2e556bc --- /dev/null +++ b/src/test/java/de/adorsys/keycloak/config/service/ImportUserUpdateIgnoredPropertiesIT.java @@ -0,0 +1,75 @@ +/*- + * ---license-start + * keycloak-config-cli + * --- + * Copyright (C) 2017 - 2021 adorsys GmbH & Co. KG @ https://adorsys.com + * --- + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ---license-end + */ + +package de.adorsys.keycloak.config.service; + +import de.adorsys.keycloak.config.AbstractImportIT; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.keycloak.representations.idm.UserRepresentation; +import org.springframework.test.context.TestPropertySource; + +import java.io.IOException; +import java.util.List; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.hasItems; +import static org.hamcrest.core.Is.is; + +@TestPropertySource(properties = { + "import.behaviors.user-update-ignored-properties=attributes,email" +}) +@SuppressWarnings({"java:S5961", "java:S5976"}) +class ImportUserUpdateIgnoredPropertiesIT extends AbstractImportIT { + + private static final String REALM_NAME = "realmUserUpdateIgnoredProps"; + + ImportUserUpdateIgnoredPropertiesIT() { + this.resourcePath = "import-files/user-update-ignored-properties"; + } + + @Test + @Order(0) + void shouldCreateRealmWithUser() throws IOException { + doImport("00_create_realm_with_user.json"); + + UserRepresentation user = keycloakRepository.getUser(REALM_NAME, "ldapuser"); + assertThat(user.getEmail(), is("ldapuser@old.example")); + } + + @Test + @Order(1) + void shouldUpdateRolesButNotOverwriteEmailWhenConfigured() throws IOException { + UserRepresentation userBefore = keycloakRepository.getUser(REALM_NAME, "ldapuser"); + assertThat(userBefore.getEmail(), is("ldapuser@old.example")); + + doImport("01_update_user_roles_try_change_email.json"); + + UserRepresentation userAfter = keycloakRepository.getUser(REALM_NAME, "ldapuser"); + assertThat(userAfter.getEmail(), is("ldapuser@old.example")); + + List realmLevelRoles = keycloakRepository.getUserRealmLevelRoles(REALM_NAME, "ldapuser"); + assertThat(realmLevelRoles, hasItems("role_a", "role_b")); + + List clientLevelRoles = keycloakRepository.getUserClientLevelRoles(REALM_NAME, "ldapuser", "test-client"); + assertThat(clientLevelRoles, contains("client_role_a")); + } +} From 0029ce49f906f1a178b1f51e799ec32fb4da0fbd Mon Sep 17 00:00:00 2001 From: Nkwenti-Severian-Ndongtsop Date: Wed, 25 Feb 2026 15:31:35 +0100 Subject: [PATCH 10/15] test: remove codepoint limit --- .../keycloak/config/provider/KeycloakExportProviderTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/java/de/adorsys/keycloak/config/provider/KeycloakExportProviderTest.java b/src/test/java/de/adorsys/keycloak/config/provider/KeycloakExportProviderTest.java index 637e34dbf..512b3a860 100644 --- a/src/test/java/de/adorsys/keycloak/config/provider/KeycloakExportProviderTest.java +++ b/src/test/java/de/adorsys/keycloak/config/provider/KeycloakExportProviderTest.java @@ -77,7 +77,6 @@ void setUp() { lenient().when(patternResolver.getPathMatcher()).thenReturn(pathMatcher); lenient().when(filesProperties.getExcludes()).thenReturn(Collections.emptyList()); lenient().when(filesProperties.isIncludeHiddenFiles()).thenReturn(false); - lenient().when(filesProperties.getCodePointLimit()).thenReturn(500 * 1024 * 1024); exportProvider = new KeycloakExportProvider(patternResolver, normalizationConfigProperties); } From 939c5dabca027c2bd69dbddb648d98ebf9820b79 Mon Sep 17 00:00:00 2001 From: Nkwenti-Severian-Ndongtsop Date: Mon, 2 Mar 2026 16:25:15 +0100 Subject: [PATCH 11/15] fix: ensure user can re-use user update without disabling fields --- .../properties/ImportConfigProperties.java | 26 +++++++++++++++++-- .../config/service/UserImportService.java | 11 ++++++++ .../ImportConfigPropertiesTest.java | 2 ++ 3 files changed, 37 insertions(+), 2 deletions(-) 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 fabc32fa6..6f44831f3 100644 --- a/src/main/java/de/adorsys/keycloak/config/properties/ImportConfigProperties.java +++ b/src/main/java/de/adorsys/keycloak/config/properties/ImportConfigProperties.java @@ -26,6 +26,7 @@ import java.util.Collection; import java.util.List; +import java.util.stream.Collectors; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; @@ -387,15 +388,32 @@ public static class ImportBehaviorsProperties { @NotNull private final Collection userUpdateIgnoredProperties; + @NotNull + private final Collection userUpdateIgnoredPropertiesRemove; + public ImportBehaviorsProperties(boolean syncUserFederation, boolean removeDefaultRoleFromUser, boolean skipAttributesForFederatedUser, boolean checksumWithCacheKey, ChecksumChangedOption checksumChanged, - @DefaultValue("attributes") Collection userUpdateIgnoredProperties) { + @DefaultValue("attributes") Collection userUpdateIgnoredProperties, + @DefaultValue("") Collection userUpdateIgnoredPropertiesRemove) { this.syncUserFederation = syncUserFederation; this.removeDefaultRoleFromUser = removeDefaultRoleFromUser; this.skipAttributesForFederatedUser = skipAttributesForFederatedUser; this.checksumWithCacheKey = checksumWithCacheKey; this.checksumChanged = checksumChanged; - this.userUpdateIgnoredProperties = userUpdateIgnoredProperties == null ? List.of("attributes") : userUpdateIgnoredProperties; + this.userUpdateIgnoredProperties = normalizeStringCollection( + userUpdateIgnoredProperties == null ? List.of("attributes") : userUpdateIgnoredProperties + ); + this.userUpdateIgnoredPropertiesRemove = normalizeStringCollection( + userUpdateIgnoredPropertiesRemove == null ? List.of() : userUpdateIgnoredPropertiesRemove + ); + } + + private static Collection normalizeStringCollection(Collection values) { + if (values == null) return List.of(); + return values.stream() + .filter(v -> v != null && !v.trim().isEmpty()) + .map(String::trim) + .collect(Collectors.toList()); } public boolean isSyncUserFederation() { @@ -422,6 +440,10 @@ public Collection getUserUpdateIgnoredProperties() { return userUpdateIgnoredProperties; } + public Collection getUserUpdateIgnoredPropertiesRemove() { + return userUpdateIgnoredPropertiesRemove; + } + public enum ChecksumChangedOption { CONTINUE, FAIL } 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 f33b6ca0d..cf026b470 100644 --- a/src/main/java/de/adorsys/keycloak/config/service/UserImportService.java +++ b/src/main/java/de/adorsys/keycloak/config/service/UserImportService.java @@ -258,8 +258,10 @@ private String[] getIgnoredPropertiesForUpdate() { ignored.addAll(Arrays.asList(ALWAYS_IGNORED_PROPERTIES_FOR_UPDATE)); Collection configuredIgnored = null; + Collection configuredIgnoredRemove = null; if (importConfigProperties != null && importConfigProperties.getBehaviors() != null) { configuredIgnored = importConfigProperties.getBehaviors().getUserUpdateIgnoredProperties(); + configuredIgnoredRemove = importConfigProperties.getBehaviors().getUserUpdateIgnoredPropertiesRemove(); } if (configuredIgnored != null) { @@ -271,6 +273,15 @@ private String[] getIgnoredPropertiesForUpdate() { } } + if (configuredIgnoredRemove != null) { + for (String prop : configuredIgnoredRemove) { + if (prop == null) continue; + String trimmed = prop.trim(); + if (trimmed.isEmpty()) continue; + ignored.remove(trimmed); + } + } + return ignored.toArray(new String[0]); } diff --git a/src/test/java/de/adorsys/keycloak/config/properties/ImportConfigPropertiesTest.java b/src/test/java/de/adorsys/keycloak/config/properties/ImportConfigPropertiesTest.java index 2551ef627..86cb0a83e 100644 --- a/src/test/java/de/adorsys/keycloak/config/properties/ImportConfigPropertiesTest.java +++ b/src/test/java/de/adorsys/keycloak/config/properties/ImportConfigPropertiesTest.java @@ -33,6 +33,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; // From: https://tuhrig.de/testing-configurationproperties-in-spring-boot/ @@ -118,6 +119,7 @@ void shouldPopulateConfigurationProperties() { assertThat(properties.getBehaviors().isChecksumWithCacheKey(), is(true)); assertThat(properties.getBehaviors().getChecksumChanged(), is(ChecksumChangedOption.FAIL)); assertThat(properties.getBehaviors().getUserUpdateIgnoredProperties(), contains("attributes")); + assertThat(properties.getBehaviors().getUserUpdateIgnoredPropertiesRemove(), hasSize(0)); } @EnableConfigurationProperties(ImportConfigProperties.class) From 568da23d9aacdc8a344bd17037c3dc5cdeeb6b0e Mon Sep 17 00:00:00 2001 From: Nkwenti-Severian-Ndongtsop Date: Wed, 4 Mar 2026 17:13:06 +0100 Subject: [PATCH 12/15] fix: checksum issue which was causing the flag bug --- .../provider/KeycloakImportProvider.java | 25 ++++++++++- .../ImportConfigPropertiesTest.java | 2 - .../ImportUserUpdateIgnoredPropertiesIT.java | 43 +++++++++---------- 3 files changed, 44 insertions(+), 26 deletions(-) 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 a7720abde..8dc72745a 100644 --- a/src/main/java/de/adorsys/keycloak/config/provider/KeycloakImportProvider.java +++ b/src/main/java/de/adorsys/keycloak/config/provider/KeycloakImportProvider.java @@ -254,7 +254,7 @@ private String evaluateScripts(String content) { private Pair> readRealmImportFromImportResource(ImportResource resource) { String location = resource.getFilename(); String content = resource.getValue(); - String contentChecksum = DigestUtils.sha256Hex(content); + String contentChecksum = DigestUtils.sha256Hex(content + getImportBehaviorChecksumSalt()); if (logger.isTraceEnabled()) { logger.trace(content); @@ -274,6 +274,29 @@ private Pair> readRealmImportFromImportResource(Import return new ImmutablePair<>(location, realmImports); } + private String getImportBehaviorChecksumSalt() { + if (importConfigProperties == null || importConfigProperties.getBehaviors() == null) { + return ""; + } + + Collection ignored = importConfigProperties.getBehaviors().getUserUpdateIgnoredProperties(); + String ignoredNormalized = normalizeAndSortChecksumValues(ignored); + + return "\n#userUpdateIgnoredProperties=" + ignoredNormalized; + } + + private String normalizeAndSortChecksumValues(Collection values) { + if (values == null || values.isEmpty()) { + return ""; + } + return values.stream() + .filter(Objects::nonNull) + .map(String::trim) + .filter(v -> !v.isEmpty()) + .sorted() + .collect(Collectors.joining(",")); + } + private List readContent(String content) { List realmImports = new ArrayList<>(); diff --git a/src/test/java/de/adorsys/keycloak/config/properties/ImportConfigPropertiesTest.java b/src/test/java/de/adorsys/keycloak/config/properties/ImportConfigPropertiesTest.java index 86cb0a83e..2551ef627 100644 --- a/src/test/java/de/adorsys/keycloak/config/properties/ImportConfigPropertiesTest.java +++ b/src/test/java/de/adorsys/keycloak/config/properties/ImportConfigPropertiesTest.java @@ -33,7 +33,6 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.contains; -import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; // From: https://tuhrig.de/testing-configurationproperties-in-spring-boot/ @@ -119,7 +118,6 @@ void shouldPopulateConfigurationProperties() { assertThat(properties.getBehaviors().isChecksumWithCacheKey(), is(true)); assertThat(properties.getBehaviors().getChecksumChanged(), is(ChecksumChangedOption.FAIL)); assertThat(properties.getBehaviors().getUserUpdateIgnoredProperties(), contains("attributes")); - assertThat(properties.getBehaviors().getUserUpdateIgnoredPropertiesRemove(), hasSize(0)); } @EnableConfigurationProperties(ImportConfigProperties.class) diff --git a/src/test/java/de/adorsys/keycloak/config/service/ImportUserUpdateIgnoredPropertiesIT.java b/src/test/java/de/adorsys/keycloak/config/service/ImportUserUpdateIgnoredPropertiesIT.java index ac2e556bc..7d0e33ccb 100644 --- a/src/test/java/de/adorsys/keycloak/config/service/ImportUserUpdateIgnoredPropertiesIT.java +++ b/src/test/java/de/adorsys/keycloak/config/service/ImportUserUpdateIgnoredPropertiesIT.java @@ -21,25 +21,19 @@ package de.adorsys.keycloak.config.service; import de.adorsys.keycloak.config.AbstractImportIT; +import de.adorsys.keycloak.config.properties.ImportConfigProperties; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.keycloak.representations.idm.UserRepresentation; -import org.springframework.test.context.TestPropertySource; +import org.springframework.test.util.ReflectionTestUtils; import java.io.IOException; import java.util.List; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.contains; -import static org.hamcrest.Matchers.hasItems; -import static org.hamcrest.core.Is.is; +import static org.hamcrest.Matchers.is; -@TestPropertySource(properties = { - "import.behaviors.user-update-ignored-properties=attributes,email" -}) -@SuppressWarnings({"java:S5961", "java:S5976"}) class ImportUserUpdateIgnoredPropertiesIT extends AbstractImportIT { - private static final String REALM_NAME = "realmUserUpdateIgnoredProps"; ImportUserUpdateIgnoredPropertiesIT() { @@ -48,28 +42,31 @@ class ImportUserUpdateIgnoredPropertiesIT extends AbstractImportIT { @Test @Order(0) - void shouldCreateRealmWithUser() throws IOException { + void shouldReproduceIssue() throws IOException { + // 1. Initial import: create user with old email doImport("00_create_realm_with_user.json"); UserRepresentation user = keycloakRepository.getUser(REALM_NAME, "ldapuser"); assertThat(user.getEmail(), is("ldapuser@old.example")); - } - - @Test - @Order(1) - void shouldUpdateRolesButNotOverwriteEmailWhenConfigured() throws IOException { - UserRepresentation userBefore = keycloakRepository.getUser(REALM_NAME, "ldapuser"); - assertThat(userBefore.getEmail(), is("ldapuser@old.example")); + // 2. Second import: ignore email, and change email in JSON + // We simulate the CLI flag by modifying the bean + ImportConfigProperties importProperties = (ImportConfigProperties) ReflectionTestUtils.getField(realmImportService, "importProperties"); + ReflectionTestUtils.setField(importProperties.getBehaviors(), "userUpdateIgnoredProperties", List.of("email")); + doImport("01_update_user_roles_try_change_email.json"); - UserRepresentation userAfter = keycloakRepository.getUser(REALM_NAME, "ldapuser"); - assertThat(userAfter.getEmail(), is("ldapuser@old.example")); + user = keycloakRepository.getUser(REALM_NAME, "ldapuser"); + assertThat(user.getEmail(), is("ldapuser@old.example")); // Should still be old email because it's ignored + + // 3. Third import: remove email from ignore list, but same JSON + ReflectionTestUtils.setField(importProperties.getBehaviors(), "userUpdateIgnoredProperties", List.of()); - List realmLevelRoles = keycloakRepository.getUserRealmLevelRoles(REALM_NAME, "ldapuser"); - assertThat(realmLevelRoles, hasItems("role_a", "role_b")); + doImport("01_update_user_roles_try_change_email.json"); - List clientLevelRoles = keycloakRepository.getUserClientLevelRoles(REALM_NAME, "ldapuser", "test-client"); - assertThat(clientLevelRoles, contains("client_role_a")); + user = keycloakRepository.getUser(REALM_NAME, "ldapuser"); + // THIS IS WHERE IT IS EXPECTED TO FAIL: + // If the checksum matches, it will skip the import, and the email will STILL be "ldapuser@old.example" + assertThat(user.getEmail(), is("ldapuser@new.example")); } } From 665b4e01000e38985a66b19fd262391c4ff61f9e Mon Sep 17 00:00:00 2001 From: Nkwenti-Severian-Ndongtsop Date: Wed, 4 Mar 2026 17:13:22 +0100 Subject: [PATCH 13/15] remove flag implementation --- .../config/properties/ImportConfigProperties.java | 13 +------------ .../keycloak/config/service/UserImportService.java | 11 ----------- 2 files changed, 1 insertion(+), 23 deletions(-) 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 6f44831f3..99ef621ce 100644 --- a/src/main/java/de/adorsys/keycloak/config/properties/ImportConfigProperties.java +++ b/src/main/java/de/adorsys/keycloak/config/properties/ImportConfigProperties.java @@ -388,13 +388,9 @@ public static class ImportBehaviorsProperties { @NotNull private final Collection userUpdateIgnoredProperties; - @NotNull - private final Collection userUpdateIgnoredPropertiesRemove; - public ImportBehaviorsProperties(boolean syncUserFederation, boolean removeDefaultRoleFromUser, boolean skipAttributesForFederatedUser, boolean checksumWithCacheKey, ChecksumChangedOption checksumChanged, - @DefaultValue("attributes") Collection userUpdateIgnoredProperties, - @DefaultValue("") Collection userUpdateIgnoredPropertiesRemove) { + @DefaultValue("attributes") Collection userUpdateIgnoredProperties) { this.syncUserFederation = syncUserFederation; this.removeDefaultRoleFromUser = removeDefaultRoleFromUser; this.skipAttributesForFederatedUser = skipAttributesForFederatedUser; @@ -403,9 +399,6 @@ public ImportBehaviorsProperties(boolean syncUserFederation, boolean removeDefau this.userUpdateIgnoredProperties = normalizeStringCollection( userUpdateIgnoredProperties == null ? List.of("attributes") : userUpdateIgnoredProperties ); - this.userUpdateIgnoredPropertiesRemove = normalizeStringCollection( - userUpdateIgnoredPropertiesRemove == null ? List.of() : userUpdateIgnoredPropertiesRemove - ); } private static Collection normalizeStringCollection(Collection values) { @@ -440,10 +433,6 @@ public Collection getUserUpdateIgnoredProperties() { return userUpdateIgnoredProperties; } - public Collection getUserUpdateIgnoredPropertiesRemove() { - return userUpdateIgnoredPropertiesRemove; - } - public enum ChecksumChangedOption { CONTINUE, FAIL } 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 cf026b470..f33b6ca0d 100644 --- a/src/main/java/de/adorsys/keycloak/config/service/UserImportService.java +++ b/src/main/java/de/adorsys/keycloak/config/service/UserImportService.java @@ -258,10 +258,8 @@ private String[] getIgnoredPropertiesForUpdate() { ignored.addAll(Arrays.asList(ALWAYS_IGNORED_PROPERTIES_FOR_UPDATE)); Collection configuredIgnored = null; - Collection configuredIgnoredRemove = null; if (importConfigProperties != null && importConfigProperties.getBehaviors() != null) { configuredIgnored = importConfigProperties.getBehaviors().getUserUpdateIgnoredProperties(); - configuredIgnoredRemove = importConfigProperties.getBehaviors().getUserUpdateIgnoredPropertiesRemove(); } if (configuredIgnored != null) { @@ -273,15 +271,6 @@ private String[] getIgnoredPropertiesForUpdate() { } } - if (configuredIgnoredRemove != null) { - for (String prop : configuredIgnoredRemove) { - if (prop == null) continue; - String trimmed = prop.trim(); - if (trimmed.isEmpty()) continue; - ignored.remove(trimmed); - } - } - return ignored.toArray(new String[0]); } From bbb28394c57f324d2399b37896e37516e2e7ccf8 Mon Sep 17 00:00:00 2001 From: Nkwenti-Severian-Ndongtsop Date: Wed, 4 Mar 2026 19:03:06 +0100 Subject: [PATCH 14/15] fix checksum backward failure --- .../keycloak/config/provider/KeycloakImportProvider.java | 6 ++++++ 1 file changed, 6 insertions(+) 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 8dc72745a..21398e50a 100644 --- a/src/main/java/de/adorsys/keycloak/config/provider/KeycloakImportProvider.java +++ b/src/main/java/de/adorsys/keycloak/config/provider/KeycloakImportProvider.java @@ -275,6 +275,12 @@ private Pair> readRealmImportFromImportResource(Import } private String getImportBehaviorChecksumSalt() { + if (environment == null + || !environment.containsProperty("import.behaviors.user-update-ignored-properties") + ) { + return ""; + } + if (importConfigProperties == null || importConfigProperties.getBehaviors() == null) { return ""; } From f3fbb412c154c493efd4581dd3754999215994e9 Mon Sep 17 00:00:00 2001 From: Nkwenti-Severian-Ndongtsop Date: Wed, 4 Mar 2026 20:46:42 +0100 Subject: [PATCH 15/15] fix checksum check activation --- .../ImportUserUpdateIgnoredPropertiesIT.java | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/test/java/de/adorsys/keycloak/config/service/ImportUserUpdateIgnoredPropertiesIT.java b/src/test/java/de/adorsys/keycloak/config/service/ImportUserUpdateIgnoredPropertiesIT.java index 7d0e33ccb..bcf9cd531 100644 --- a/src/test/java/de/adorsys/keycloak/config/service/ImportUserUpdateIgnoredPropertiesIT.java +++ b/src/test/java/de/adorsys/keycloak/config/service/ImportUserUpdateIgnoredPropertiesIT.java @@ -53,8 +53,18 @@ void shouldReproduceIssue() throws IOException { // We simulate the CLI flag by modifying the bean ImportConfigProperties importProperties = (ImportConfigProperties) ReflectionTestUtils.getField(realmImportService, "importProperties"); ReflectionTestUtils.setField(importProperties.getBehaviors(), "userUpdateIgnoredProperties", List.of("email")); - - doImport("01_update_user_roles_try_change_email.json"); + + String previousSystemProperty = System.getProperty("import.behaviors.user-update-ignored-properties"); + System.setProperty("import.behaviors.user-update-ignored-properties", "email"); + try { + doImport("01_update_user_roles_try_change_email.json"); + } finally { + if (previousSystemProperty == null) { + System.clearProperty("import.behaviors.user-update-ignored-properties"); + } else { + System.setProperty("import.behaviors.user-update-ignored-properties", previousSystemProperty); + } + } user = keycloakRepository.getUser(REALM_NAME, "ldapuser"); assertThat(user.getEmail(), is("ldapuser@old.example")); // Should still be old email because it's ignored