diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 6f7c55d37..23ce5296f 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -251,7 +251,7 @@ jobs: run: ./mvnw ${MAVEN_CLI_OPTS} -Dkeycloak.version=${{ matrix.env.KEYCLOAK_VERSION }} -Dkeycloak.client.version=${{ matrix.env.KEYCLOAK_CLIENT_VERSION }} -Dkeycloak.dockerTagSuffix="-legacy" ${ADJUSTED_RESTEASY_VERSION} clean verify ${COMPATIBILITY_PROFILE} lint-other-files: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 timeout-minutes: 10 steps: - uses: actions/checkout@v4.2.2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 2012e4873..ef0ddd50d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] +### Fixed +- Fix Service Account User always triggers UPDATE USER event [#878](https://github.com/adorsys/keycloak-config-cli/issues/878) ### Added - Publish charts with github pages [#941](https://github.com/adorsys/keycloak-config-cli/issues/941) 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 e6ba35d2a..2dd67f0e6 100644 --- a/src/main/java/de/adorsys/keycloak/config/repository/UserRepository.java +++ b/src/main/java/de/adorsys/keycloak/config/repository/UserRepository.java @@ -56,6 +56,13 @@ public Optional search(String realmName, String username) { user = Optional.of(foundUsers.get(0)); } + // Retrieve the service account client id if it exists + user.ifPresent(u -> { + UserResource userResource = usersResource.get(u.getId()); + UserRepresentation userRepresentation = userResource.toRepresentation(); + u.setServiceAccountClientId(userRepresentation.getServiceAccountClientId()); + }); + return user; } 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 c1af0878e..303c7e7f0 100644 --- a/src/main/java/de/adorsys/keycloak/config/service/RealmImportService.java +++ b/src/main/java/de/adorsys/keycloak/config/service/RealmImportService.java @@ -169,7 +169,13 @@ private void setEventsEnabledWorkaround(RealmImport realmImport) { if (realmImport.isEventsEnabled() != null) return; Boolean existingEventsEnabled = realmRepository.get(realmImport.getRealm()).isEventsEnabled(); - realmImport.setEventsEnabled(existingEventsEnabled); + if (existingEventsEnabled != null) { + realmImport.setEventsEnabled(existingEventsEnabled); + } else { + realmImport.setEventsEnabled(false); + logger.warn("Events enabled status is null for realm '{}'. " + "Setting to false by default.", realmImport.getRealm()); + } + } private void createRealm(RealmImport realmImport) { 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 a967142b6..0efbc0063 100644 --- a/src/main/java/de/adorsys/keycloak/config/service/UserImportService.java +++ b/src/main/java/de/adorsys/keycloak/config/service/UserImportService.java @@ -45,7 +45,7 @@ public class UserImportService { private static final Logger logger = LoggerFactory.getLogger(UserImportService.class); - private static final String[] IGNORED_PROPERTIES_FOR_UPDATE = {"realmRoles", "clientRoles"}; + private static final String[] IGNORED_PROPERTIES_FOR_UPDATE = {"realmRoles", "clientRoles", "serviceAccountClientId", "attributes"}; private static final String USER_LABEL_FOR_INITIAL_CREDENTIAL = "initial"; private final RealmRepository realmRepository; @@ -159,6 +159,9 @@ private void updateUser(UserRepresentation existingUser) { patchedUser.setCredentials(userCredentials.isEmpty() ? null : userCredentials); } + logger.debug("Existing user: {}", existingUser); + logger.debug("Patched user: {}", patchedUser); + if (!CloneUtil.deepEquals(existingUser, patchedUser, "access")) { logger.debug("Update user '{}' in realm '{}'", userToImport.getUsername(), realmName); userRepository.updateUser(realmName, patchedUser); 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 87bd7b21a..056e3ce7a 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 @@ -34,6 +34,7 @@ import org.springframework.stereotype.Service; import java.text.MessageFormat; +import java.util.HashMap; import java.util.Map; import java.util.Objects; @@ -54,6 +55,11 @@ public void doImport(RealmImport realmImport) { RealmRepresentation existingRealm = realmRepository.get(realmImport.getRealm()); Map customAttributes = existingRealm.getAttributes(); + if (customAttributes == null) { + customAttributes = new HashMap<>(); + existingRealm.setAttributes(customAttributes); + } + String importChecksum = realmImport.getChecksum(); String attributeKey = getCustomAttributeKey(realmImport); customAttributes.put(attributeKey, importChecksum); @@ -69,6 +75,11 @@ public boolean hasToBeUpdated(RealmImport realmImport) { } Map customAttributes = existingRealm.getAttributes(); + if (customAttributes == null) { + customAttributes = new HashMap<>(); + existingRealm.setAttributes(customAttributes); + } + String readChecksum = customAttributes.get(getCustomAttributeKey(realmImport)); if (readChecksum == null) { return true; 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 5088ef24c..92da1a4dc 100644 --- a/src/main/java/de/adorsys/keycloak/config/util/CloneUtil.java +++ b/src/main/java/de/adorsys/keycloak/config/util/CloneUtil.java @@ -108,6 +108,16 @@ public static boolean deepEquals(S origin, T other, String... ignoredProp logger.trace("objects.deepEquals: ret: {} | origin: {} | other: {} | ignoredProperties: {}", ret, originJsonNode, otherJsonNode, ignoredProperties ); + if (!ret) { + logger.debug("Differences detected between origin and other:"); + originJsonNode.fieldNames().forEachRemaining(fieldName -> { + JsonNode originValue = originJsonNode.get(fieldName); + JsonNode otherValue = otherJsonNode.get(fieldName); + if (!Objects.equals(originValue, otherValue)) { + logger.debug("Field '{}' is different: origin = {}, other = {}", fieldName, originValue, otherValue); + } + }); + } return ret; } 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 76b303380..fa5b2b433 100644 --- a/src/test/java/de/adorsys/keycloak/config/service/ImportUsersIT.java +++ b/src/test/java/de/adorsys/keycloak/config/service/ImportUsersIT.java @@ -526,6 +526,39 @@ void shouldNotUpdateUserWhenOnlyInitialPasswordChanges() throws IOException { assertThat(token.getToken(), notNullValue()); } + @Test + @Order(16) + void shouldNotTriggerUpdateUserEventForServiceAccountUserWithoutChanges() throws IOException { + doImport("60.1_update_realm_add_clientl_with_service_account.json"); + + // Re-import the same configuration to check if UPDATE USER event is not triggered + doImport("60.2_update_realm_add_clientl_with_service_account.json"); + + RealmRepresentation realm = keycloakProvider.getInstance().realm(REALM_NAME).toRepresentation(); + assertThat(realm.getRealm(), is(REALM_NAME)); + assertThat(realm.isEnabled(), is(true)); + assertThat(realm.isRegistrationAllowed(), is(true)); + assertThat(realm.isRegistrationEmailAsUsername(), is(true)); + + final ClientRepresentation client = keycloakRepository.getClient(REALM_NAME, "technical-client"); + assertThat(client.getClientId(), is("technical-client")); + + UserRepresentation user = keycloakProvider.getInstance().realm(REALM_NAME) + .clients() + .get(client.getId()) + .getServiceAccountUser(); + assertThat(user.getUsername(), is("service-account-technical-client")); + + List clientLevelRoles = keycloakRepository.getServiceAccountUserClientLevelRoles( + REALM_NAME, client.getClientId(), "moped-client"); + assertThat(clientLevelRoles, containsInAnyOrder("test_client_role", "other_test_client_role")); + + List keycloakNativeClientLevelRoles = keycloakRepository.getServiceAccountUserClientLevelRoles( + REALM_NAME, client.getClientId(), "realm-management"); + assertThat(keycloakNativeClientLevelRoles, contains("view-realm")); + + } + @Test @Order(50) void shouldUpdateUserWithEmailAsRegistration() throws IOException { diff --git a/src/test/resources/import-files/users/60.2_update_realm_add_clientl_with_service_account.json b/src/test/resources/import-files/users/60.2_update_realm_add_clientl_with_service_account.json new file mode 100644 index 000000000..176149e29 --- /dev/null +++ b/src/test/resources/import-files/users/60.2_update_realm_add_clientl_with_service_account.json @@ -0,0 +1,84 @@ +{ + "enabled": true, + "realm": "realmWithUsers", + "registrationAllowed": true, + "registrationEmailAsUsername": true, + "roles": { + "client": { + "moped-client": [ + { + "name": "test_client_role", + "description": "My updated moped-client role", + "composite": false, + "clientRole": true + }, + { + "name": "other_test_client_role", + "description": "My changed other moped-client role", + "composite": false, + "clientRole": true + } + ] + } + }, + "clients": [ + { + "clientId": "technical-client", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": false, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": true, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "defaultClientScopes": [ + "role_list", + "roles" + ], + "optionalClientScopes": [] + }, + { + "clientId": "moped-client", + "name": "moped-client", + "description": "Moped-Client", + "enabled": true, + "clientAuthenticatorType": "client-secret", + "secret": "my-special-client-secret", + "bearerOnly": true, + "redirectUris": [], + "webOrigins": [] + } + ], + "users": [ + { + "username": "service-account-technical-client", + "enabled": true, + "totp": false, + "emailVerified": false, + "serviceAccountClientId": "technical-client", + "clientRoles": { + "account": [ + "manage-account", + "view-profile" + ], + "moped-client": [ + "test_client_role", + "other_test_client_role" + ], + "realm-management": [ + "view-realm" + ] + }, + "notBefore": 0 + } + ] +}