Add Groups resource type and scopes to authorization schema and evaluation implementation

Closes #35562

Signed-off-by: vramik <vramik@redhat.com>
This commit is contained in:
vramik
2025-02-04 12:15:09 +01:00
committed by Pedro Igor
parent 7a8d18122a
commit 679f44692d
19 changed files with 834 additions and 129 deletions

View File

@@ -144,16 +144,12 @@ public class BruteForceUsersResource {
private Stream<BruteUser> searchForUser(Map<String, String> attributes, RealmModel realm, UserPermissionEvaluator usersEvaluator, Boolean briefRepresentation, Integer firstResult, Integer maxResults, Boolean includeServiceAccounts) {
attributes.put(UserModel.INCLUDE_SERVICE_ACCOUNT, includeServiceAccounts.toString());
if (!auth.users().canView()) {
Set<String> groupModels = auth.groups().getGroupsWithViewPermission();
if (!groupModels.isEmpty()) {
session.setAttribute(UserModel.GROUPS, groupModels);
}
Set<String> groupIds = auth.groups().getGroupIdsWithViewPermission();
if (!groupIds.isEmpty()) {
session.setAttribute(UserModel.GROUPS, groupIds);
}
Stream<UserModel> userModels = session.users().searchForUserStream(realm, attributes, firstResult, maxResults);
return toRepresentation(realm, usersEvaluator, briefRepresentation, userModels);
return toRepresentation(realm, usersEvaluator, briefRepresentation, session.users().searchForUserStream(realm, attributes, firstResult, maxResults));
}
private Stream<BruteUser> toRepresentation(RealmModel realm, UserPermissionEvaluator usersEvaluator,

View File

@@ -33,6 +33,7 @@ import org.keycloak.common.Profile;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientProvider;
import org.keycloak.models.Constants;
import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelException;
import org.keycloak.models.ModelIllegalStateException;
@@ -50,27 +51,42 @@ import org.keycloak.representations.idm.authorization.ScopePermissionRepresentat
import org.keycloak.representations.idm.authorization.ScopeRepresentation;
public class AdminPermissionsSchema extends AuthorizationSchema {
public static final String USERS_RESOURCE_TYPE = "Users";
public static final String CLIENTS_RESOURCE_TYPE = "Clients";
//scopes
public static final String CLIENTS_RESOURCE_TYPE = "Clients";
public static final String GROUPS_RESOURCE_TYPE = "Groups";
public static final String USERS_RESOURCE_TYPE = "Users";
// common scopes
public static final String MANAGE = "manage";
public static final String VIEW = "view";
public static final String IMPERSONATE = "impersonate";
public static final String MAP_ROLES = "map-roles";
public static final String MANAGE_GROUP_MEMBERSHIP = "manage-group-membership";
// client specific scopes
public static final String CONFIGURE = "configure";
public static final String MAP_ROLES_CLIENT_SCOPE = "map-roles-client-scope";
public static final String MAP_ROLES_COMPOSITE = "map-roles-composite";
public static final ResourceType USERS = new ResourceType(USERS_RESOURCE_TYPE, Set.of(MANAGE, VIEW, IMPERSONATE, MAP_ROLES, MANAGE_GROUP_MEMBERSHIP));
// group specific scopes
public static final String MANAGE_MEMBERSHIP = "manage-membership";
public static final String MANAGE_MEMBERS = "manage-members";
public static final String VIEW_MEMBERS = "view-members";
// user specific scopes
public static final String IMPERSONATE = "impersonate";
public static final String MAP_ROLES = "map-roles";
public static final String MANAGE_GROUP_MEMBERSHIP = "manage-group-membership";
public static final ResourceType CLIENTS = new ResourceType(CLIENTS_RESOURCE_TYPE, Set.of(CONFIGURE, MANAGE, MAP_ROLES, MAP_ROLES_CLIENT_SCOPE, MAP_ROLES_COMPOSITE, VIEW));
public static final ResourceType GROUPS = new ResourceType(GROUPS_RESOURCE_TYPE, Set.of(MANAGE, VIEW, MANAGE_MEMBERSHIP, MANAGE_MEMBERS, VIEW_MEMBERS));
public static final ResourceType USERS = new ResourceType(USERS_RESOURCE_TYPE, Set.of(MANAGE, VIEW, IMPERSONATE, MAP_ROLES, MANAGE_GROUP_MEMBERSHIP));
public static final AdminPermissionsSchema SCHEMA = new AdminPermissionsSchema();
private AdminPermissionsSchema() {
super(Map.of(USERS_RESOURCE_TYPE, USERS, CLIENTS_RESOURCE_TYPE, CLIENTS));
super(Map.of(
CLIENTS_RESOURCE_TYPE, CLIENTS,
GROUPS_RESOURCE_TYPE, GROUPS,
USERS_RESOURCE_TYPE, USERS
));
}
public Resource getOrCreateResource(KeycloakSession session, ResourceServer resourceServer, String policyType, String resourceType, String id) {
@@ -86,12 +102,13 @@ public class AdminPermissionsSchema extends AuthorizationSchema {
return resource;
}
String name = null;
String name;
if (USERS.getType().equals(resourceType)) {
name = resolveUser(session, id);
} else if (CLIENTS.getType().equals(resourceType)) {
name = resolveClient(session, id);
switch (resourceType) {
case CLIENTS_RESOURCE_TYPE -> name = resolveClient(session, id);
case GROUPS_RESOURCE_TYPE -> name = resolveGroup(session, id);
case USERS_RESOURCE_TYPE -> name = resolveUser(session, id);
default -> throw new IllegalStateException("Resource type [" + resourceType + "] not found.");
}
if (name == null) {
@@ -161,6 +178,13 @@ public class AdminPermissionsSchema extends AuthorizationSchema {
}
}
private String resolveGroup(KeycloakSession session, String id) {
RealmModel realm = session.getContext().getRealm();
GroupModel group = session.groups().getGroupById(realm, id);
return group == null ? null : group.getId();
}
private String resolveUser(KeycloakSession session, String id) {
RealmModel realm = session.getContext().getRealm();
UserModel user = session.users().getUserById(realm, id);

View File

@@ -173,10 +173,6 @@ public class ModelToRepresentation {
return rep;
}
public static Stream<GroupModel> searchGroupModelsByAttributes(KeycloakSession session, RealmModel realm, Map<String,String> attributes, Integer first, Integer max) {
return session.groups().searchGroupsByAttributes(realm, attributes, first, max);
}
@Deprecated
public static Stream<GroupRepresentation> toGroupHierarchy(KeycloakSession session, RealmModel realm, boolean full) {
return session.groups().getTopLevelGroupsStream(realm, null, null)

View File

@@ -174,10 +174,9 @@ public class GroupResource {
@Parameter(description = "The maximum number of results that are to be returned. Defaults to 10") @QueryParam("max") @DefaultValue("10") Integer max,
@Parameter(description = "Boolean which defines whether brief groups representations are returned or not (default: false)") @QueryParam("briefRepresentation") @DefaultValue("false") Boolean briefRepresentation) {
this.auth.groups().requireView(group);
boolean canViewGlobal = auth.groups().canView();
return paginatedStream(
group.getSubGroupsStream(search, exact, -1, -1)
.filter(g -> canViewGlobal || auth.groups().canView(g)), first, max)
.filter(auth.groups()::canView), first, max)
.map(g -> GroupUtils.populateSubGroupCount(g, GroupUtils.toRepresentation(auth.groups(), g, !briefRepresentation)));
}
@@ -204,7 +203,7 @@ public class GroupResource {
try {
Response.ResponseBuilder builder = Response.status(204);
GroupModel child = null;
GroupModel child;
if (rep.getId() != null) {
child = realm.getGroupById(rep.getId());
if (child == null) {

View File

@@ -47,7 +47,6 @@ import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.organization.utils.Organizations;
import org.keycloak.representations.idm.GroupRepresentation;
import org.keycloak.services.ErrorResponse;
@@ -100,7 +99,7 @@ public class GroupsResource {
Stream<GroupModel> stream;
if (Objects.nonNull(searchQuery)) {
Map<String, String> attributes = SearchQueryUtils.getFields(searchQuery);
stream = ModelToRepresentation.searchGroupModelsByAttributes(session, realm, attributes, firstResult, maxResults);
stream = session.groups().searchGroupsByAttributes(realm, attributes, firstResult, maxResults);
} else if (Objects.nonNull(search)) {
stream = session.groups().searchForGroupByNameStream(realm, search.trim(), exact, firstResult, maxResults);
} else {
@@ -110,9 +109,8 @@ public class GroupsResource {
if (populateHierarchy) {
return GroupUtils.populateGroupHierarchyFromSubGroups(session, realm, stream, !briefRepresentation, groupsEvaluator);
}
boolean canViewGlobal = groupsEvaluator.canView();
return stream
.filter(g -> canViewGlobal || groupsEvaluator.canView(g))
return stream.filter(groupsEvaluator::canView)
.map(g -> GroupUtils.populateSubGroupCount(g, GroupUtils.toRepresentation(groupsEvaluator, g, !briefRepresentation)));
}

View File

@@ -71,7 +71,6 @@ import java.util.Objects;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -396,7 +395,7 @@ public class UsersResource {
} else if (userPermissionEvaluator.canView()) {
return session.users().getUsersCount(realm, search.trim());
} else {
return session.users().getUsersCount(realm, search.trim(), auth.groups().getGroupsWithViewPermission());
return session.users().getUsersCount(realm, search.trim(), auth.groups().getGroupIdsWithViewPermission());
}
} else if (last != null || first != null || email != null || username != null || emailVerified != null || enabled != null || !searchAttributes.isEmpty()) {
Map<String, String> parameters = new HashMap<>();
@@ -423,12 +422,12 @@ public class UsersResource {
if (userPermissionEvaluator.canView()) {
return session.users().getUsersCount(realm, parameters);
} else {
return session.users().getUsersCount(realm, parameters, auth.groups().getGroupsWithViewPermission());
return session.users().getUsersCount(realm, parameters, auth.groups().getGroupIdsWithViewPermission());
}
} else if (userPermissionEvaluator.canView()) {
return session.users().getUsersCount(realm);
} else {
return session.users().getUsersCount(realm, auth.groups().getGroupsWithViewPermission());
return session.users().getUsersCount(realm, auth.groups().getGroupIdsWithViewPermission());
}
}
@@ -446,16 +445,12 @@ public class UsersResource {
private Stream<UserRepresentation> searchForUser(Map<String, String> attributes, RealmModel realm, UserPermissionEvaluator usersEvaluator, Boolean briefRepresentation, Integer firstResult, Integer maxResults, Boolean includeServiceAccounts) {
attributes.put(UserModel.INCLUDE_SERVICE_ACCOUNT, includeServiceAccounts.toString());
if (!auth.users().canView()) {
Set<String> groupModels = auth.groups().getGroupsWithViewPermission();
if (!groupModels.isEmpty()) {
session.setAttribute(UserModel.GROUPS, groupModels);
}
Set<String> groupIds = auth.groups().getGroupIdsWithViewPermission();
if (!groupIds.isEmpty()) {
session.setAttribute(UserModel.GROUPS, groupIds);
}
Stream<UserModel> userModels = session.users().searchForUserStream(realm, attributes, firstResult, maxResults).filter(usersEvaluator::canView);
return toRepresentation(realm, usersEvaluator, briefRepresentation, userModels);
return toRepresentation(realm, usersEvaluator, briefRepresentation, session.users().searchForUserStream(realm, attributes, firstResult, maxResults));
}
private Stream<UserRepresentation> toRepresentation(RealmModel realm, UserPermissionEvaluator usersEvaluator, Boolean briefRepresentation, Stream<UserModel> userModels) {

View File

@@ -71,10 +71,10 @@ public class AdminPermissions {
}
public static void registerListener(ProviderEventManager manager) {
manager.register(new ProviderEventListener() {
@Override
public void onEvent(ProviderEvent event) {
if (Profile.isFeatureEnabled(Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ)) {
if (Profile.isFeatureEnabled(Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ)) {
manager.register(new ProviderEventListener() {
@Override
public void onEvent(ProviderEvent event) {
if (event instanceof RoleContainerModel.RoleRemovedEvent) {
RoleContainerModel.RoleRemovedEvent cast = (RoleContainerModel.RoleRemovedEvent) event;
RoleModel role = cast.getRole();
@@ -94,8 +94,8 @@ public class AdminPermissions {
management(cast.getKeycloakSession(), cast.getRealm()).groups().setPermissionsEnabled(cast.getGroup(), false);
}
}
}
});
});
}
}

View File

@@ -16,6 +16,8 @@
*/
package org.keycloak.services.resources.admin.permissions;
import org.keycloak.authorization.AdminPermissionsSchema;
import org.keycloak.models.AdminRoles;
import org.keycloak.models.GroupModel;
import java.util.Map;
@@ -26,41 +28,121 @@ import java.util.Set;
* @version $Revision: 1 $
*/
public interface GroupPermissionEvaluator {
/**
* Returns {@code true} if the caller has at least one of {@link AdminRoles#QUERY_GROUPS},
* {@link AdminRoles#MANAGE_USERS} or {@link AdminRoles#VIEW_USERS} roles.
* <p/>
* For V2 only: Also if it has a permission to {@link AdminPermissionsSchema#VIEW} or
* {@link AdminPermissionsSchema#MANAGE} groups.
*/
boolean canList();
/**
* Throws ForbiddenException if {@link #canList()} returns {@code false}.
*/
void requireList();
/**
* Returns {@code true} if the caller has {@link AdminRoles#MANAGE_USERS} role.
* <p/>
* Or if it has a permission to {@link AdminPermissionsSchema#MANAGE} the group.
*/
boolean canManage(GroupModel group);
/**
* Throws ForbiddenException if {@link #canManage(GroupModel)} returns {@code false}.
*/
void requireManage(GroupModel group);
/**
* Returns {@code true} if the caller has one of {@link AdminRoles#MANAGE_USERS} or
* {@link AdminRoles#VIEW_USERS} roles.
* <p/>
* Or if it has a permission to {@link AdminPermissionsSchema#VIEW} or
* {@link AdminPermissionsSchema#MANAGE} the group.
*/
boolean canView(GroupModel group);
/**
* Throws ForbiddenException if {@link #canView(GroupModel)} returns {@code false}.
*/
void requireView(GroupModel group);
/**
* Returns {@code true} if the caller has {@link AdminRoles#MANAGE_USERS} role.
* <p/>
* For V2 only: Also if it has permission to {@link AdminPermissionsSchema#VIEW} or
* {@link AdminPermissionsSchema#MANAGE} groups.
*/
boolean canManage();
/**
* Throws ForbiddenException if {@link #canManage()} returns {@code false}.
*/
void requireManage();
/**
* Returns {@code true} if the caller has one of {@link AdminRoles#MANAGE_USERS} or
* {@link AdminRoles#VIEW_USERS} roles.
* <p/>
* Or if it has a permission to {@link AdminPermissionsSchema#VIEW} or
* {@link AdminPermissionsSchema#MANAGE} groups.
*/
boolean canView();
/**
* Throws ForbiddenException if {@link #canView()} returns {@code false}.
*/
void requireView();
boolean getGroupsWithViewPermission(GroupModel group);
/**
* Throws ForbiddenException if {@link #canViewMembers(GroupModel)} returns {@code false}.
*/
void requireViewMembers(GroupModel group);
/**
* Returns {@code true} if {@link UserPermissionEvaluator#canManage()} evaluates to {@code true}.
* <p/>
* Or if it has a permission to {@link AdminPermissionsSchema#MANAGE_MEMBERS} of the group.
*/
boolean canManageMembers(GroupModel group);
/**
* Returns {@code true} if the caller has one of {@link AdminRoles#MANAGE_USERS} role.
* <p/>
* Or if it has a permission to {@link AdminPermissionsSchema#MANAGE} the group or
* {@link AdminPermissionsSchema#MANAGE_MEMBERSHIP} of the group.
*/
boolean canManageMembership(GroupModel group);
/**
* Returns {@code true} if {@link UserPermissionEvaluator#canView()} evaluates to {@code true}.
* <p/>
* Or if it has a permission to {@link AdminPermissionsSchema#VIEW_MEMBERS} or
* {@link AdminPermissionsSchema#MANAGE_MEMBERS} of the group.
*/
boolean canViewMembers(GroupModel group);
/**
* Throws ForbiddenException if {@link #canManageMembership(GroupModel)} returns {@code false}.
*/
void requireManageMembership(GroupModel group);
/**
* Throws ForbiddenException if {@link #canManageMembership(GroupModel)} returns {@code false}.
*/
void requireManageMembers(GroupModel group);
/**
* Returns Map with information what access the caller for the provided group has.
*/
Map<String, Boolean> getAccess(GroupModel group);
Set<String> getGroupsWithViewPermission();
/**
* If {@link UserPermissionEvaluator#canView()} evaluates to {@code true}, returns empty set.
*
* @return Stream of IDs of groups with view permission.
*/
Set<String> getGroupIdsWithViewPermission();
}

View File

@@ -53,9 +53,9 @@ class GroupPermissions implements GroupPermissionEvaluator, GroupPermissionManag
private static final String RESOURCE_NAME_PREFIX = "group.resource.";
private final AuthorizationProvider authz;
private final MgmtPermissions root;
private final ResourceStore resourceStore;
private final PolicyStore policyStore;
protected final MgmtPermissions root;
protected final ResourceStore resourceStore;
protected final PolicyStore policyStore;
GroupPermissions(AuthorizationProvider authz, MgmtPermissions root) {
this.authz = authz;
@@ -73,7 +73,6 @@ class GroupPermissions implements GroupPermissionEvaluator, GroupPermissionManag
return RESOURCE_NAME_PREFIX + group.getId();
}
private static String getManagePermissionGroup(GroupModel group) {
return "manage.permission.group." + group.getId();
}
@@ -147,7 +146,7 @@ class GroupPermissions implements GroupPermissionEvaluator, GroupPermissionManag
@Override
public boolean canList() {
return canView() || root.hasOneAdminRole(AdminRoles.VIEW_USERS, AdminRoles.MANAGE_USERS, AdminRoles.QUERY_GROUPS);
return root.hasOneAdminRole(AdminRoles.QUERY_GROUPS) || canView();
}
@Override
@@ -273,7 +272,7 @@ class GroupPermissions implements GroupPermissionEvaluator, GroupPermissionManag
@Override
public boolean canManage() {
return root.users().canManageDefault();
return root.hasOneAdminRole(AdminRoles.MANAGE_USERS);
}
@Override
@@ -282,9 +281,10 @@ class GroupPermissions implements GroupPermissionEvaluator, GroupPermissionManag
throw new ForbiddenException();
}
}
@Override
public boolean canView() {
return root.users().canViewDefault();
return root.hasOneAdminRole(AdminRoles.MANAGE_USERS, AdminRoles.VIEW_USERS);
}
@Override
@@ -295,24 +295,8 @@ class GroupPermissions implements GroupPermissionEvaluator, GroupPermissionManag
}
@Override
public boolean getGroupsWithViewPermission(GroupModel group) {
if (root.users().canView() || root.users().canManage()) {
return true;
}
if (!root.isAdminSameRealm()) {
return false;
}
ResourceServer server = root.realmResourceServer();
if (server == null) return false;
return hasPermission(group, VIEW_MEMBERS_SCOPE, MANAGE_MEMBERS_SCOPE);
}
@Override
public Set<String> getGroupsWithViewPermission() {
if (root.users().canView() || root.users().canManage()) return Collections.emptySet();
public Set<String> getGroupIdsWithViewPermission() {
if (root.users().canView()) return Collections.emptySet();
if (!root.isAdminSameRealm()) {
return Collections.emptySet();
@@ -337,7 +321,7 @@ class GroupPermissions implements GroupPermissionEvaluator, GroupPermissionManag
@Override
public void requireViewMembers(GroupModel group) {
if (!getGroupsWithViewPermission(group)) {
if (!canViewMembers(group)) {
throw new ForbiddenException();
}
}
@@ -353,7 +337,7 @@ class GroupPermissions implements GroupPermissionEvaluator, GroupPermissionManag
ResourceServer server = root.realmResourceServer();
if (server == null) return false;
return hasPermission(group, VIEW_MEMBERS_SCOPE);
return hasPermission(group, VIEW_MEMBERS_SCOPE, MANAGE_MEMBERS_SCOPE);
}
@Override

View File

@@ -0,0 +1,211 @@
/*
* Copyright 2025 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.services.resources.admin.permissions;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.keycloak.authorization.AdminPermissionsSchema;
import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.model.Resource;
import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.authorization.permission.ResourcePermission;
import org.keycloak.models.AdminRoles;
import org.keycloak.representations.idm.authorization.Permission;
import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession;
class GroupPermissionsV2 extends GroupPermissions {
private final KeycloakSession session;
GroupPermissionsV2(KeycloakSession session, AuthorizationProvider authz, MgmtPermissions root) {
super(authz, root);
this.session = session;
}
@Override
public boolean canView() {
if (root.hasOneAdminRole(AdminRoles.MANAGE_USERS, AdminRoles.VIEW_USERS)) {
return true;
}
return hasPermission(null, AdminPermissionsSchema.VIEW, AdminPermissionsSchema.MANAGE);
}
@Override
public boolean canView(GroupModel group) {
if (root.hasOneAdminRole(AdminRoles.MANAGE_USERS, AdminRoles.VIEW_USERS)) {
return true;
}
return hasPermission(group.getId(), AdminPermissionsSchema.VIEW, AdminPermissionsSchema.MANAGE);
}
@Override
public boolean canManage() {
if (root.hasOneAdminRole(AdminRoles.MANAGE_USERS)) {
return true;
}
return hasPermission(null, AdminPermissionsSchema.VIEW, AdminPermissionsSchema.MANAGE);
}
@Override
public boolean canManage(GroupModel group) {
if (root.hasOneAdminRole(AdminRoles.MANAGE_USERS)) {
return true;
}
return hasPermission(group.getId(), AdminPermissionsSchema.MANAGE);
}
@Override
public boolean canViewMembers(GroupModel group) {
if (root.users().canView()) return true;
return hasPermission(group.getId(), AdminPermissionsSchema.VIEW_MEMBERS, AdminPermissionsSchema.MANAGE_MEMBERS);
}
@Override
public boolean canManageMembers(GroupModel group) {
if (root.users().canManage()) return true;
return hasPermission(group.getId(), AdminPermissionsSchema.MANAGE_MEMBERS);
}
@Override
public boolean canManageMembership(GroupModel group) {
if (root.hasOneAdminRole(AdminRoles.MANAGE_USERS)) {
return true;
}
return hasPermission(group.getId(), AdminPermissionsSchema.MANAGE, AdminPermissionsSchema.MANAGE_MEMBERSHIP);
}
@Override
public Set<String> getGroupIdsWithViewPermission() {
if (root.users().canView()) return Collections.emptySet();
if (!root.isAdminSameRealm()) {
return Collections.emptySet();
}
ResourceServer server = root.realmResourceServer();
if (server == null) {
return Collections.emptySet();
}
Set<String> granted = new HashSet<>();
resourceStore.findByType(server, AdminPermissionsSchema.GROUPS_RESOURCE_TYPE, groupResource -> {
if (hasPermission(groupResource.getId(), AdminPermissionsSchema.VIEW_MEMBERS, AdminPermissionsSchema.MANAGE_MEMBERS)) {
granted.add(groupResource.getId());
}
});
return granted;
}
private boolean hasPermission(String groupId, String... scopes) {
if (!root.isAdminSameRealm()) {
return false;
}
ResourceServer server = root.realmResourceServer();
if (server == null) {
return false;
}
Resource resource = groupId == null ? null : resourceStore.findByName(server, groupId);
if (resource == null) {
resource = AdminPermissionsSchema.SCHEMA.getResourceTypeResource(session, server, AdminPermissionsSchema.GROUPS_RESOURCE_TYPE);
// check if there is a permission for "all-groups". If so, proceed with the evaluation to check scopes
if (policyStore.findByResource(server, resource).isEmpty()) {
return false;
}
}
Collection<Permission> permissions = root.evaluatePermission(new ResourcePermission(resource, resource.getScopes(), server), server);
List<String> expectedScopes = Arrays.asList(scopes);
for (Permission permission : permissions) {
for (String scope : permission.getScopes()) {
if (expectedScopes.contains(scope)) {
return true;
}
}
}
return false;
}
@Override
public boolean isPermissionsEnabled(GroupModel group) {
throw new UnsupportedOperationException("Not supported in V2");
}
@Override
public void setPermissionsEnabled(GroupModel group, boolean enable) {
throw new UnsupportedOperationException("Not supported in V2");
}
@Override
public Policy viewMembersPermission(GroupModel group) {
throw new UnsupportedOperationException("Not supported in V2");
}
@Override
public Policy manageMembersPermission(GroupModel group) {
throw new UnsupportedOperationException("Not supported in V2");
}
@Override
public Policy manageMembershipPermission(GroupModel group) {
throw new UnsupportedOperationException("Not supported in V2");
}
@Override
public Policy viewPermission(GroupModel group) {
throw new UnsupportedOperationException("Not supported in V2");
}
@Override
public Policy managePermission(GroupModel group) {
throw new UnsupportedOperationException("Not supported in V2");
}
@Override
public Resource resource(GroupModel group) {
throw new UnsupportedOperationException("Not supported in V2");
}
@Override
public Map<String, String> getPermissions(GroupModel group) {
throw new UnsupportedOperationException("Not supported in V2");
}
}

View File

@@ -24,6 +24,7 @@ import org.keycloak.services.resources.admin.AdminAuth;
class MgmtPermissionsV2 extends MgmtPermissions {
private GroupPermissionsV2 groupPermissions;
private UserPermissionsV2 userPermissions;
private ClientPermissionsV2 clientPermissions;
@@ -53,6 +54,13 @@ class MgmtPermissionsV2 extends MgmtPermissions {
return realm.getAdminPermissionsClient();
}
@Override
public GroupPermissions groups() {
if (groupPermissions != null) return groupPermissions;
groupPermissions = new GroupPermissionsV2(session, authz, this);
return groupPermissions;
}
@Override
public UserPermissions users() {
if (userPermissions != null) return userPermissions;

View File

@@ -299,7 +299,7 @@ class RolePermissions implements RolePermissionEvaluator, RolePermissionManageme
*/
@Override
public boolean canMapRole(RoleModel role) {
if (root.users().canManageDefault()) return checkAdminRoles(role);
if (root.hasOneAdminRole(AdminRoles.MANAGE_USERS)) return checkAdminRoles(role);
if (!root.isAdminSameRealm()) {
return false;
}

View File

@@ -16,7 +16,10 @@
*/
package org.keycloak.services.resources.admin.permissions;
import org.keycloak.authorization.AdminPermissionsSchema;
import org.keycloak.models.AdminRoles;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ImpersonationConstants;
import org.keycloak.models.UserModel;
import java.util.Map;
@@ -26,30 +29,138 @@ import java.util.Map;
* @version $Revision: 1 $
*/
public interface UserPermissionEvaluator {
/**
* Throws ForbiddenException if {@link #canManage()} returns {@code false}.
*/
void requireManage();
/**
* Throws ForbiddenException if {@link #canManage(UserModel)} returns {@code false}.
*/
void requireManage(UserModel user);
/**
* Returns {@code true} if the caller has {@link AdminRoles#MANAGE_USERS} role.
* <p/>
* Or if it has a permission to {@link AdminPermissionsSchema#MANAGE} users.
*/
boolean canManage();
/**
* Returns {@code true} if the caller has {@link AdminRoles#MANAGE_USERS} role.
* <p/>
* Or if it has a permission to {@link AdminPermissionsSchema#MANAGE} the user.
* <p/>
* Or if it has a permission to {@link AdminPermissionsSchema#MANAGE_MEMBERS}
* of the group chain the user is associated with.
*/
boolean canManage(UserModel user);
/**
* Throws ForbiddenException if {@link #canQuery()} returns {@code false}.
*/
void requireQuery();
/**
* Returns {@code true} if the caller has at least one of {@link AdminRoles#QUERY_USERS},
* {@link AdminRoles#MANAGE_USERS} or {@link AdminRoles#VIEW_USERS} roles.
* <p/>
* Or if it has a permission to {@link AdminPermissionsSchema#VIEW} or
* {@link AdminPermissionsSchema#MANAGE} users.
*/
boolean canQuery();
/**
* Throws ForbiddenException if {@link #canView()} returns {@code false}.
*/
void requireView();
/**
* Throws ForbiddenException if {@link #canView(UserModel)} returns {@code false}.
*/
void requireView(UserModel user);
/**
* Returns {@code true} if the caller has one of {@link AdminRoles#MANAGE_USERS} or
* {@link AdminRoles#VIEW_USERS} roles.
* <p/>
* Or if it has a permission to {@link AdminPermissionsSchema#VIEW} or
* {@link AdminPermissionsSchema#MANAGE} users.
*/
boolean canView();
/**
* Returns {@code true} if the caller has at least one of {@link AdminRoles#MANAGE_USERS} or
* {@link AdminRoles#VIEW_USERS} roles.
* <p/>
* Or if it has a permission to {@link AdminPermissionsSchema#VIEW} or
* {@link AdminPermissionsSchema#MANAGE} the user.
* <p/>
* Or if it has a permission to {@link AdminPermissionsSchema#VIEW_MEMBERS}
* of the group chain the user is associated with.
*/
boolean canView(UserModel user);
/**
* Throws ForbiddenException if {@link #canImpersonate(UserModel, ClientModel)} returns {@code false}.
*/
void requireImpersonate(UserModel user);
boolean canImpersonate();
boolean canImpersonate(UserModel user, ClientModel requester);
boolean isImpersonatable(UserModel user, ClientModel requester);
/**
* Returns {@code true} if the caller has the {@link ImpersonationConstants#IMPERSONATION_ROLE}.
* <p/>
* Or if it has a permission to {@link AdminPermissionsSchema#IMPERSONATE} users.
*/
boolean canImpersonate();
/**
* Returns {@code true} if the caller has the {@link ImpersonationConstants#IMPERSONATION_ROLE}.
* <p/>
* NOTE: If requester is provided, it's clientId is added to evaluation context.
* <p/>
* Or if it has a permission to {@link AdminPermissionsSchema#IMPERSONATE} the user.
*/
boolean canImpersonate(UserModel user, ClientModel requester);
/**
* Returns Map with information what access the caller for the provided user has.
*/
Map<String, Boolean> getAccess(UserModel user);
/**
* Throws ForbiddenException if {@link #canMapRoles(UserModel)} returns {@code false}.
*/
void requireMapRoles(UserModel user);
/**
* Returns {@code true} if the caller has {@link AdminRoles#MANAGE_USERS} role.
* <p/>
* Or if it has a permission to {@link AdminPermissionsSchema#MANAGE} the user or
* {@link AdminPermissionsSchema#MAP_ROLES} of the user.
* <p/>
* Or if it has a permission to {@link AdminPermissionsSchema#MANAGE_MEMBERS}
* of the group chain the user is associated with.
*/
boolean canMapRoles(UserModel user);
/**
* Throws ForbiddenException if {@link #canManageGroupMembership(UserModel)} returns {@code false}.
*/
void requireManageGroupMembership(UserModel user);
/**
* Returns {@code true} if the caller has {@link AdminRoles#MANAGE_USERS} role.
* <p/>
* Or if it has a permission to {@link AdminPermissionsSchema#MANAGE} the user or
* {@link AdminPermissionsSchema#MANAGE_GROUP_MEMBERSHIP} of the user.
* <p/>
* Or if it has a permission to {@link AdminPermissionsSchema#MANAGE_MEMBERS}
* of the group chain the user is associated with.
*/
boolean canManageGroupMembership(UserModel user);
@Deprecated
boolean isImpersonatable(UserModel user, ClientModel requester);
void grantIfNoPermission(boolean grantIfNoPermission);
}

View File

@@ -177,10 +177,6 @@ class UserPermissions implements UserPermissionEvaluator, UserPermissionManageme
}
}
public boolean canManageDefault() {
return root.hasOneAdminRole(AdminRoles.MANAGE_USERS);
}
@Override
public Resource resource() {
ResourceServer server = root.realmResourceServer();
@@ -235,7 +231,7 @@ class UserPermissions implements UserPermissionEvaluator, UserPermissionManageme
*/
@Override
public boolean canManage() {
if (canManageDefault()) {
if (root.hasOneAdminRole(AdminRoles.MANAGE_USERS)) {
return true;
}
@@ -274,7 +270,7 @@ class UserPermissions implements UserPermissionEvaluator, UserPermissionManageme
@Override
public boolean canQuery() {
return canView() || root.hasOneAdminRole(AdminRoles.QUERY_USERS);
return root.hasOneAdminRole(AdminRoles.QUERY_USERS) || canView();
}
@Override
@@ -299,7 +295,7 @@ class UserPermissions implements UserPermissionEvaluator, UserPermissionManageme
*/
@Override
public boolean canView() {
if (canViewDefault() || canManageDefault()) {
if (root.hasOneAdminRole(AdminRoles.MANAGE_USERS, AdminRoles.VIEW_USERS)) {
return true;
}
@@ -585,15 +581,11 @@ class UserPermissions implements UserPermissionEvaluator, UserPermissionManageme
protected boolean canManageByGroup(UserModel user) {
if (authz == null) return false;
return evaluateHierarchy(user, (group) -> root.groups().canManageMembers(group));
return evaluateHierarchy(user, root.groups()::canManageMembers);
}
protected boolean canViewByGroup(UserModel user) {
if (authz == null) return false;
return evaluateHierarchy(user, (group) -> root.groups().getGroupsWithViewPermission(group));
}
public boolean canViewDefault() {
return root.hasOneAdminRole(AdminRoles.MANAGE_USERS, AdminRoles.VIEW_USERS);
return evaluateHierarchy(user, root.groups()::canViewMembers);
}
}

View File

@@ -45,32 +45,20 @@ class UserPermissionsV2 extends UserPermissions {
@Override
public boolean canView(UserModel user) {
if (root.hasOneAdminRole(AdminRoles.ADMIN, AdminRoles.MANAGE_USERS, AdminRoles.VIEW_USERS)) {
if (root.hasOneAdminRole(AdminRoles.MANAGE_USERS, AdminRoles.VIEW_USERS)) {
return true;
}
boolean result = hasPermission(user, null, AdminPermissionsSchema.VIEW, AdminPermissionsSchema.MANAGE);
if (!result) {
return canViewByGroup(user);
}
return result;
return hasPermission(user, null, AdminPermissionsSchema.VIEW, AdminPermissionsSchema.MANAGE) || canViewByGroup(user);
}
@Override
public boolean canManage(UserModel user) {
if (root.hasOneAdminRole(AdminRoles.ADMIN, AdminRoles.MANAGE_USERS)) {
if (root.hasOneAdminRole(AdminRoles.MANAGE_USERS)) {
return true;
}
boolean result = hasPermission(user, null, AdminPermissionsSchema.MANAGE);
if (!result) {
return canManageByGroup(user);
}
return result;
return hasPermission(user, null, AdminPermissionsSchema.MANAGE) || canManageByGroup(user);
}
@Override
@@ -87,20 +75,20 @@ class UserPermissionsV2 extends UserPermissions {
@Override
public boolean canMapRoles(UserModel user) {
if (canManage(user)) {
if (root.hasOneAdminRole(AdminRoles.MANAGE_USERS)) {
return true;
}
return hasPermission(user, null, AdminPermissionsSchema.MAP_ROLES);
return hasPermission(user, null, AdminPermissionsSchema.MANAGE, AdminPermissionsSchema.MAP_ROLES) || canManageByGroup(user);
}
@Override
public boolean canManageGroupMembership(UserModel user) {
if (canManage(user)) {
if (root.hasOneAdminRole(AdminRoles.MANAGE_USERS)) {
return true;
}
return hasPermission(user, null, AdminPermissionsSchema.MANAGE_GROUP_MEMBERSHIP);
return hasPermission(user, null, AdminPermissionsSchema.MANAGE, AdminPermissionsSchema.MANAGE_GROUP_MEMBERSHIP) || canManageByGroup(user);
}
private boolean hasPermission(UserModel user, EvaluationContext context, String... scopes) {

View File

@@ -6,11 +6,11 @@ import org.junit.jupiter.api.Assertions;
public class ApiUtil {
public static String handleCreatedResponse(Response response) {
Assertions.assertEquals(201, response.getStatus());
String path = response.getLocation().getPath();
String uuid = path.substring(path.lastIndexOf('/') + 1);
response.close();
return uuid;
try (response) {
Assertions.assertEquals(201, response.getStatus());
String path = response.getLocation().getPath();
return path.substring(path.lastIndexOf('/') + 1);
}
}
}

View File

@@ -0,0 +1,320 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.tests.admin.authz.fgap;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.instanceOf;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import static org.keycloak.authorization.AdminPermissionsSchema.MANAGE;
import static org.keycloak.authorization.AdminPermissionsSchema.MANAGE_GROUP_MEMBERSHIP;
import static org.keycloak.authorization.AdminPermissionsSchema.MANAGE_MEMBERS;
import static org.keycloak.authorization.AdminPermissionsSchema.MANAGE_MEMBERSHIP;
import static org.keycloak.authorization.AdminPermissionsSchema.VIEW;
import static org.keycloak.authorization.AdminPermissionsSchema.VIEW_MEMBERS;
import jakarta.ws.rs.ForbiddenException;
import jakarta.ws.rs.NotFoundException;
import jakarta.ws.rs.core.Response;
import java.util.List;
import java.util.Set;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.keycloak.admin.client.Keycloak;
import org.keycloak.admin.client.resource.ScopePermissionsResource;
import org.keycloak.authorization.AdminPermissionsSchema;
import org.keycloak.representations.idm.GroupRepresentation;
import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.representations.idm.authorization.ScopePermissionRepresentation;
import org.keycloak.representations.idm.authorization.UserPolicyRepresentation;
import org.keycloak.testframework.annotations.InjectAdminClient;
import org.keycloak.testframework.annotations.InjectUser;
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
import org.keycloak.testframework.realm.ManagedUser;
import org.keycloak.testframework.realm.UserConfigBuilder;
import org.keycloak.testframework.util.ApiUtil;
@KeycloakIntegrationTest(config = KeycloakAdminPermissionsServerConfig.class)
public class GroupResourceTypeEvaluationTest extends AbstractPermissionTest {
@InjectUser(ref = "alice")
ManagedUser userAlice;
@InjectAdminClient(mode = InjectAdminClient.Mode.MANAGED_REALM, client = "myclient", user = "myadmin")
Keycloak realmAdminClient;
private final String groupName = "top_group";
private final GroupRepresentation topGroup = new GroupRepresentation();;
@BeforeEach // cannot use @BeforeAll, realm is not initializaed yet
public void onBefore() {
topGroup.setName(groupName);
try (Response response = realm.admin().groups().add(topGroup)) {
assertThat(response.getStatus(), equalTo(Response.Status.CREATED.getStatusCode()));
topGroup.setId(ApiUtil.handleCreatedResponse(response));
realm.cleanup().add(r -> r.groups().group(topGroup.getId()).remove());
}
realm.admin().users().get(userAlice.getId()).joinGroup(topGroup.getId());
}
@AfterEach
public void onAfter() {
ScopePermissionsResource permissions = getScopePermissionsResource(client);
for (ScopePermissionRepresentation permission : permissions.findAll(null, null, null, -1, -1)) {
permissions.findById(permission.getId()).remove();
}
}
@Test
public void testCanViewUserByViewGroupMembers() {
UserRepresentation myadmin = realm.admin().users().search("myadmin").get(0);
UserPolicyRepresentation allowMyAdminPermission = createUserPolicy(realm, client, "Only My Admin User Policy", myadmin.getId());
// my admin should NOT be able to see Alice
List<UserRepresentation> search = realmAdminClient.realm(realm.getName()).users().search(null, -1, -1);
assertTrue(search.isEmpty());
// allow my admin to view members of the group where Alice is member of
createGroupPermission(topGroup, Set.of(VIEW_MEMBERS), allowMyAdminPermission);
// my admin should be able to see Alice due to her membership and VIEW_MEMBERS permission
search = realmAdminClient.realm(realm.getName()).users().search(null, -1, -1);
assertEquals(1, search.size());
assertEquals(userAlice.getUsername(), search.get(0).getUsername());
}
@Test
public void testCanViewUserByManageGroupMembers() {
UserRepresentation myadmin = realm.admin().users().search("myadmin").get(0);
UserPolicyRepresentation allowMyAdminPermission = createUserPolicy(realm, client, "Only My Admin User Policy", myadmin.getId());
// my admin should NOT be able to see Alice
List<UserRepresentation> search = realmAdminClient.realm(realm.getName()).users().search(null, -1, -1);
assertTrue(search.isEmpty());
// my admin should not be able to manage yet
try {
realmAdminClient.realm(realm.getName()).users().get(userAlice.getId()).update(UserConfigBuilder.create().email("email@test.com").build());
fail("Expected Exception wasn't thrown.");
} catch (Exception ex) {
assertThat(ex, instanceOf(ForbiddenException.class));
}
// allow my admin to manage members of the group where Alice is member of
createGroupPermission(topGroup, Set.of(MANAGE_MEMBERS), allowMyAdminPermission);
// my admin should be able to see Alice due to her membership and MANAGE_MEMBERS permission
search = realmAdminClient.realm(realm.getName()).users().search(null, -1, -1);
assertEquals(1, search.size());
assertEquals(userAlice.getUsername(), search.get(0).getUsername());
// my admin should be able to update Alice due to her membership and MANAGE_MEMBERS permission
realmAdminClient.realm(realm.getName()).users().get(userAlice.getId()).update(UserConfigBuilder.create().email("email@test.com").build());
assertEquals("email@test.com", realmAdminClient.realm(realm.getName()).users().get(userAlice.getId()).toRepresentation().getEmail());
}
@Test
public void testManageAllGroups() {
// myadmin shouldn't be able to create groups just yet
try (Response response = realmAdminClient.realm(realm.getName()).groups().add(new GroupRepresentation())) {
assertEquals(Response.Status.FORBIDDEN.getStatusCode(), response.getStatus());
}
// myadmin shouldn't be able to add child for a group
try (Response response = realmAdminClient.realm(realm.getName()).groups().group(topGroup.getId()).subGroup(new GroupRepresentation())) {
assertEquals(Response.Status.FORBIDDEN.getStatusCode(), response.getStatus());
}
// myadmin shouldn't be able to map roles for group
try {
realmAdminClient.realm(realm.getName()).groups().group(topGroup.getId()).roles().realmLevel().add(List.of());
fail("Expected Exception wasn't thrown.");
} catch (Exception ex) {
assertThat(ex, instanceOf(ForbiddenException.class));
}
//create all-groups permission for "myadmin" (so that myadmin can manage all groups in the realm)
UserPolicyRepresentation policy = createUserPolicy(realm, client, "Only My Admin User Policy", realm.admin().users().search("myadmin").get(0).getId());
createAllGroupsPermission(policy, Set.of(MANAGE));
// creating group requires manage scope
GroupRepresentation group = new GroupRepresentation();
group.setName("testGroup");
String testGroupId = ApiUtil.handleCreatedResponse(realmAdminClient.realm(realm.getName()).groups().add(group));
group.setId(testGroupId);
// it should be possible to update the group due to fallback to all-groups permission
group.setName("newGroup");
realmAdminClient.realm(realm.getName()).groups().group(testGroupId).update(group);
assertEquals("newGroup", realmAdminClient.realm(realm.getName()).groups().group(testGroupId).toRepresentation().getName());
// it should be possible to add the child to the group now
try (Response response = realmAdminClient.realm(realm.getName()).groups().group(topGroup.getId()).subGroup(group)) {
assertEquals(Response.Status.NO_CONTENT.getStatusCode(), response.getStatus());
}
// it should be possible to map roles now
// trying with non existent role as we need to test manage permission for groups (not `auth.roles().requireMapRole(roleModel);`)
// expecting NotFoundException
try {
realmAdminClient.realm(realm.getName()).groups().group(topGroup.getId()).roles().realmLevel().add(List.of(new RoleRepresentation("non_existent", null, false)));
fail("Expected Exception wasn't thrown.");
} catch (Exception ex) {
assertThat(ex, instanceOf(NotFoundException.class));
}
}
@Test
public void testManageGroup() {
// create group
GroupRepresentation myGroup = new GroupRepresentation();
myGroup.setName("my_group");
try (Response response = realm.admin().groups().add(myGroup)) {
assertThat(response.getStatus(), equalTo(Response.Status.CREATED.getStatusCode()));
myGroup.setId(ApiUtil.handleCreatedResponse(response));
realm.cleanup().add(r -> r.groups().group(myGroup.getId()).remove());
}
//create group permission for "myadmin" to manage the myGroup
UserPolicyRepresentation policy = createUserPolicy(realm, client, "Only My Admin User Policy", realm.admin().users().search("myadmin").get(0).getId());
createGroupPermission(myGroup, Set.of(MANAGE), policy);
// myadmin shouldn't be able to update the topGroup
try {
realmAdminClient.realm(realm.getName()).groups().group(topGroup.getId()).update(myGroup);
fail("Expected Exception wasn't thrown.");
} catch (Exception ex) {
assertThat(ex, instanceOf(ForbiddenException.class));
}
// it should be possible to update the myGroup
myGroup.setName("newGroup");
realmAdminClient.realm(realm.getName()).groups().group(myGroup.getId()).update(myGroup);
assertEquals("newGroup", realmAdminClient.realm(realm.getName()).groups().group(myGroup.getId()).toRepresentation().getName());
// it should not be possible to add child to the topGroup
GroupRepresentation subGroup = new GroupRepresentation();
subGroup.setName("subGroup");
try (Response response = realmAdminClient.realm(realm.getName()).groups().group(topGroup.getId()).subGroup(subGroup)) {
assertEquals(Response.Status.FORBIDDEN.getStatusCode(), response.getStatus());
}
// it should be possible to add child to the myGroup
try (Response response = realmAdminClient.realm(realm.getName()).groups().group(myGroup.getId()).subGroup(subGroup)) {
assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus());
}
// it should not be possible to map roles to topGroup
try {
realmAdminClient.realm(realm.getName()).groups().group(topGroup.getId()).roles().realmLevel().add(List.of(new RoleRepresentation("non_existent", null, false)));
fail("Expected Exception wasn't thrown.");
} catch (Exception ex) {
assertThat(ex, instanceOf(ForbiddenException.class));
}
// it should be possible to map roles to myGroup
// trying with non existent role as we need to test manage permission for groups (not `auth.roles().requireMapRole(roleModel);`)
// expecting NotFoundException
try {
realmAdminClient.realm(realm.getName()).groups().group(myGroup.getId()).roles().realmLevel().add(List.of(new RoleRepresentation("non_existent", null, false)));
fail("Expected Exception wasn't thrown.");
} catch (Exception ex) {
assertThat(ex, instanceOf(NotFoundException.class));
}
}
@Test
public void testViewGroups() {
UserPolicyRepresentation policy = createUserPolicy(realm, client, "Only My Admin User Policy", realm.admin().users().search("myadmin").get(0).getId());
// should not see the groups
List<GroupRepresentation> search = realmAdminClient.realm(realm.getName()).groups().groups();
assertThat(search, hasSize(0));
// create group
GroupRepresentation myGroup = new GroupRepresentation();
myGroup.setName("my_group");
try (Response response = realm.admin().groups().add(myGroup)) {
assertThat(response.getStatus(), equalTo(Response.Status.CREATED.getStatusCode()));
myGroup.setId(ApiUtil.handleCreatedResponse(response));
realm.cleanup().add(r -> r.groups().group(myGroup.getId()).remove());
}
//create permission to view myGroup
createGroupPermission(myGroup, Set.of(VIEW), policy);
// myadmin should be able to view only myGroup
search = realmAdminClient.realm(realm.getName()).groups().groups();
assertThat(search, hasSize(1));
assertThat(search.get(0).getName(), equalTo(myGroup.getName()));
// create view all groups permission for myadmin
createAllGroupsPermission(policy, Set.of(VIEW));
// now two groups should be returned (myGroup, topGroup)
search = realmAdminClient.realm(realm.getName()).groups().groups();
assertThat(search, hasSize(2));
}
@Test
public void testManageGroupMembership() {
// myadmin shouldn't be able to manage group membership of the user just yet
try {
realmAdminClient.realm(realm.getName()).users().get(userAlice.getId()).joinGroup("no-such");
fail("Expected Exception wasn't thrown.");
} catch (Exception ex) {
assertThat(ex, instanceOf(ForbiddenException.class));
}
//create all-users permission for "myadmin" (so that myadmin can add users into a group)
UserPolicyRepresentation policy = createUserPolicy(realm, client, "Only My Admin User Policy", realm.admin().users().search("myadmin").get(0).getId());
createAllUserPermission(policy, Set.of(MANAGE_GROUP_MEMBERSHIP));
//create group permission to allow manage membership for the group
createGroupPermission(topGroup, Set.of(MANAGE_MEMBERSHIP), policy);
//create new user
String bobId = ApiUtil.handleCreatedResponse(realm.admin().users().create(UserConfigBuilder.create().username("bob").build()));
realm.cleanup().add(r -> r.users().delete(bobId));
//check myadmin can manage membership
realmAdminClient.realm(realm.getName()).users().get(bobId).joinGroup(topGroup.getId());
}
private ScopePermissionRepresentation createAllGroupsPermission(UserPolicyRepresentation policy, Set<String> scopes) {
return createAllPermission(client, AdminPermissionsSchema.GROUPS_RESOURCE_TYPE, policy, scopes);
}
private ScopePermissionRepresentation createAllUserPermission(UserPolicyRepresentation policy, Set<String> scopes) {
return createAllPermission(client, AdminPermissionsSchema.USERS_RESOURCE_TYPE, policy, scopes);
}
private ScopePermissionRepresentation createGroupPermission(GroupRepresentation group, Set<String> scopes, UserPolicyRepresentation... policies) {
return createPermission(client, group.getId(), AdminPermissionsSchema.GROUPS_RESOURCE_TYPE, scopes, policies);
}
}

View File

@@ -31,7 +31,9 @@ public class RealmAdminPermissionsConfig implements RealmConfig {
.email("myadmin@localhost")
.emailVerified()
.password("password")
.clientRoles(Constants.REALM_MANAGEMENT_CLIENT_ID, AdminRoles.QUERY_USERS);
.clientRoles(Constants.REALM_MANAGEMENT_CLIENT_ID,
AdminRoles.QUERY_USERS,
AdminRoles.QUERY_GROUPS);
realm.addClient("myclient")
.secret("mysecret")
.directAccessGrants();

View File

@@ -32,7 +32,6 @@ import static org.keycloak.authorization.AdminPermissionsSchema.VIEW;
import jakarta.ws.rs.ForbiddenException;
import jakarta.ws.rs.NotFoundException;
import jakarta.ws.rs.core.Response;
import java.util.Arrays;
import java.util.List;
import java.util.Set;