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/RealmRepository.java b/src/main/java/de/adorsys/keycloak/config/repository/RealmRepository.java index 8a6d44c48..9fd7e123f 100644 --- a/src/main/java/de/adorsys/keycloak/config/repository/RealmRepository.java +++ b/src/main/java/de/adorsys/keycloak/config/repository/RealmRepository.java @@ -23,6 +23,7 @@ import de.adorsys.keycloak.config.exception.KeycloakRepositoryException; import de.adorsys.keycloak.config.provider.KeycloakProvider; import de.adorsys.keycloak.config.util.ResponseUtil; +import org.apache.commons.lang3.ObjectUtils; import org.keycloak.admin.client.Keycloak; import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.admin.client.resource.RealmsResource; @@ -30,6 +31,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import java.util.HashMap; + import jakarta.ws.rs.NotFoundException; import jakarta.ws.rs.WebApplicationException; @@ -57,7 +60,10 @@ public RealmResource getResource(String realmName) { } public RealmRepresentation get(String realmName) { - return getResource(realmName).toRepresentation(); + final var realm = getResource(realmName).toRepresentation(); + realm.setAttributes(ObjectUtils.firstNonNull(realm.getAttributes(), new HashMap<>())); + realm.setEventsEnabled(ObjectUtils.firstNonNull(realm.isEventsEnabled(), false)); + return realm; } public void create(RealmRepresentation realm) { 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/UserImportService.java b/src/main/java/de/adorsys/keycloak/config/service/UserImportService.java index a967142b6..07d3fdc03 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; 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 + } + ] +}