Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PoC for Keycloak 23.0.x support #952

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# Used in docker-compose
# shellcheck disable=SC2034
KEYCLOAK_VERSION=22.0.4
KEYCLOAK_VERSION=23.0.0
5 changes: 2 additions & 3 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,9 @@ jobs:
env:
# we keep 18.0.2 for backwards compatibility with RH-SSO 7.6
- KEYCLOAK_VERSION: 18.0.2
- KEYCLOAK_VERSION: 19.0.3
- KEYCLOAK_VERSION: 20.0.5
- KEYCLOAK_VERSION: 21.1.1
- KEYCLOAK_VERSION: 22.0.4
- KEYCLOAK_VERSION: 22.0.0
- KEYCLOAK_VERSION: 23.0.0
steps:
- uses: actions/checkout@v3
with:
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ 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]
- Support for Keycloak 23
- Updated CI to use Keycloak 23.0.0
- Fix issue[#948](/adorsys/keycloak-config-cli/issues/948)
- UserProfile handling now uses UPConfig instead of a simple Map
- Group handling adapted to new logic in Keycloak

## [5.9.0] - 2023-10-13
- Updated CI to use Keycloak 22.0.4
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ FROM ${BUILDER_IMAGE} AS BUILDER

WORKDIR /app/

ARG KEYCLOAK_VERSION=22.0.4
ARG KEYCLOAK_VERSION=23.0.0
ARG MAVEN_CLI_OPTS="-ntp -B"

COPY .mvn .mvn
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>

<keycloak.version>22.0.4</keycloak.version>
<keycloak.version>23.0.0</keycloak.version>

<checkstyle-plugin.version>3.2.0</checkstyle-plugin.version>
<checkstyle.version>10.0</checkstyle.version>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,17 @@
import com.fasterxml.jackson.annotation.JsonSetter;
import org.keycloak.representations.idm.AuthenticationFlowRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.userprofile.config.UPConfig;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

@Component
public class RealmImport extends RealmRepresentation {
private List<AuthenticationFlowImport> authenticationFlowImports;

private Map<String, List<Map<String, Object>>> userProfile;
private UPConfig userProfile;

private String checksum;

Expand All @@ -54,11 +54,11 @@ public void setAuthenticationFlowImports(List<AuthenticationFlowImport> authenti

@SuppressWarnings("unused")
@JsonSetter("userProfile")
public void setUserProfile(Map<String, List<Map<String, Object>>> userProfile) {
public void setUserProfile(UPConfig userProfile) {
this.userProfile = userProfile;
}

public Map<String, List<Map<String, Object>>> getUserProfile() {
public UPConfig getUserProfile() {
return userProfile;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,13 +111,12 @@ public void addSubGroup(String realmName, String parentGroupId, GroupRepresentat
}

public GroupRepresentation getSubGroupByName(String realmName, String parentGroupId, String name) {
GroupRepresentation existingGroup = getResourceById(realmName, parentGroupId).toRepresentation();

return existingGroup.getSubGroups()
GroupRepresentation subGroup = getSubGroups(realmName, parentGroupId)
.stream()
.filter(subgroup -> Objects.equals(subgroup.getName(), name))
.findFirst()
.orElse(null);
return subGroup;
}

public void addRealmRoles(String realmName, String groupId, List<String> roleNames) {
Expand Down Expand Up @@ -239,4 +238,12 @@ private GroupResource getResourceById(String realmName, String groupId) {
.groups()
.group(groupId);
}

public List<GroupRepresentation> getSubGroups(String realmName, String parentGroupId) {
// TODO make max size configurable
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use subGroupCount from parent group as max

// note this is currently the only way to populate the subgroup information
// The meaning of the briefRepresentation was apprently inverted by mistake in Keycloak 23.0.0
// see: https://github.com/keycloak/keycloak/issues/25096
return getResourceById(realmName, parentGroupId).getSubGroups(0, 100, true);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,11 @@
import de.adorsys.keycloak.config.exception.KeycloakRepositoryException;
import de.adorsys.keycloak.config.util.JsonUtil;
import org.keycloak.admin.client.resource.UserProfileResource;
import org.keycloak.representations.userprofile.config.UPConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.util.Optional;

import jakarta.ws.rs.core.Response;

@Component
public class UserProfileRepository {
Expand All @@ -47,7 +43,7 @@ public UserProfileRepository(RealmRepository realmRepository) {
this.realmRepository = realmRepository;
}

public void updateUserProfile(String realm, boolean newUserProfileEnabled, String newUserProfileConfiguration) {
public void updateUserProfile(String realm, boolean newUserProfileEnabled, UPConfig newUserProfileConfiguration) {

var userProfileResource = getResource(realm);
if (userProfileResource == null) {
Expand All @@ -57,16 +53,15 @@ public void updateUserProfile(String realm, boolean newUserProfileEnabled, Strin

if (!newUserProfileEnabled) {
logger.trace("UserProfile is explicitly disabled, removing configuration.");
try (var response = userProfileResource.update(null)) {
logger.trace("UserProfile configuration removed.");
}
userProfileResource.update(null);
logger.trace("UserProfile configuration removed.");
return;
}

var realmAttributes = realmRepository.get(realm).getAttributesOrEmpty();
var currentUserProfileConfiguration = Optional.ofNullable(userProfileResource.getConfiguration()).orElse("");
if (!StringUtils.hasText(currentUserProfileConfiguration)) {
logger.warn("UserProfile is enabled, but no configuration string provided.");
var currentUserProfileConfiguration = userProfileResource.getConfiguration();
if (currentUserProfileConfiguration == null) {
logger.warn("UserProfile is enabled, but no configuration provided.");
return;
}

Expand All @@ -82,18 +77,18 @@ public void updateUserProfile(String realm, boolean newUserProfileEnabled, Strin
return;
}

try (var updateUserProfileResponse = userProfileResource.update(newUserProfileConfiguration)) {
if (!updateUserProfileResponse.getStatusInfo().equals(Response.Status.OK)) {
throw new KeycloakRepositoryException("Could not update UserProfile Definition");
}
try {
userProfileResource.update(newUserProfileConfiguration);
} catch (Exception ex) {
throw new KeycloakRepositoryException("Could not update UserProfile Definition", ex);
}

logger.trace("UserProfile updated.");
}

private boolean hasUserProfileConfigurationChanged(String newUserProfileConfiguration, String currentUserProfileConfiguration) {
var newValue = JsonUtil.getJsonOrNullNode(newUserProfileConfiguration);
var currentValue = JsonUtil.getJsonOrNullNode(currentUserProfileConfiguration);
private boolean hasUserProfileConfigurationChanged(UPConfig newUserProfileConfiguration, UPConfig currentUserProfileConfiguration) {
var newValue = JsonUtil.toJson(newUserProfileConfiguration);
var currentValue = JsonUtil.toJson(currentUserProfileConfiguration);
return !currentValue.equals(newValue);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.util.*;
import java.util.function.Consumer;
Expand Down Expand Up @@ -77,18 +78,44 @@ private void deleteGroupsMissingInImport(
List<GroupRepresentation> importedGroups,
List<GroupRepresentation> existingGroups
) {
Set<String> importedGroupNames = importedGroups.stream()
.map(GroupRepresentation::getName)
.collect(Collectors.toSet());
Map<String, GroupRepresentation> groupPathMap = new HashMap<>();
for (GroupRepresentation groupRep : importedGroups) {
buildGroupPathLookupMap(groupPathMap, groupRep, "/");
}

for (GroupRepresentation existingGroup : existingGroups) {
if (importedGroupNames.contains(existingGroup.getName())) continue;
if (groupPathMap.containsKey("/" + existingGroup.getName())) {
if (existingGroup.getSubGroupCount() > 0) {
tryRecursivelyDeletingDanglingSubGroups(groupPathMap, realmName, existingGroup.getId());
}
continue;
}

logger.debug("Delete group '{}' in realm '{}'", existingGroup.getName(), realmName);
groupRepository.deleteGroup(realmName, existingGroup.getId());
}
}

private void tryRecursivelyDeletingDanglingSubGroups(Map<String, GroupRepresentation> groupPathMap, String realmName, String parentGroupId) {
List<GroupRepresentation> subGroups = groupRepository.getSubGroups(realmName, parentGroupId);
for (GroupRepresentation subGroup : subGroups) {
String path = subGroup.getPath();
if (!groupPathMap.containsKey(path)) {
groupRepository.deleteGroup(realmName, subGroup.getId());
} else {
tryRecursivelyDeletingDanglingSubGroups(groupPathMap, realmName, subGroup.getId());
}
}
}

private void buildGroupPathLookupMap(Map<String, GroupRepresentation> map, GroupRepresentation currentGroup, String prefix) {
String groupPath = prefix + currentGroup.getName();
map.put(groupPath, currentGroup);
for (GroupRepresentation subGroup : currentGroup.getSubGroups()) {
buildGroupPathLookupMap(map, subGroup, groupPath + "/");
}
}

private void createOrUpdateRealmGroup(String realmName, GroupRepresentation group) {
String groupName = group.getName();

Expand Down Expand Up @@ -243,26 +270,32 @@ private void updateGroupRealmRoles(String realmName, String groupId, List<String
}

private List<String> estimateRealmRolesToRemove(List<String> realmRoles, List<String> existingRealmRolesNames) {
List<String> realmRoleNamesToRemove = new ArrayList<>();

if (existingRealmRolesNames == null) {
return Collections.emptyList();
}

List<String> realmRoleNamesToRemove = new ArrayList<>();
for (String existingRealmRolesName : existingRealmRolesNames) {
if (!realmRoles.contains(existingRealmRolesName)) {
realmRoleNamesToRemove.add(existingRealmRolesName);
}
}

return realmRoleNamesToRemove;
}

private List<String> estimateRealmRolesToAdd(List<String> realmRoles, List<String> existingRealmRolesNames) {
List<String> realmRoleNamesToAdd = new ArrayList<>();

if (existingRealmRolesNames == null) {
return realmRoles;
}

List<String> realmRoleNamesToAdd = new ArrayList<>();
for (String realmRoleName : realmRoles) {
if (!existingRealmRolesNames.contains(realmRoleName)) {
realmRoleNamesToAdd.add(realmRoleName);
}
}

return realmRoleNamesToAdd;
}

Expand All @@ -285,10 +318,17 @@ private void updateClientRoles(
String clientId = clientRole.getKey();
List<String> clientRoleNames = clientRole.getValue();

List<String> existingClientRoleNamesForClient = existingClientRoleNames.get(clientId);
List<String> clientRoleNamesToAdd;
List<String> clientRoleNamesToRemove;

List<String> clientRoleNamesToAdd = estimateClientRolesToAdd(existingClientRoleNamesForClient, clientRoleNames);
List<String> clientRoleNamesToRemove = estimateClientRolesToRemove(existingClientRoleNamesForClient, clientRoleNames);
if (existingClientRoleNames != null) {
List<String> existingClientRoleNamesForClient = existingClientRoleNames.get(clientId);
clientRoleNamesToAdd = estimateClientRolesToAdd(existingClientRoleNamesForClient, clientRoleNames);
clientRoleNamesToRemove = estimateClientRolesToRemove(existingClientRoleNamesForClient, clientRoleNames);
} else {
clientRoleNamesToAdd = clientRoleNames;
clientRoleNamesToRemove = Collections.emptyList();
}

groupRepository.addClientRoles(realmName, groupId, clientId, clientRoleNamesToAdd);
groupRepository.removeClientRoles(realmName, groupId, clientId, clientRoleNamesToRemove);
Expand All @@ -301,6 +341,11 @@ private void deleteClientRolesMissingInImport(
Map<String, List<String>> existingClientRoleNames,
Map<String, List<String>> groupClientRoles
) {

if (CollectionUtils.isEmpty(existingClientRoleNames)) {
return;
}

for (Map.Entry<String, List<String>> existingClientRoleNamesEntry : existingClientRoleNames.entrySet()) {
String clientId = existingClientRoleNamesEntry.getKey();
List<String> clientRoleNames = existingClientRoleNamesEntry.getValue();
Expand All @@ -312,34 +357,37 @@ private void deleteClientRolesMissingInImport(
}

private List<String> estimateClientRolesToRemove(List<String> existingClientRoleNamesForClient, List<String> clientRoleNamesFromImport) {
List<String> clientRoleNamesToRemove = new ArrayList<>();

if (existingClientRoleNamesForClient != null) {
for (String existingClientRoleNameForClient : existingClientRoleNamesForClient) {
if (!clientRoleNamesFromImport.contains(existingClientRoleNameForClient)) {
clientRoleNamesToRemove.add(existingClientRoleNameForClient);
}
}
if (CollectionUtils.isEmpty(existingClientRoleNamesForClient)) {
return Collections.emptyList();
}

List<String> clientRoleNamesToRemove = new ArrayList<>();
for (String existingClientRoleNameForClient : existingClientRoleNamesForClient) {
if (!clientRoleNamesFromImport.contains(existingClientRoleNameForClient)) {
clientRoleNamesToRemove.add(existingClientRoleNameForClient);
}
}
return clientRoleNamesToRemove;
}

private List<String> estimateClientRolesToAdd(List<String> existingClientRoleNamesForClient, List<String> clientRoleNamesFromImport) {
List<String> clientRoleNamesToAdd = new ArrayList<>();

if (CollectionUtils.isEmpty(existingClientRoleNamesForClient)) {
return clientRoleNamesFromImport;
}

List<String> clientRoleNamesToAdd = new ArrayList<>();
for (String clientRoleName : clientRoleNamesFromImport) {
if (existingClientRoleNamesForClient == null || !existingClientRoleNamesForClient.contains(clientRoleName)) {
if (!existingClientRoleNamesForClient.contains(clientRoleName)) {
clientRoleNamesToAdd.add(clientRoleName);
}
}

return clientRoleNamesToAdd;
}

private void updateSubGroups(String realmName, String parentGroupId, List<GroupRepresentation> subGroups) {
GroupRepresentation existingGroup = groupRepository.getGroupById(realmName, parentGroupId);
List<GroupRepresentation> existingSubGroups = existingGroup.getSubGroups();
List<GroupRepresentation> existingSubGroups = groupRepository.getSubGroups(realmName, parentGroupId);

deleteAllSubGroupsMissingInImport(realmName, subGroups, existingSubGroups);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@

import de.adorsys.keycloak.config.model.RealmImport;
import de.adorsys.keycloak.config.repository.UserProfileRepository;
import de.adorsys.keycloak.config.util.JsonUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
Expand Down Expand Up @@ -50,17 +49,7 @@ public void doImport(RealmImport realmImport) {
}

var userProfileEnabled = Boolean.parseBoolean(userProfileEnabledString);
var userProfileAttributeString = buildUserProfileConfigurationString(realmImport);

this.userProfileRepository.updateUserProfile(realmImport.getRealm(), userProfileEnabled, userProfileAttributeString);
}

private String buildUserProfileConfigurationString(RealmImport realmImport) {
var userProfile = realmImport.getUserProfile();
if (userProfile == null || userProfile.isEmpty()) {
return null;
}
return JsonUtil.toJson(userProfile);
this.userProfileRepository.updateUserProfile(realmImport.getRealm(), userProfileEnabled, realmImport.getUserProfile());
}

}
Loading
Loading