GroupUtils.java
package org.keycloak.utils;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Stream;
import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.representations.idm.GroupRepresentation;
import org.keycloak.services.resources.admin.permissions.GroupPermissionEvaluator;
public class GroupUtils {
/**
* This method takes the provided groups and attempts to load their parents all the way to the root group while maintaining the hierarchy data
* for each GroupRepresentation object. Each resultant GroupRepresentation object in the stream should contain relevant subgroups to the originally
* provided groups
* @param session The active keycloak session
* @param realm The realm to operate on
* @param groups The groups that we want to populate the hierarchy for
* @return A stream of groups that contain all relevant groups from the root down with no extra siblings
*/
public static Stream<GroupRepresentation> populateGroupHierarchyFromSubGroups(KeycloakSession session, RealmModel realm, Stream<GroupModel> groups, boolean full, GroupPermissionEvaluator groupEvaluator) {
Map<String, GroupRepresentation> groupIdToGroups = new HashMap<>();
groups.forEach(group -> {
//TODO GROUPS do permissions work in such a way that if you can view the children you can definitely view the parents?
if(!groupEvaluator.canView() && !groupEvaluator.canView(group)) return;
GroupRepresentation currGroup = toRepresentation(groupEvaluator, group, full);
populateSubGroupCount(group, currGroup);
groupIdToGroups.putIfAbsent(currGroup.getId(), currGroup);
while(currGroup.getParentId() != null) {
GroupModel parentModel = session.groups().getGroupById(realm, currGroup.getParentId());
//TODO GROUPS not sure if this is even necessary but if somehow you can't view the parent we need to remove the child and move on
if(!groupEvaluator.canView() && !groupEvaluator.canView(parentModel)) {
groupIdToGroups.remove(currGroup.getId());
break;
}
GroupRepresentation parent = groupIdToGroups.computeIfAbsent(currGroup.getParentId(),
id -> toRepresentation(groupEvaluator, parentModel, full));
populateSubGroupCount(parentModel, parent);
GroupRepresentation finalCurrGroup = currGroup;
// check the parent for existing subgroups that match the group we're currently operating on and merge them if needed
Optional<GroupRepresentation> duplicateGroup = parent.getSubGroups() == null ?
Optional.empty() : parent.getSubGroups().stream().filter(g -> g.equals(finalCurrGroup)).findFirst();
if(duplicateGroup.isPresent()) {
duplicateGroup.get().merge(currGroup);
} else {
parent.getSubGroups().add(currGroup);
}
groupIdToGroups.remove(currGroup.getId());
currGroup = parent;
}
});
return groupIdToGroups.values().stream().sorted(Comparator.comparing(GroupRepresentation::getName));
}
/**
* This method's purpose is to look up the subgroup count of a Group and populate it on the representation. This has been kept separate from
* {@link #toRepresentation} in order to keep database lookups separate from a function that aims to only convert objects
* A way of cohesively ensuring that a GroupRepresentation always has a group count should be considered
*
* @param group model
* @param representation group representation
* @return
*/
public static GroupRepresentation populateSubGroupCount(GroupModel group, GroupRepresentation representation) {
representation.setSubGroupCount(group.getSubGroupsCount());
return representation;
}
//From org.keycloak.admin.ui.rest.GroupsResource
// set fine-grained access for each group in the tree
public static GroupRepresentation toRepresentation(GroupPermissionEvaluator groupsEvaluator, GroupModel groupTree, boolean full) {
GroupRepresentation rep = ModelToRepresentation.toRepresentation(groupTree, full);
rep.setAccess(groupsEvaluator.getAccess(groupTree));
return rep;
}
private static boolean groupMatchesSearchOrIsPathElement(GroupModel group, String search) {
if (StringUtil.isBlank(search)) {
return true;
}
if (group.getName().contains(search)) {
return true;
}
return group.getSubGroupsStream().findAny().isPresent();
}
}