8000 Filter role mappings by visibility in full group listings by pedroigor · Pull Request #54914 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content
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
8000
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,30 @@ List<GroupRepresentation> groups(@QueryParam("search") String search,
@QueryParam("max") Integer max,
@QueryParam("briefRepresentation") @DefaultValue("true") boolean briefRepresentation);

/**
* Get groups by name with optional hierarchy population.
* @param search an exact or partial group name.
* @param exact whether to match the group name exactly.
* @param first index of the first element (pagination offset).
* @param max the maximum number of results.
* @param briefRepresentation whether to return only basic information rather than full representations,
* including role mappings and attributes.
* @param subGroupsCount whether to include subgroup counts.
* @param populateHierarchy whether to return matching groups within their parent hierarchy.
* If {@code false}, matching groups are returned as a flat list.
* @return A list containing the matching groups.
*/
@GET
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
List<GroupRepresentation> groups(@QueryParam("search") String search,
@QueryParam("exact") Boolean exact,
@QueryParam("first") Integer first,
@QueryParam("max") Integer max,
@QueryParam("briefRepresentation") @DefaultValue("true") boolean briefRepresentation,
@QueryParam("subGroupsCount") @DefaultValue("true") Boolean subGroupsCount,
@QueryParam("populateHierarchy") @DefaultValue("true") boolean populateHierarchy);

