Skip to content

Commit 679f446

Browse files
vramikpedroigor
authored andcommitted
Add Groups resource type and scopes to authorization schema and evaluation implementation
Closes #35562 Signed-off-by: vramik <[email protected]>
1 parent 7a8d181 commit 679f446

File tree

19 files changed

+831
-126
lines changed

19 files changed

+831
-126
lines changed

rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/BruteForceUsersResource.java

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -144,16 +144,12 @@ public final Stream<BruteUser> searchUser(@QueryParam("search") String search,
144144
private Stream<BruteUser> searchForUser(Map<String, String> attributes, RealmModel realm, UserPermissionEvaluator usersEvaluator, Boolean briefRepresentation, Integer firstResult, Integer maxResults, Boolean includeServiceAccounts) {
145145
attributes.put(UserModel.INCLUDE_SERVICE_ACCOUNT, includeServiceAccounts.toString());
146146

147-
if (!auth.users().canView()) {
148-
Set<String> groupModels = auth.groups().getGroupsWithViewPermission();
149-
150-
if (!groupModels.isEmpty()) {
151-
session.setAttribute(UserModel.GROUPS, groupModels);
152-
}
147+
Set<String> groupIds = auth.groups().getGroupIdsWithViewPermission();
148+
if (!groupIds.isEmpty()) {
149+
session.setAttribute(UserModel.GROUPS, groupIds);
153150
}
154151

155-
Stream<UserModel> userModels = session.users().searchForUserStream(realm, attributes, firstResult, maxResults);
156-
return toRepresentation(realm, usersEvaluator, briefRepresentation, userModels);
152+
return toRepresentation(realm, usersEvaluator, briefRepresentation, session.users().searchForUserStream(realm, attributes, firstResult, maxResults));
157153
}
158154

159155
private Stream<BruteUser> toRepresentation(RealmModel realm, UserPermissionEvaluator usersEvaluator,

server-spi-private/src/main/java/org/keycloak/authorization/AdminPermissionsSchema.java

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import org.keycloak.models.ClientModel;
3434
import org.keycloak.models.ClientProvider;
3535
import org.keycloak.models.Constants;
36+
import org.keycloak.models.GroupModel;
3637
import org.keycloak.models.KeycloakSession;
3738
import org.keycloak.models.ModelException;
3839
import org.keycloak.models.ModelIllegalStateException;
@@ -50,27 +51,42 @@
5051
import org.keycloak.representations.idm.authorization.ScopeRepresentation;
5152

5253
public class AdminPermissionsSchema extends AuthorizationSchema {
53-
54-
public static final String USERS_RESOURCE_TYPE = "Users";
54+
5555
public static final String CLIENTS_RESOURCE_TYPE = "Clients";
56+
public static final String GROUPS_RESOURCE_TYPE = "Groups";
57+
public static final String USERS_RESOURCE_TYPE = "Users";
5658

57-
//scopes
59+
// common scopes
5860
public static final String MANAGE = "manage";
5961
public static final String VIEW = "view";
60-
public static final String IMPERSONATE = "impersonate";
61-
public static final String MAP_ROLES = "map-roles";
62-
public static final String MANAGE_GROUP_MEMBERSHIP = "manage-group-membership";
62+
63+
// client specific scopes
6364
public static final String CONFIGURE = "configure";
6465
public static final String MAP_ROLES_CLIENT_SCOPE = "map-roles-client-scope";
6566
public static final String MAP_ROLES_COMPOSITE = "map-roles-composite";
6667

67-
public static final ResourceType USERS = new ResourceType(USERS_RESOURCE_TYPE, Set.of(MANAGE, VIEW, IMPERSONATE, MAP_ROLES, MANAGE_GROUP_MEMBERSHIP));
68+
// group specific scopes
69+
public static final String MANAGE_MEMBERSHIP = "manage-membership";
70+
public static final String MANAGE_MEMBERS = "manage-members";
71+
public static final String VIEW_MEMBERS = "view-members";
72+
73+
// user specific scopes
74+
public static final String IMPERSONATE = "impersonate";
75+
public static final String MAP_ROLES = "map-roles";
76+
public static final String MANAGE_GROUP_MEMBERSHIP = "manage-group-membership";
77+
6878
public static final ResourceType CLIENTS = new ResourceType(CLIENTS_RESOURCE_TYPE, Set.of(CONFIGURE, MANAGE, MAP_ROLES, MAP_ROLES_CLIENT_SCOPE, MAP_ROLES_COMPOSITE, VIEW));
79+
public static final ResourceType GROUPS = new ResourceType(GROUPS_RESOURCE_TYPE, Set.of(MANAGE, VIEW, MANAGE_MEMBERSHIP, MANAGE_MEMBERS, VIEW_MEMBERS));
80+
public static final ResourceType USERS = new ResourceType(USERS_RESOURCE_TYPE, Set.of(MANAGE, VIEW, IMPERSONATE, MAP_ROLES, MANAGE_GROUP_MEMBERSHIP));
6981

7082
public static final AdminPermissionsSchema SCHEMA = new AdminPermissionsSchema();
7183

7284
private AdminPermissionsSchema() {
73-
super(Map.of(USERS_RESOURCE_TYPE, USERS, CLIENTS_RESOURCE_TYPE, CLIENTS));
85+
super(Map.of(
86+
CLIENTS_RESOURCE_TYPE, CLIENTS,
87+
GROUPS_RESOURCE_TYPE, GROUPS,
88+
USERS_RESOURCE_TYPE, USERS
89+
));
7490
}
7591

7692
public Resource getOrCreateResource(KeycloakSession session, ResourceServer resourceServer, String policyType, String resourceType, String id) {
@@ -86,12 +102,13 @@ public Resource getOrCreateResource(KeycloakSession session, ResourceServer reso
86102
return resource;
87103
}
88104

89-
String name = null;
105+
String name;
90106

91-
if (USERS.getType().equals(resourceType)) {
92-
name = resolveUser(session, id);
93-
} else if (CLIENTS.getType().equals(resourceType)) {
94-
name = resolveClient(session, id);
107+
switch (resourceType) {
108+
case CLIENTS_RESOURCE_TYPE -> name = resolveClient(session, id);
109+
case GROUPS_RESOURCE_TYPE -> name = resolveGroup(session, id);
110+
case USERS_RESOURCE_TYPE -> name = resolveUser(session, id);
111+
default -> throw new IllegalStateException("Resource type [" + resourceType + "] not found.");
95112
}
96113

97114
if (name == null) {
@@ -161,6 +178,13 @@ public void throwExceptionIfAdminPermissionClient(KeycloakSession session, Strin
161178
}
162179
}
163180

181+
private String resolveGroup(KeycloakSession session, String id) {
182+
RealmModel realm = session.getContext().getRealm();
183+
GroupModel group = session.groups().getGroupById(realm, id);
184+
185+
return group == null ? null : group.getId();
186+
}
187+
164188
private String resolveUser(KeycloakSession session, String id) {
165189
RealmModel realm = session.getContext().getRealm();
166190
UserModel user = session.users().getUserById(realm, id);

server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -173,10 +173,6 @@ public static GroupRepresentation toRepresentation(GroupModel group, boolean ful
173173
return rep;
174174
}
175175

176-
public static Stream<GroupModel> searchGroupModelsByAttributes(KeycloakSession session, RealmModel realm, Map<String,String> attributes, Integer first, Integer max) {
177-
return session.groups().searchGroupsByAttributes(realm, attributes, first, max);
178-
}
179-
180176
@Deprecated
181177
public static Stream<GroupRepresentation> toGroupHierarchy(KeycloakSession session, RealmModel realm, boolean full) {
182178
return session.groups().getTopLevelGroupsStream(realm, null, null)

services/src/main/java/org/keycloak/services/resources/admin/GroupResource.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -174,10 +174,9 @@ public Stream<GroupRepresentation> getSubGroups(
174174
@Parameter(description = "The maximum number of results that are to be returned. Defaults to 10") @QueryParam("max") @DefaultValue("10") Integer max,
175175
@Parameter(description = "Boolean which defines whether brief groups representations are returned or not (default: false)") @QueryParam("briefRepresentation") @DefaultValue("false") Boolean briefRepresentation) {
176176
this.auth.groups().requireView(group);
177-
boolean canViewGlobal = auth.groups().canView();
178177
return paginatedStream(
179178
group.getSubGroupsStream(search, exact, -1, -1)
180-
.filter(g -> canViewGlobal || auth.groups().canView(g)), first, max)
179+
.filter(auth.groups()::canView), first, max)
181180
.map(g -> GroupUtils.populateSubGroupCount(g, GroupUtils.toRepresentation(auth.groups(), g, !briefRepresentation)));
182181
}
183182

@@ -204,7 +203,7 @@ public Response addChild(GroupRepresentation rep) {
204203

205204
try {
206205
Response.ResponseBuilder builder = Response.status(204);
207-
GroupModel child = null;
206+
GroupModel child;
208207
if (rep.getId() != null) {
209208
child = realm.getGroupById(rep.getId());
210209
if (child == null) {

services/src/main/java/org/keycloak/services/resources/admin/GroupsResource.java

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@
4747
import org.keycloak.models.KeycloakSession;
4848
import org.keycloak.models.ModelDuplicateException;
4949
import org.keycloak.models.RealmModel;
50-
import org.keycloak.models.utils.ModelToRepresentation;
5150
import org.keycloak.organization.utils.Organizations;
5251
import org.keycloak.representations.idm.GroupRepresentation;
5352
import org.keycloak.services.ErrorResponse;
@@ -100,7 +99,7 @@ public Stream<GroupRepresentation> getGroups(@QueryParam("search") String search
10099
Stream<GroupModel> stream;
101100
if (Objects.nonNull(searchQuery)) {
102101
Map<String, String> attributes = SearchQueryUtils.getFields(searchQuery);
103-
stream = ModelToRepresentation.searchGroupModelsByAttributes(session, realm, attributes, firstResult, maxResults);
102+
stream = session.groups().searchGroupsByAttributes(realm, attributes, firstResult, maxResults);
104103
} else if (Objects.nonNull(search)) {
105104
stream = session.groups().searchForGroupByNameStream(realm, search.trim(), exact, firstResult, maxResults);
106105
} else {
@@ -110,9 +109,8 @@ public Stream<GroupRepresentation> getGroups(@QueryParam("search") String search
110109
if (populateHierarchy) {
111110
return GroupUtils.populateGroupHierarchyFromSubGroups(session, realm, stream, !briefRepresentation, groupsEvaluator);
112111
}
113-
boolean canViewGlobal = groupsEvaluator.canView();
114-
return stream
115-
.filter(g -> canViewGlobal || groupsEvaluator.canView(g))
112+
113+
return stream.filter(groupsEvaluator::canView)
116114
.map(g -> GroupUtils.populateSubGroupCount(g, GroupUtils.toRepresentation(groupsEvaluator, g, !briefRepresentation)));
117115
}
118116

services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,6 @@
7171
import java.util.Optional;
7272
import java.util.Properties;
7373
import java.util.Set;
74-
import java.util.function.Predicate;
7574
import java.util.stream.Collectors;
7675
import java.util.stream.Stream;
7776

@@ -396,7 +395,7 @@ public Integer getUsersCount(
396395
} else if (userPermissionEvaluator.canView()) {
397396
return session.users().getUsersCount(realm, search.trim());
398397
} else {
399-
return session.users().getUsersCount(realm, search.trim(), auth.groups().getGroupsWithViewPermission());
398+
return session.users().getUsersCount(realm, search.trim(), auth.groups().getGroupIdsWithViewPermission());
400399
}
401400
} else if (last != null || first != null || email != null || username != null || emailVerified != null || enabled != null || !searchAttributes.isEmpty()) {
402401
Map<String, String> parameters = new HashMap<>();
@@ -423,12 +422,12 @@ public Integer getUsersCount(
423422
if (userPermissionEvaluator.canView()) {
424423
return session.users().getUsersCount(realm, parameters);
425424
} else {
426-
return session.users().getUsersCount(realm, parameters, auth.groups().getGroupsWithViewPermission());
425+
return session.users().getUsersCount(realm, parameters, auth.groups().getGroupIdsWithViewPermission());
427426
}
428427
} else if (userPermissionEvaluator.canView()) {
429428
return session.users().getUsersCount(realm);
430429
} else {
431-
return session.users().getUsersCount(realm, auth.groups().getGroupsWithViewPermission());
430+
return session.users().getUsersCount(realm, auth.groups().getGroupIdsWithViewPermission());
432431
}
433432
}
434433

@@ -446,16 +445,12 @@ public UserProfileResource userProfile() {
446445
private Stream<UserRepresentation> searchForUser(Map<String, String> attributes, RealmModel realm, UserPermissionEvaluator usersEvaluator, Boolean briefRepresentation, Integer firstResult, Integer maxResults, Boolean includeServiceAccounts) {
447446
attributes.put(UserModel.INCLUDE_SERVICE_ACCOUNT, includeServiceAccounts.toString());
448447

449-
if (!auth.users().canView()) {
450-
Set<String> groupModels = auth.groups().getGroupsWithViewPermission();
451-
452-
if (!groupModels.isEmpty()) {
453-
session.setAttribute(UserModel.GROUPS, groupModels);
454-
}
448+
Set<String> groupIds = auth.groups().getGroupIdsWithViewPermission();
449+
if (!groupIds.isEmpty()) {
450+
session.setAttribute(UserModel.GROUPS, groupIds);
455451
}
456452

457-
Stream<UserModel> userModels = session.users().searchForUserStream(realm, attributes, firstResult, maxResults).filter(usersEvaluator::canView);
458-
return toRepresentation(realm, usersEvaluator, briefRepresentation, userModels);
453+
return toRepresentation(realm, usersEvaluator, briefRepresentation, session.users().searchForUserStream(realm, attributes, firstResult, maxResults));
459454
}
460455

461456
private Stream<UserRepresentation> toRepresentation(RealmModel realm, UserPermissionEvaluator usersEvaluator, Boolean briefRepresentation, Stream<UserModel> userModels) {

services/src/main/java/org/keycloak/services/resources/admin/permissions/AdminPermissions.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,10 +71,10 @@ public static AdminPermissionManagement management(KeycloakSession session, Real
7171
}
7272

7373
public static void registerListener(ProviderEventManager manager) {
74-
manager.register(new ProviderEventListener() {
75-
@Override
76-
public void onEvent(ProviderEvent event) {
77-
if (Profile.isFeatureEnabled(Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ)) {
74+
if (Profile.isFeatureEnabled(Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ)) {
75+
manager.register(new ProviderEventListener() {
76+
@Override
77+
public void onEvent(ProviderEvent event) {
7878
if (event instanceof RoleContainerModel.RoleRemovedEvent) {
7979
RoleContainerModel.RoleRemovedEvent cast = (RoleContainerModel.RoleRemovedEvent) event;
8080
RoleModel role = cast.getRole();
@@ -94,8 +94,8 @@ public void onEvent(ProviderEvent event) {
9494
management(cast.getKeycloakSession(), cast.getRealm()).groups().setPermissionsEnabled(cast.getGroup(), false);
9595
}
9696
}
97-
}
98-
});
97+
});
98+
}
9999
}
100100

101101

services/src/main/java/org/keycloak/services/resources/admin/permissions/GroupPermissionEvaluator.java

Lines changed: 87 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
*/
1717
package org.keycloak.services.resources.admin.permissions;
1818

19+
import org.keycloak.authorization.AdminPermissionsSchema;
20+
import org.keycloak.models.AdminRoles;
1921
import org.keycloak.models.GroupModel;
2022

2123
import java.util.Map;
@@ -26,41 +28,121 @@
2628
* @version $Revision: 1 $
2729
*/
2830
public interface GroupPermissionEvaluator {
31+
32+
/**
33+
* Returns {@code true} if the caller has at least one of {@link AdminRoles#QUERY_GROUPS},
34+
* {@link AdminRoles#MANAGE_USERS} or {@link AdminRoles#VIEW_USERS} roles.
35+
* <p/>
36+
* For V2 only: Also if it has a permission to {@link AdminPermissionsSchema#VIEW} or
37+
* {@link AdminPermissionsSchema#MANAGE} groups.
38+
*/
2939
boolean canList();
3040

41+
/**
42+
* Throws ForbiddenException if {@link #canList()} returns {@code false}.
43+
*/
3144
void requireList();
3245

46+
/**
47+
* Returns {@code true} if the caller has {@link AdminRoles#MANAGE_USERS} role.
48+
* <p/>
49+
* Or if it has a permission to {@link AdminPermissionsSchema#MANAGE} the group.
50+
*/
3351
boolean canManage(GroupModel group);
3452

53+
/**
54+
* Throws ForbiddenException if {@link #canManage(GroupModel)} returns {@code false}.
55+
*/
3556
void requireManage(GroupModel group);
3657

58+
/**
59+
* Returns {@code true} if the caller has one of {@link AdminRoles#MANAGE_USERS} or
60+
* {@link AdminRoles#VIEW_USERS} roles.
61+
* <p/>
62+
* Or if it has a permission to {@link AdminPermissionsSchema#VIEW} or
63+
* {@link AdminPermissionsSchema#MANAGE} the group.
64+
*/
3765
boolean canView(GroupModel group);
3866

67+
/**
68+
* Throws ForbiddenException if {@link #canView(GroupModel)} returns {@code false}.
69+
*/
3970
void requireView(GroupModel group);
4071

72+
/**
73+
* Returns {@code true} if the caller has {@link AdminRoles#MANAGE_USERS} role.
74+
* <p/>
75+
* For V2 only: Also if it has permission to {@link AdminPermissionsSchema#VIEW} or
76+
* {@link AdminPermissionsSchema#MANAGE} groups.
77+
*/
4178
boolean canManage();
4279

80+
/**
81+
* Throws ForbiddenException if {@link #canManage()} returns {@code false}.
82+
*/
4383
void requireManage();
4484

85+
/**
86+
* Returns {@code true} if the caller has one of {@link AdminRoles#MANAGE_USERS} or
87+
* {@link AdminRoles#VIEW_USERS} roles.
88+
* <p/>
89+
* Or if it has a permission to {@link AdminPermissionsSchema#VIEW} or
90+
* {@link AdminPermissionsSchema#MANAGE} groups.
91+
*/
4592
boolean canView();
4693

94+
/**
95+
* Throws ForbiddenException if {@link #canView()} returns {@code false}.
96+
*/
4797
void requireView();
4898

49-
boolean getGroupsWithViewPermission(GroupModel group);
50-
99+
/**
100+
* Throws ForbiddenException if {@link #canViewMembers(GroupModel)} returns {@code false}.
101+
*/
51102
void requireViewMembers(GroupModel group);
52103

104+
/**
105+
* Returns {@code true} if {@link UserPermissionEvaluator#canManage()} evaluates to {@code true}.
106+
* <p/>
107+
* Or if it has a permission to {@link AdminPermissionsSchema#MANAGE_MEMBERS} of the group.
108+
*/
53109
boolean canManageMembers(GroupModel group);
54110

111+
/**
112+
* Returns {@code true} if the caller has one of {@link AdminRoles#MANAGE_USERS} role.
113+
* <p/>
114+
* Or if it has a permission to {@link AdminPermissionsSchema#MANAGE} the group or
115+
* {@link AdminPermissionsSchema#MANAGE_MEMBERSHIP} of the group.
116+
*/
55117
boolean canManageMembership(GroupModel group);
56-
118+
119+
/**
120+
* Returns {@code true} if {@link UserPermissionEvaluator#canView()} evaluates to {@code true}.
121+
* <p/>
122+
* Or if it has a permission to {@link AdminPermissionsSchema#VIEW_MEMBERS} or
123+
* {@link AdminPermissionsSchema#MANAGE_MEMBERS} of the group.
124+
*/
57125
boolean canViewMembers(GroupModel group);
58-
126+
127+
/**
128+
* Throws ForbiddenException if {@link #canManageMembership(GroupModel)} returns {@code false}.
129+
*/
59130
void requireManageMembership(GroupModel group);
60131

132+
/**
133+
* Throws ForbiddenException if {@link #canManageMembership(GroupModel)} returns {@code false}.
134+
*/
61135
void requireManageMembers(GroupModel group);
62136

137+
/**
138+
* Returns Map with information what access the caller for the provided group has.
139+
*/
63140
Map<String, Boolean> getAccess(GroupModel group);
64141

65-
Set<String> getGroupsWithViewPermission();
142+
/**
143+
* If {@link UserPermissionEvaluator#canView()} evaluates to {@code true}, returns empty set.
144+
*
145+
* @return Stream of IDs of groups with view permission.
146+
*/
147+
Set<String> getGroupIdsWithViewPermission();
66148
}

0 commit comments

Comments
 (0)