diff --git a/CHANGELOG.md b/CHANGELOG.md index db1f3af3f..ffc35b372 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Fixed +- Fix Initial Credentials Causes Update [819](https://github.com/adorsys/keycloak-config-cli/issues/819) ## Fixed - otpPolicyAlgorithm ignored during import [#847](https://github.com/adorsys/keycloak-config-cli/issues/847) 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 595805013..a967142b6 100644 --- a/src/main/java/de/adorsys/keycloak/config/service/UserImportService.java +++ b/src/main/java/de/adorsys/keycloak/config/service/UserImportService.java @@ -156,7 +156,7 @@ private void updateUser(UserRepresentation existingUser) { credentialRepresentation.getUserLabel(), USER_LABEL_FOR_INITIAL_CREDENTIAL )) .toList(); - patchedUser.setCredentials(userCredentials); + patchedUser.setCredentials(userCredentials.isEmpty() ? null : userCredentials); } if (!CloneUtil.deepEquals(existingUser, patchedUser, "access")) { diff --git a/src/main/java/de/adorsys/keycloak/config/util/CloneUtil.java b/src/main/java/de/adorsys/keycloak/config/util/CloneUtil.java index 13d0c4f27..5088ef24c 100644 --- a/src/main/java/de/adorsys/keycloak/config/util/CloneUtil.java +++ b/src/main/java/de/adorsys/keycloak/config/util/CloneUtil.java @@ -100,6 +100,10 @@ public static boolean deepEquals(S origin, T other, String... ignoredProp removeIgnoredProperties(originJsonNode, ignoredProperties); removeIgnoredProperties(otherJsonNode, ignoredProperties); + + handleEmptyCredentials(originJsonNode); + handleEmptyCredentials(otherJsonNode); + boolean ret = Objects.equals(originJsonNode, otherJsonNode); logger.trace("objects.deepEquals: ret: {} | origin: {} | other: {} | ignoredProperties: {}", ret, originJsonNode, otherJsonNode, ignoredProperties @@ -108,6 +112,12 @@ public static boolean deepEquals(S origin, T other, String... ignoredProp return ret; } + private static void handleEmptyCredentials(JsonNode jsonNode) { + if (jsonNode.has("credentials") && jsonNode.get("credentials").isEmpty()) { + ((ObjectNode) jsonNode).remove("credentials"); + } + } + private static void removeIgnoredProperties(JsonNode jsonNode, String[] ignoredProperties) { ((ObjectNode) jsonNode).remove(Arrays.asList(ignoredProperties)); } diff --git a/src/test/java/de/adorsys/keycloak/config/service/ImportUsersIT.java b/src/test/java/de/adorsys/keycloak/config/service/ImportUsersIT.java index 4d469e1b0..76b303380 100644 --- a/src/test/java/de/adorsys/keycloak/config/service/ImportUsersIT.java +++ b/src/test/java/de/adorsys/keycloak/config/service/ImportUsersIT.java @@ -494,6 +494,38 @@ void shouldUpdateRealmUpdateUserRemoveSubGroup() throws IOException { assertThat(group2.getName(), is("subgroup2")); } + @Test + @Order(15) + void shouldNotUpdateUserWhenOnlyInitialPasswordChanges() throws IOException { + doImport("15.0_update_realm_change_clientusers_password.json"); + + UserRepresentation userBefore = keycloakRepository.getUser(REALM_NAME, "myinitialclientuser"); + String modifiedAtBefore = null; + if (userBefore.getAttributes() != null && userBefore.getAttributes().containsKey("modifiedAt")) { + modifiedAtBefore = userBefore.getAttributes().get("modifiedAt").get(0); + } + + doImport("15.1_update_realm_change_clientusers_password.json"); + + UserRepresentation userAfter = keycloakRepository.getUser(REALM_NAME, "myinitialclientuser"); + String modifiedAtAfter = null; + if (userAfter.getAttributes() != null && userAfter.getAttributes().containsKey("modifiedAt")) { + modifiedAtAfter = userAfter.getAttributes().get("modifiedAt").get(0); + } + + assertThat(modifiedAtAfter, is(modifiedAtBefore)); + + AccessTokenResponse token = keycloakAuthentication.login( + REALM_NAME, + "moped-client", + "my-special-client-secret", + "myinitialclientuser", + "initialchangedclientuser123" + ); + + assertThat(token.getToken(), notNullValue()); + } + @Test @Order(50) void shouldUpdateUserWithEmailAsRegistration() throws IOException { diff --git a/src/test/resources/import-files/users/15.0_update_realm_change_clientusers_password.json b/src/test/resources/import-files/users/15.0_update_realm_change_clientusers_password.json new file mode 100644 index 000000000..dcc7f207a --- /dev/null +++ b/src/test/resources/import-files/users/15.0_update_realm_change_clientusers_password.json @@ -0,0 +1,40 @@ +{ + "enabled": true, + "realm": "realmWithUsers", + "users": [ + { + "username": "myinitialclientuser", + "email": "myinitialclientuser@mail.de", + "enabled": true, + "firstName": "My clientuser's firstname", + "lastName": "My clientuser's lastname", + "attributes": { + "modifiedAt": ["2023-01-01T00:00:00Z"] + }, + "credentials": [ + { + "type": "password", + "userLabel": "initial", + "value": "initialchangedclientuser123" + } + ] + } + ], + "clients": [ + { + "clientId": "moped-client", + "name": "moped-client", + "description": "Moped-Client", + "enabled": true, + "clientAuthenticatorType": "client-secret", + "secret": "my-special-client-secret", + "directAccessGrantsEnabled": true, + "redirectUris": [ + "*" + ], + "webOrigins": [ + "*" + ] + } + ] +} diff --git a/src/test/resources/import-files/users/15.1_update_realm_change_clientusers_password.json b/src/test/resources/import-files/users/15.1_update_realm_change_clientusers_password.json new file mode 100644 index 000000000..dfe27e2f7 --- /dev/null +++ b/src/test/resources/import-files/users/15.1_update_realm_change_clientusers_password.json @@ -0,0 +1,40 @@ +{ + "enabled": true, + "realm": "realmWithUsers", + "users": [ + { + "username": "myinitialclientuser", + "email": "myinitialclientuser@mail.de", + "enabled": true, + "firstName": "My clientuser's firstname", + "lastName": "My clientuser's lastname", + "attributes": { + "modifiedAt": ["2023-01-01T00:00:00Z"] + }, + "credentials": [ + { + "type": "password", + "userLabel": "initial", + "value": "newInitialPassword123" + } + ] + } + ], + "clients": [ + { + "clientId": "moped-client", + "name": "moped-client", + "description": "Moped-Client", + "enabled": true, + "clientAuthenticatorType": "client-secret", + "secret": "my-special-client-secret", + "directAccessGrantsEnabled": true, + "redirectUris": [ + "*" + ], + "webOrigins": [ + "*" + ] + } + ] +}