/**
* Counts all groups.
* @return A map containing key "count" with number of groups as value.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,12 @@ public Stream<GroupRepresentation> getGroups(@QueryParam("search") String search
}

if (populateHierarchy) {
return GroupUtils.populateGroupHierarchyFromSubGroups(session, realm, stream, !briefRepresentation, groupsEvaluator, subGroupsCount);
Stream<GroupRepresentation> hierarchy = GroupUtils.populateGroupHierarchyFromSubGroups(session, realm, stream, !briefRepresentation, groupsEvaluator, subGroupsCount);
if (!briefRepresentation) {
// filterRolesInRepresentation recurses into subgroups, so applying it to the roots covers the whole tree
hierarchy = hierarchy.peek(rep -> GroupUtils.filterRolesInRepresentation(rep, realm, session, auth));
}
return hierarchy;
}

if (!AdminPermissionsSchema.SCHEMA.isAdminPermissionsEnabled(realm)) {
Expand All @@ -121,6 +126,9 @@ public Stream<GroupRepresentation> getGroups(@QueryParam("search") String search

return stream.map(g -> {
GroupRepresentation rep = GroupUtils.toRepresentation(groupsEvaluator, g, !briefRepresentation);
if (!briefRepresentation) {
GroupUtils.filterRolesInRepresentation(rep, realm, session, auth);
}

if (subGroupsCount) {
return GroupUtils.populateSubGroupCount(g, rep);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1172,7 +1172,15 @@ public Stream<GroupRepresentation> groupMembership(@QueryParam("search") String
@QueryParam("max") Integer maxResults,
@QueryParam("briefRepresentation") @DefaultValue("true") boolean briefRepresentation) {
auth.users().requireView(user);
return user.getGroupsStream(search, firstResult, maxResults).filter(auth.groups()::canView).map(g -> ModelToRepresentation.toRepresentation(g, !briefRepresentation));
return user.getGroupsStream(search, firstResult, maxResults)
.filter(auth.groups()::canView)
.map(g -> {
GroupRepresentation rep = ModelToRepresentation.toRepresentation(g, !briefRepresentation);
if (!briefRepresentation) {
GroupUtils.filterRolesInRepresentation(rep, realm, session, auth);
}
return rep;
});
}

@GET
8000 Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

package org.keycloak.tests.admin;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
Expand Down Expand Up @@ -1144,6 +1145,7 @@ public void realmRoleMappingsVisibleWithManageUsers() {
.add(GroupBuilder.create().name("role-mapping-group").build()));
managedRealm1.cleanup().add(r -> r.groups().group(groupUuid).remove());
managedRealm1.admin().groups().group(groupUuid).roles().realmLevel().add(List.of(createdRole));
managedRealm1.admin().users().get(userUuid).joinGroup(groupUuid);

String clientUuid = ApiUtil.getCreatedId(managedRealm1.admin().clients()
.create(ClientBuilder.create().clientId("role-mapping-client").build()));
Expand All @@ -11 8000 54,6 +1156,14 @@ public void realmRoleMappingsVisibleWithManageUsers() {
managedRealm1.admin().users().get(userUuid).roles().clientLevel(clientUuid).add(List.of(clientRole));
managedRealm1.admin().groups().group(groupUuid).roles().clientLevel(clientUuid).add(List.of(clientRole));

String childUuid;
try (Response response = managedRealm1.admin().groups().group(groupUuid)
.subGroup(GroupBuilder.create().name("role-mapping-group-child").build())) {
childUuid = ApiUtil.getCreatedId(response);
}
managedRealm1.admin().groups().group(childUuid).roles().realmLevel().add(List.of(createdRole));
managedRealm1.admin().groups().group(childUuid).roles().clientLevel(clientUuid).add(List.of(clientRole));

RealmResource manageUsers = clients.get(AdminRoles.MANAGE_USERS).realm(REALM_NAME);

MappingsRepresentation userMappings = manageUsers.users().get(userUuid).roles().getAll();
Expand All @@ -1178,6 +1188,13 @@ public void realmRoleMappingsVisibleWithManageUsers() {
assertThat(groupRep.getClientRoles(), Matchers.hasKey("role-mapping-client"));
assertThat(groupRep.getClientRoles().get("role-mapping-client"), Matchers.hasItem(clientRoleName));

// full group listings follow the same rule
for (GroupRepresentation rep : groupListings(AdminRoles.MANAGE_USERS, userUuid, groupUuid, childUuid, false)) {
assertThat(rep.getRealmRoles(), Matchers.hasItem(roleName));
assertThat(rep.getClientRoles(), Matchers.hasKey("role-mapping-client"));
assertThat(rep.getClientRoles().get("role-mapping-client"), Matchers.hasItem(clientRoleName));
}

RealmResource viewUsers = clients.get(AdminRoles.VIEW_USERS).realm(REALM_NAME);

assertThat(realmRoleNames(viewUsers.users().get(userUuid).roles().getAll()),
Expand All @@ -1193,6 +1210,20 @@ public void realmRoleMappingsVisibleWithManageUsers() {
Matchers.not(Matchers.hasItem(roleName)));
assertThat(viewUsersGroupRep.getClientRoles() == null ? Map.<String, List<String>>of() : viewUsersGroupRep.getClientRoles(),
Matchers.not(Matchers.hasKey("role-mapping-client")));
for (GroupRepresentation rep : groupListings(AdminRoles.VIEW_USERS, userUuid, groupUuid, childUuid, false)) {
assertThat(rep.getRealmRoles() == null ? List.<String>of() : rep.getRealmRoles(),
Matchers.not(Matchers.hasItem(roleName)));
assertThat(rep.getClientRoles() == null ? Map.<String, List<String>>of() : rep.getClientRoles(),
Matchers.not(Matchers.hasKey("role-mapping-client")));
}

for (String adminRole : List.of(AdminRoles.MANAGE_USERS, AdminRoles.VIEW_USERS)) {
for (GroupRepresentation rep : groupListings(adminRole, userUuid, groupUuid, childUuid, true)) {
assertThat(rep.getRealmRoles(), Matchers.nullValue());
assertThat(rep.getClientRoles(), Matchers.nullValue());
assertThat(rep.getAttributes(), Matchers.nullValue());
}
}

// being able to view (and map) the role does not grant management of the role itself
String roleId = createdRole.getId();
Expand All @@ -1216,6 +1247,38 @@ private static List<String> roleNames(List<RoleRepresentation> roles) {
return roles.stream().map(RoleRepresentation::getName).toList();
}

/**
* Returns parent and child representations as produced by the listing endpoints:
* {@code GET /groups} (hierarchy and flat) and {@code GET /users/{id}/groups}.
*/
private List<GroupRepresentation> groupListings(String adminRole, String userUuid, String groupUuid, String childUuid, boolean briefRepresentation) {
RealmResource realm = clients.get(adminRole).realm(REALM_NAME);
List<GroupRepresentation> reps = new ArrayList<>();
reps.add(singleGroup(realm.users().get(userUuid).groups("role-mapping-group", null, null, briefRepresentation), groupUuid));

for (String groupName : List.of("role-mapping-group", "role-mapping-group-child")) {
boolean searchChild = groupName.endsWith("-child");
GroupRepresentation root = singleGroup(
realm.groups().groups(groupName, true, null, null, briefRepresentation), groupUuid);
reps.add(root);
if (searchChild) {
reps.add(singleGroup(root.getSubGroups(), childUuid));
}

List<GroupRepresentation> flat = realm.groups().groups(groupName, true, null, null, briefRepresentation, true, false);
reps.add(singleGroup(flat, searchChild ? childUuid : groupUuid));
}

return reps;
}

