* fix: closes #21095 * Added overloaded version of GroupUtils.toGroupHierarchy with additional full parameter.
This commit is contained in:
parent
561bd65b4a
commit
817f129484
4 changed files with 96 additions and 60 deletions
|
@ -1,8 +1,6 @@
|
||||||
package org.keycloak.admin.ui.rest;
|
package org.keycloak.admin.ui.rest;
|
||||||
|
|
||||||
import static org.keycloak.models.utils.ModelToRepresentation.toRepresentation;
|
|
||||||
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
import jakarta.ws.rs.Consumes;
|
import jakarta.ws.rs.Consumes;
|
||||||
import jakarta.ws.rs.DefaultValue;
|
import jakarta.ws.rs.DefaultValue;
|
||||||
|
@ -15,15 +13,13 @@ import org.eclipse.microprofile.openapi.annotations.enums.SchemaType;
|
||||||
import org.eclipse.microprofile.openapi.annotations.media.Content;
|
import org.eclipse.microprofile.openapi.annotations.media.Content;
|
||||||
import org.eclipse.microprofile.openapi.annotations.media.Schema;
|
import org.eclipse.microprofile.openapi.annotations.media.Schema;
|
||||||
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
|
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
|
||||||
import org.keycloak.common.Profile;
|
|
||||||
import org.keycloak.models.GroupModel;
|
import org.keycloak.models.GroupModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.utils.ModelToRepresentation;
|
|
||||||
import org.keycloak.representations.idm.GroupRepresentation;
|
import org.keycloak.representations.idm.GroupRepresentation;
|
||||||
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
|
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
|
||||||
import org.keycloak.services.resources.admin.permissions.GroupPermissionEvaluator;
|
import org.keycloak.services.resources.admin.permissions.GroupPermissionEvaluator;
|
||||||
import org.keycloak.utils.StringUtil;
|
import org.keycloak.utils.GroupUtils;
|
||||||
|
|
||||||
public class GroupsResource {
|
public class GroupsResource {
|
||||||
private final KeycloakSession session;
|
private final KeycloakSession session;
|
||||||
|
@ -68,49 +64,6 @@ public class GroupsResource {
|
||||||
|
|
||||||
boolean canViewGlobal = groupsEvaluator.canView();
|
boolean canViewGlobal = groupsEvaluator.canView();
|
||||||
return stream.filter(group -> canViewGlobal || groupsEvaluator.canView(group))
|
return stream.filter(group -> canViewGlobal || groupsEvaluator.canView(group))
|
||||||
.map(group -> toGroupHierarchy(group, search, exact));
|
.map(group -> GroupUtils.toGroupHierarchy(groupsEvaluator, group, search, exact));
|
||||||
}
|
|
||||||
|
|
||||||
private GroupRepresentation toGroupHierarchy(GroupModel group, final String search, boolean exact) {
|
|
||||||
GroupRepresentation rep = toRepresentation(group, true);
|
|
||||||
rep.setSubGroups(group.getSubGroupsStream().filter(g ->
|
|
||||||
groupMatchesSearchOrIsPathElement(
|
|
||||||
g, search
|
|
||||||
)
|
|
||||||
).map(subGroup ->
|
|
||||||
ModelToRepresentation.toGroupHierarchy(
|
|
||||||
subGroup, true, search, exact
|
|
||||||
)
|
|
||||||
|
|
||||||
).collect(Collectors.toList()));
|
|
||||||
|
|
||||||
if (Profile.isFeatureEnabled(Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ)) {
|
|
||||||
setAccess(group, rep);
|
|
||||||
}
|
|
||||||
|
|
||||||
return rep;
|
|
||||||
}
|
|
||||||
|
|
||||||
// set fine-grained access for each group in the tree
|
|
||||||
private void setAccess(GroupModel groupTree, GroupRepresentation rootGroup) {
|
|
||||||
if (rootGroup == null) return;
|
|
||||||
|
|
||||||
rootGroup.setAccess(auth.groups().getAccess(groupTree));
|
|
||||||
|
|
||||||
rootGroup.getSubGroups().stream().forEach(subGroup -> {
|
|
||||||
GroupModel foundGroupModel = groupTree.getSubGroupsStream().filter(g -> g.getId().equals(subGroup.getId())).findFirst().get();
|
|
||||||
setAccess(foundGroupModel, subGroup);
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -151,6 +151,13 @@ public class ModelToRepresentation {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Stream<GroupRepresentation> searchGroupsByAttributes(KeycloakSession session, RealmModel realm, boolean full, boolean populateHierarchy, Map<String,String> attributes, Integer first, Integer max) {
|
public static Stream<GroupRepresentation> searchGroupsByAttributes(KeycloakSession session, RealmModel realm, boolean full, boolean populateHierarchy, Map<String,String> attributes, Integer first, Integer max) {
|
||||||
|
Stream<GroupModel> groups = searchGroupModelsByAttributes(session, realm, full, populateHierarchy, attributes, first, max);
|
||||||
|
// and then turn the result into GroupRepresentations creating whole hierarchy of child groups for each root group
|
||||||
|
return groups.map(g -> toGroupHierarchy(g, full, attributes));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Stream<GroupModel> searchGroupModelsByAttributes(KeycloakSession session, RealmModel realm, boolean full, boolean populateHierarchy, Map<String,String> attributes, Integer first, Integer max) {
|
||||||
Stream<GroupModel> groups = session.groups().searchGroupsByAttributes(realm, attributes, first, max);
|
Stream<GroupModel> groups = session.groups().searchGroupsByAttributes(realm, attributes, first, max);
|
||||||
if(populateHierarchy) {
|
if(populateHierarchy) {
|
||||||
groups = groups
|
groups = groups
|
||||||
|
@ -166,13 +173,16 @@ public class ModelToRepresentation {
|
||||||
// More child groups of one root can fulfill the search, so we need to filter duplicates
|
// More child groups of one root can fulfill the search, so we need to filter duplicates
|
||||||
.filter(StreamsUtil.distinctByKey(GroupModel::getId));
|
.filter(StreamsUtil.distinctByKey(GroupModel::getId));
|
||||||
}
|
}
|
||||||
// and then turn the result into GroupRepresentations creating whole hierarchy of child groups for each root group
|
return groups;
|
||||||
return groups.map(g -> toGroupHierarchy(g, full, attributes));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Stream<GroupRepresentation> searchForGroupByName(KeycloakSession session, RealmModel realm, boolean full, String search, Boolean exact, Integer first, Integer max) {
|
public static Stream<GroupRepresentation> searchForGroupByName(KeycloakSession session, RealmModel realm, boolean full, String search, Boolean exact, Integer first, Integer max) {
|
||||||
return session.groups().searchForGroupByNameStream(realm, search, exact, first, max)
|
return searchForGroupModelByName(session, realm, full, search, exact, first, max)
|
||||||
.map(g -> toGroupHierarchy(g, full, search, exact));
|
.map(g -> toGroupHierarchy(g, full, search, exact));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Stream<GroupModel> searchForGroupModelByName(KeycloakSession session, RealmModel realm, boolean full, String search, Boolean exact, Integer first, Integer max) {
|
||||||
|
return session.groups().searchForGroupByNameStream(realm, search, exact, first, max);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Stream<GroupRepresentation> searchForGroupByName(UserModel user, boolean full, String search, Integer first, Integer max) {
|
public static Stream<GroupRepresentation> searchForGroupByName(UserModel user, boolean full, String search, Integer first, Integer max) {
|
||||||
|
@ -181,8 +191,12 @@ public class ModelToRepresentation {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Stream<GroupRepresentation> toGroupHierarchy(RealmModel realm, boolean full, Integer first, Integer max) {
|
public static Stream<GroupRepresentation> toGroupHierarchy(RealmModel realm, boolean full, Integer first, Integer max) {
|
||||||
return realm.getTopLevelGroupsStream(first, max)
|
return toGroupModelHierarchy(realm, full, first, max)
|
||||||
.map(g -> toGroupHierarchy(g, full));
|
.map(g -> toGroupHierarchy(g, full));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Stream<GroupModel> toGroupModelHierarchy(RealmModel realm, boolean full, Integer first, Integer max) {
|
||||||
|
return realm.getTopLevelGroupsStream(first, max);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Stream<GroupRepresentation> toGroupHierarchy(UserModel user, boolean full, Integer first, Integer max) {
|
public static Stream<GroupRepresentation> toGroupHierarchy(UserModel user, boolean full, Integer first, Integer max) {
|
||||||
|
|
|
@ -21,6 +21,7 @@ import org.eclipse.microprofile.openapi.annotations.extensions.Extension;
|
||||||
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
|
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
|
||||||
import org.jboss.resteasy.annotations.cache.NoCache;
|
import org.jboss.resteasy.annotations.cache.NoCache;
|
||||||
import jakarta.ws.rs.NotFoundException;
|
import jakarta.ws.rs.NotFoundException;
|
||||||
|
|
||||||
import org.keycloak.common.util.ObjectUtil;
|
import org.keycloak.common.util.ObjectUtil;
|
||||||
import org.keycloak.events.admin.OperationType;
|
import org.keycloak.events.admin.OperationType;
|
||||||
import org.keycloak.events.admin.ResourceType;
|
import org.keycloak.events.admin.ResourceType;
|
||||||
|
@ -33,6 +34,8 @@ import org.keycloak.representations.idm.GroupRepresentation;
|
||||||
import org.keycloak.services.ErrorResponse;
|
import org.keycloak.services.ErrorResponse;
|
||||||
import org.keycloak.services.resources.KeycloakOpenAPI;
|
import org.keycloak.services.resources.KeycloakOpenAPI;
|
||||||
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
|
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
|
||||||
|
import org.keycloak.services.resources.admin.permissions.GroupPermissionEvaluator;
|
||||||
|
import org.keycloak.utils.GroupUtils;
|
||||||
import org.keycloak.utils.SearchQueryUtils;
|
import org.keycloak.utils.SearchQueryUtils;
|
||||||
|
|
||||||
import jakarta.ws.rs.Consumes;
|
import jakarta.ws.rs.Consumes;
|
||||||
|
@ -88,18 +91,24 @@ public class GroupsResource {
|
||||||
@QueryParam("max") Integer maxResults,
|
@QueryParam("max") Integer maxResults,
|
||||||
@QueryParam("briefRepresentation") @DefaultValue("true") boolean briefRepresentation,
|
@QueryParam("briefRepresentation") @DefaultValue("true") boolean briefRepresentation,
|
||||||
@QueryParam("populateHierarchy") @DefaultValue("true") boolean populateHierarchy) {
|
@QueryParam("populateHierarchy") @DefaultValue("true") boolean populateHierarchy) {
|
||||||
auth.groups().requireList();
|
GroupPermissionEvaluator groupsEvaluator = auth.groups();
|
||||||
|
groupsEvaluator.requireList();
|
||||||
|
|
||||||
|
Stream<GroupModel> stream = null;
|
||||||
if (Objects.nonNull(searchQuery)) {
|
if (Objects.nonNull(searchQuery)) {
|
||||||
Map<String, String> attributes = SearchQueryUtils.getFields(searchQuery);
|
Map<String, String> attributes = SearchQueryUtils.getFields(searchQuery);
|
||||||
return ModelToRepresentation.searchGroupsByAttributes(session, realm, !briefRepresentation, populateHierarchy, attributes, firstResult, maxResults);
|
stream = ModelToRepresentation.searchGroupModelsByAttributes(session, realm, !briefRepresentation, populateHierarchy, attributes, firstResult, maxResults);
|
||||||
} else if (Objects.nonNull(search)) {
|
} else if (Objects.nonNull(search)) {
|
||||||
return ModelToRepresentation.searchForGroupByName(session, realm, !briefRepresentation, search.trim(), exact, firstResult, maxResults);
|
stream = ModelToRepresentation.searchForGroupModelByName(session, realm, !briefRepresentation, search.trim(), exact, firstResult, maxResults);
|
||||||
} else if(Objects.nonNull(firstResult) && Objects.nonNull(maxResults)) {
|
} else if(Objects.nonNull(firstResult) && Objects.nonNull(maxResults)) {
|
||||||
return ModelToRepresentation.toGroupHierarchy(realm, !briefRepresentation, firstResult, maxResults);
|
stream = ModelToRepresentation.toGroupModelHierarchy(realm, !briefRepresentation, firstResult, maxResults);
|
||||||
} else {
|
} else {
|
||||||
return ModelToRepresentation.toGroupHierarchy(realm, !briefRepresentation);
|
stream = realm.getTopLevelGroupsStream();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
boolean canViewGlobal = groupsEvaluator.canView();
|
||||||
|
return stream.filter(group -> canViewGlobal || groupsEvaluator.canView(group))
|
||||||
|
.map(group -> GroupUtils.toGroupHierarchy(groupsEvaluator, group, search, exact, !briefRepresentation));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
60
services/src/main/java/org/keycloak/utils/GroupUtils.java
Normal file
60
services/src/main/java/org/keycloak/utils/GroupUtils.java
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
package org.keycloak.utils;
|
||||||
|
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import org.keycloak.common.Profile;
|
||||||
|
import org.keycloak.models.GroupModel;
|
||||||
|
import org.keycloak.models.utils.ModelToRepresentation;
|
||||||
|
import org.keycloak.representations.idm.GroupRepresentation;
|
||||||
|
import org.keycloak.services.resources.admin.permissions.GroupPermissionEvaluator;
|
||||||
|
|
||||||
|
public class GroupUtils {
|
||||||
|
// Moved out from org.keycloak.admin.ui.rest.GroupsResource
|
||||||
|
public static GroupRepresentation toGroupHierarchy(GroupPermissionEvaluator groupsEvaluator, GroupModel group, final String search, boolean exact) {
|
||||||
|
return toGroupHierarchy(groupsEvaluator, group, search, exact, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static GroupRepresentation toGroupHierarchy(GroupPermissionEvaluator groupsEvaluator, GroupModel group, final String search, boolean exact, boolean full) {
|
||||||
|
GroupRepresentation rep = ModelToRepresentation.toRepresentation(group, full);
|
||||||
|
rep.setSubGroups(group.getSubGroupsStream().filter(g ->
|
||||||
|
groupMatchesSearchOrIsPathElement(
|
||||||
|
g, search
|
||||||
|
)
|
||||||
|
).map(subGroup ->
|
||||||
|
ModelToRepresentation.toGroupHierarchy(
|
||||||
|
subGroup, full, search, exact
|
||||||
|
)
|
||||||
|
|
||||||
|
).collect(Collectors.toList()));
|
||||||
|
|
||||||
|
if (Profile.isFeatureEnabled(Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ)) {
|
||||||
|
setAccess(groupsEvaluator, group, rep);
|
||||||
|
}
|
||||||
|
|
||||||
|
return rep;
|
||||||
|
}
|
||||||
|
|
||||||
|
//From org.keycloak.admin.ui.rest.GroupsResource
|
||||||
|
// set fine-grained access for each group in the tree
|
||||||
|
private static void setAccess(GroupPermissionEvaluator groupsEvaluator, GroupModel groupTree, GroupRepresentation rootGroup) {
|
||||||
|
if (rootGroup == null) return;
|
||||||
|
|
||||||
|
rootGroup.setAccess(groupsEvaluator.getAccess(groupTree));
|
||||||
|
|
||||||
|
rootGroup.getSubGroups().stream().forEach(subGroup -> {
|
||||||
|
GroupModel foundGroupModel = groupTree.getSubGroupsStream().filter(g -> g.getId().equals(subGroup.getId())).findFirst().get();
|
||||||
|
setAccess(groupsEvaluator, foundGroupModel, subGroup);
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue