mirror of
https://github.com/keycloak/keycloak.git
synced 2025-12-21 06:20:05 -06:00
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:
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user