private static GroupRepresentation singleGroup(List<GroupRepresentation> groups, String groupUuid) {
assertThat(groups, Matchers.hasSize(1));
GroupRepresentation group = groups.get(0);
assertThat(group.getId(), Matchers.is(groupUuid));
return group;
}

@Test
public void identityProviders() {
invoke(realm -> realm.identityProviders().findAll(), Resource.IDENTITY_PROVIDER, false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import org.keycloak.admin.client.Keycloak;
import org.keycloak.admin.client.resource.ClientResource;
import org.keycloak.admin.client.resource.ClientsResource;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.admin.client.resource.RoleByIdResource;
import org.keycloak.admin.client.resource.RolesResource;
import org.keycloak.authorization.fgap.AdminPermissionsSchema;
Expand Down Expand Up @@ -63,12 +64,15 @@
import org.keycloak.testframework.annotations.InjectAdminClient;
import org.keycloak.testframework.annotations.InjectClient;
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
import org.keycloak.testframework.realm.GroupBuilder;
import org.keycloak.testframework.realm.ManagedClient;
import org.keycloak.testframework.realm.UserBuilder;
import org.keycloak.testframework.util.ApiUtil;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

import static org.keycloak.authorization.fgap.AdminPermissionsSchema.CLIENTS;
import static org.keycloak.authorization.fgap.AdminPermissionsSchema.MANAGE;
Expand All @@ -79,6 +83,7 @@

import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.hasKey;
Expand Down Expand Up @@ -909,8 +914,9 @@ public void testMapRolePermissionGrantsRoleByIdRead() {
Assertions.assertThrows(ForbiddenException.class, () -> roles.deleteRole("MAPPABLE_REALM_ROLE"));
}

@Test
public void testGroupRepresentationFiltersRolesByVisibility() {
@ParameterizedTest
@CsvSource({"false, false", "false, true", "true, false", "true, true"})
public void testGroupRepresentationFiltersRolesByVisibility(boolean briefRepresentation, boolean canViewParent) {
RoleRepresentation mappableRealmRole = new RoleRepresentation();
mappableRealmRole.setName("GROUP_REP_MAPPABLE_REALM_ROLE");
realm.admin().roles().create(mappableRealmRole);
Expand Down Expand Up @@ -941,26 +947,73 @@ public void testGroupRepresentationFiltersRolesByVisibility() {
realm.admin().clients().get(hiddenClient.getId()).roles().create(hiddenClientRole);
hiddenClientRole = realm.admin().clients().get(hiddenClient.getId()).roles().get("GROUP_REP_HIDDEN_CLIENT_ROLE").toRepresentation();

GroupRepresentation targetGroup = createGroup("group-rep-visibility-group");
realm.admin().groups().group(targetGroup.getId()).roles().realmLevel().add(List.of(mappableRealmRole, hiddenRealmRole));
realm.admin().groups().group(targetGroup.getId()).roles().clientLevel(hiddenClient.getId()).add(List.of(mappableClientRole, hiddenClientRole));
GroupRepresentation parent = createGroup("group-rep-visibility-parent");
GroupRepresentation targetGroup = GroupBuilder.create().name("group-rep-visibility-group").build();
try (Response response = realm.admin().groups().group(parent.getId()).subGroup(targetGroup)) {
targetGroup.setId(ApiUtil.getCreatedId(response));
}
for (GroupRepresentation group : List.of(parent, targetGroup)) {
group.setAttributes(Map.of("visibility-attribute", List.of("value")));
realm.admin().groups().group(group.getId()).update(group);
realm.admin().groups().group(group.getId()).roles().realmLevel().add(List.of(mappableRealmRole, hiddenRealmRole));
realm.admin().groups().group(group.getId()).roles().clientLevel(hiddenClient.getId()).add(List.of(mappableClientRole, hiddenClientRole));
}

UserRepresentation member = createUser("group-rep-visibility-member");
realm.admin().users().get(member.getId()).joinGroup(targetGroup.getId());

UserRepresentation myadmin = realm.admin().users().search("myadmin").get(0);
UserPolicyRepresentation policy = createUserPolicy(realm, adminPermissionsClient, "myadmin-group-rep-policy", myadmin.getId());
createGroupPermission(targetGroup, Set.of(VIEW), policy);
if (canViewParent) {
createGroupPermission(parent, Set.of(VIEW), policy);
}
createPermission(adminPermissionsClient, member.getId(), AdminPermissionsSchema.USERS.getType(), Set.of(VIEW), policy);
// map-role on specific roles only, without view-realm or view on the client
createPermission(adminPermissionsClient, mappableRealmRole.getId(), AdminPermissionsSchema.ROLES.getType(), Set.of(MAP_ROLE), policy);
createPermission(adminPermissionsClient, mappableClientRole.getId(), AdminPermissionsSchema.ROLES.getType(), Set.of(MAP_ROLE), policy);

GroupRepresentation groupRep = realmAdminClient.realm(realm.getName()).groups().group(targetGroup.getId()).toRepresentation();
RealmResource adminRealm = realmAdminClient.realm(realm.getName());
GroupRepresentation groupRep = adminRealm.groups().group(targetGroup.getId()).toRepresentation();
assertGroupRepresentationFiltered(groupRep);

if (!canViewParent) {
Assertions.assertThrows(ForbiddenException.class, () -> adminRealm.groups().group(parent.getId()).toRepresentation());
}

List<GroupRepresentation> listed = adminRealm.groups().groups(targetGroup.getName(), true, null, null, briefRepresentation);
GroupRepresentation listedParent = assertSingleGroup(listed, parent.getId(), briefRepresentation || !canViewParent);
assertThat(listedParent.getSubGroupCount(), is(1L));
if (!canViewParent) {
assertThat(listedParent.getAccess(), nullValue());
}
assertSingleGroup(listedParent.getSubGroups(), targetGroup.getId(), briefRepresentation);

List<GroupRepresentation> flat = adminRealm.groups().groups(targetGroup.getName(), true, null, null, briefRepresentation, true, false);
GroupRepresentation flatGroup = assertSingleGroup(flat, targetGroup.getId(), briefRepresentation);
assertThat(flatGroup.getSubGroups(), empty());

assertThat(groupRep.getRealmRoles(), notNullValue());
assertThat(groupRep.getRealmRoles(), hasItem("GROUP_REP_MAPPABLE_REALM_ROLE"));
assertThat(groupRep.getRealmRoles(), not(hasItem("GROUP_REP_HIDDEN_REALM_ROLE")));
List<GroupRepresentation> memberGroups = adminRealm.users().get(member.getId()).groups(null, null, briefRepresentation);
assertSingleGroup(memberGroups, targetGroup.getId(), briefRepresentation);
}

private static GroupRepresentation assertSingleGroup(List<GroupRepresentation> groups, String groupId, boolean briefRepresentation) {
assertThat(groups, hasSize(1));
GroupRepresentation group = groups.get(0);
assertThat(group.getId(), is(groupId));
if (briefRepresentation) {
assertThat(group.getRealmRoles(), nullValue());
assertThat(group.getClientRoles(), nullValue());
assertThat(group.getAttributes(), nullValue());
} else {
assertGroupRepresentationFiltered(group);
assertThat(group.getAttributes(), is(Map.of("visibility-attribute", List.of("value"))));
}
return group;
}

assertThat(groupRep.getClientRoles(), notNullValue());
assertThat(groupRep.getClientRoles(), hasKey("group-rep-hidden-client"));
assertThat(groupRep.getClientRoles().get("group-rep-hidden-client"), hasItem("GROUP_REP_MAPPABLE_CLIENT_ROLE"));
assertThat(groupRep.getClientRoles().get( 5970 "group-rep-hidden-client"), not(hasItem("GROUP_REP_HIDDEN_CLIENT_ROLE")));
private static void assertGroupRepresentationFiltered(GroupRepresentation groupRep) {
assertThat(groupRep.getRealmRoles(), contains("GROUP_REP_MAPPABLE_REALM_ROLE"));
assertThat(groupRep.getClientRoles(), is(Map.of("group-rep-hidden-client", List.of("GROUP_REP_MAPPABLE_CLIENT_ROLE"))));
}
}
Loading
0