More efficient listing of assigned and available client role mappings

Closes #23404

Signed-off-by: Sebastian Schuster <sebastian.schuster@bosch.io>
Co-authored-by: Vlasta Ramik <vramik@users.noreply.github.com>
This commit is contained in:
Sebastian Schuster 2023-11-22 14:10:11 +01:00 committed by GitHub
parent 203eb3421a
commit 030f42ec83
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 380 additions and 102 deletions

View file

@ -750,6 +750,16 @@ public class RealmCacheSession implements CacheRealmProvider {
return getRoleDelegate().searchForClientRolesStream(client, search, first, max); return getRoleDelegate().searchForClientRolesStream(client, search, first, max);
} }
@Override
public Stream<RoleModel> searchForClientRolesStream(RealmModel realm, Stream<String> ids, String search, Integer first, Integer max) {
return getRoleDelegate().searchForClientRolesStream(realm, ids, search, first, max);
}
@Override
public Stream<RoleModel> searchForClientRolesStream(RealmModel realm, String search, Stream<String> excludedIds, Integer first, Integer max) {
return getRoleDelegate().searchForClientRolesStream(realm, search, excludedIds, first, max);
}
@Override @Override
public Stream<RoleModel> searchForRolesStream(RealmModel realm, String search, Integer first, Integer max) { public Stream<RoleModel> searchForRolesStream(RealmModel realm, String search, Integer first, Integer max) {
return getRoleDelegate().searchForRolesStream(realm, search, first, max); return getRoleDelegate().searchForRolesStream(realm, search, first, max);

View file

@ -327,6 +327,53 @@ public class JpaRealmProvider implements RealmProvider, ClientProvider, ClientSc
.map(g -> session.roles().getRoleById(realm, g)); .map(g -> session.roles().getRoleById(realm, g));
} }
@Override
public Stream<RoleModel> searchForClientRolesStream(RealmModel realm, Stream<String> ids, String search, Integer first, Integer max) {
return searchForClientRolesStream(realm, ids, search, first, max, false);
}
@Override
public Stream<RoleModel> searchForClientRolesStream(RealmModel realm, String search, Stream<String> excludedIds, Integer first, Integer max) {
return searchForClientRolesStream(realm, excludedIds, search, first, max, true);
}
private Stream<RoleModel> searchForClientRolesStream(RealmModel realm, Stream<String> ids, String search, Integer first, Integer max, boolean negateIds) {
List<String> idList = null;
if(ids != null) {
idList = ids.collect(Collectors.toList());
if(idList.isEmpty() && !negateIds)
return Stream.empty();
}
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<RoleEntity> query = cb.createQuery(RoleEntity.class);
Root<RoleEntity> roleRoot = query.from(RoleEntity.class);
Root<ClientEntity> clientRoot = query.from(ClientEntity.class);
List<Predicate> predicates = new ArrayList<>();
predicates.add(cb.equal(roleRoot.get("realmId"), realm.getId()));
predicates.add(cb.isTrue(roleRoot.get("clientRole")));
predicates.add(cb.equal(roleRoot.get("clientId"),clientRoot.get("id")));
if(search != null && !search.isEmpty()) {
search = "%" + search.trim().toLowerCase() + "%";
predicates.add(cb.or(
cb.like(cb.lower(roleRoot.get("name")), search),
cb.like(cb.lower(clientRoot.get("clientId")), search)
));
}
if(idList != null && !idList.isEmpty()) {
Predicate idFilter = roleRoot.get("id").in(idList);
if(negateIds) idFilter = cb.not(idFilter);
predicates.add(idFilter);
}
query.select(roleRoot).where(predicates.toArray(new Predicate[0]))
.orderBy(
cb.asc(clientRoot.get("clientId")),
cb.asc(roleRoot.get("name")));
return closing(paginateQuery(em.createQuery(query),first,max).getResultStream())
.map(roleEntity -> new RoleAdapter(session, realm, em, roleEntity));
}
@Override @Override
public Stream<RoleModel> getClientRolesStream(ClientModel client, Integer first, Integer max) { public Stream<RoleModel> getClientRolesStream(ClientModel client, Integer first, Integer max) {
TypedQuery<RoleEntity> query = em.createNamedQuery("getClientRoles", RoleEntity.class); TypedQuery<RoleEntity> query = em.createNamedQuery("getClientRoles", RoleEntity.class);

View file

@ -244,6 +244,28 @@ public class RoleStorageManager implements RoleProvider {
return Stream.concat(local, ext); return Stream.concat(local, ext);
} }
@Override
public Stream<RoleModel> searchForClientRolesStream(RealmModel realm, Stream<String> ids, String search, Integer first, Integer max) {
Stream<RoleModel> local = localStorage().searchForClientRolesStream(realm, ids, search, first, max);
Stream<RoleModel> ext = getEnabledStorageProviders(session, realm, RoleLookupProvider.class)
.flatMap(ServicesUtils.timeBound(session,
roleStorageProviderTimeout,
p -> ((RoleLookupProvider) p).searchForClientRolesStream(realm, ids, search, first, max)));
return Stream.concat(local, ext);
}
@Override
public Stream<RoleModel> searchForClientRolesStream(RealmModel realm, String search, Stream<String> excludedIds, Integer first, Integer max) {
Stream<RoleModel> local = localStorage().searchForClientRolesStream(realm, search, excludedIds, first, max);
Stream<RoleModel> ext = getEnabledStorageProviders(session, realm, RoleLookupProvider.class)
.flatMap(ServicesUtils.timeBound(session,
roleStorageProviderTimeout,
p -> ((RoleLookupProvider) p).searchForClientRolesStream(realm, search, excludedIds, first, max)));
return Stream.concat(local, ext);
}
@Override @Override
public void close() { public void close() {
} }

View file

@ -28,16 +28,8 @@ import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluato
public class AuthenticationManagementResource extends RoleMappingResource { public class AuthenticationManagementResource extends RoleMappingResource {
private final KeycloakSession session;
private RealmModel realm;
private AdminPermissionEvaluator auth;
public AuthenticationManagementResource(KeycloakSession session, RealmModel realm, AdminPermissionEvaluator auth) { public AuthenticationManagementResource(KeycloakSession session, RealmModel realm, AdminPermissionEvaluator auth) {
super(realm, auth); super(session, realm, auth);
this.realm = realm;
this.auth = auth;
this.session = session;
} }
@GET @GET

View file

@ -2,7 +2,9 @@ package org.keycloak.admin.ui.rest;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.function.Predicate; import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import jakarta.ws.rs.Consumes; import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DefaultValue; import jakarta.ws.rs.DefaultValue;
import jakarta.ws.rs.ForbiddenException; import jakarta.ws.rs.ForbiddenException;
@ -18,6 +20,8 @@ import org.eclipse.microprofile.openapi.annotations.media.Content;
import org.eclipse.microprofile.openapi.annotations.media.Schema; import org.eclipse.microprofile.openapi.annotations.media.Schema;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.keycloak.admin.ui.rest.model.ClientRole; import org.keycloak.admin.ui.rest.model.ClientRole;
import org.keycloak.admin.ui.rest.model.RoleMapper;
import org.keycloak.common.Profile;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientScopeModel; import org.keycloak.models.ClientScopeModel;
import org.keycloak.models.GroupModel; import org.keycloak.models.GroupModel;
@ -28,16 +32,16 @@ import org.keycloak.models.UserModel;
import org.keycloak.models.UserProvider; import org.keycloak.models.UserProvider;
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
public class AvailableRoleMappingResource extends RoleMappingResource { import static org.keycloak.services.resources.admin.permissions.ClientPermissionManagement.MAP_ROLES_CLIENT_SCOPE;
private final KeycloakSession session; import static org.keycloak.services.resources.admin.permissions.ClientPermissionManagement.MAP_ROLES_COMPOSITE_SCOPE;
private final RealmModel realm; import static org.keycloak.services.resources.admin.permissions.ClientPermissionManagement.MAP_ROLES_SCOPE;
private final AdminPermissionEvaluator auth; import static org.keycloak.services.resources.admin.permissions.RolePermissionManagement.MAP_ROLE_CLIENT_SCOPE_SCOPE;
import static org.keycloak.services.resources.admin.permissions.RolePermissionManagement.MAP_ROLE_COMPOSITE_SCOPE;
import static org.keycloak.services.resources.admin.permissions.RolePermissionManagement.MAP_ROLE_SCOPE;
public class AvailableRoleMappingResource extends RoleMappingResource {
public AvailableRoleMappingResource(KeycloakSession session, RealmModel realm, AdminPermissionEvaluator auth) { public AvailableRoleMappingResource(KeycloakSession session, RealmModel realm, AdminPermissionEvaluator auth) {
super(realm, auth); super(session, realm, auth);
this.realm = realm;
this.auth = auth;
this.session = session;
} }
@GET @GET
@ -45,8 +49,8 @@ public class AvailableRoleMappingResource extends RoleMappingResource {
@Consumes({"application/json"}) @Consumes({"application/json"})
@Produces({"application/json"}) @Produces({"application/json"})
@Operation( @Operation(
summary = "List all composite client roles for this client scope", summary = "List all available client roles for this client scope",
description = "This endpoint returns all the client role mapping for a specific client scope" description = "This endpoint returns all the client roles the user can add to a specific client scope"
) )
@APIResponse( @APIResponse(
responseCode = "200", responseCode = "200",
@ -58,14 +62,22 @@ public class AvailableRoleMappingResource extends RoleMappingResource {
) )
)} )}
) )
public final List<ClientRole> listCompositeClientScopeRoleMappings(@PathParam("id") String id, @QueryParam("first") public final List<ClientRole> listAvailableClientScopeRoleMappings(@PathParam("id") String id, @QueryParam("first")
@DefaultValue("0") long first, @QueryParam("max") @DefaultValue("10") long max, @QueryParam("search") @DefaultValue("") String search) { @DefaultValue("0") int first, @QueryParam("max") @DefaultValue("10") int max, @QueryParam("search") @DefaultValue("") String search) {
ClientScopeModel scopeModel = this.realm.getClientScopeById(id); ClientScopeModel scopeModel = this.realm.getClientScopeById(id);
if (scopeModel == null) { if (scopeModel == null) {
throw new NotFoundException("Could not find client scope"); throw new NotFoundException("Could not find client scope");
} else { } else {
this.auth.clients().requireView(scopeModel); if(this.auth.clients().canManage() || !Profile.isFeatureEnabled(Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ)) {
return this.mapping(((Predicate<RoleModel>) scopeModel::hasDirectScope).negate(), auth.roles()::canMapClientScope, first, max, search); this.auth.clients().requireManage();
Stream<String> excludedRoleIds = scopeModel.getScopeMappingsStream().filter(RoleModel::isClientRole).map(RoleModel::getId);
return searchForClientRolesByExcludedIds(realm, search, first, max, excludedRoleIds);
} else {
this.auth.clients().requireView(scopeModel);
Set<String> roleIds = getRoleIdsWithPermissions(MAP_ROLE_CLIENT_SCOPE_SCOPE, MAP_ROLES_CLIENT_SCOPE);
scopeModel.getScopeMappingsStream().forEach(role -> roleIds.remove(role.getId()));
return searchForClientRolesByIds(realm, roleIds.stream(), search, first, max);
}
} }
} }
@ -74,8 +86,8 @@ public class AvailableRoleMappingResource extends RoleMappingResource {
@Consumes({"application/json"}) @Consumes({"application/json"})
@Produces({"application/json"}) @Produces({"application/json"})
@Operation( @Operation(
summary = "List all composite client roles for this client", summary = "List all available client roles for the scope mapping of this client",
description = "This endpoint returns all the client role mapping for a specific client" description = "This endpoint returns all the client roles a user can add to the scope mapping of a specific client"
) )
@APIResponse( @APIResponse(
responseCode = "200", responseCode = "200",
@ -87,14 +99,22 @@ public class AvailableRoleMappingResource extends RoleMappingResource {
) )
)} )}
) )
public final List<ClientRole> listCompositeClientRoleMappings(@PathParam("id") String id, @QueryParam("first") public final List<ClientRole> listAvailableClientRoleMappings(@PathParam("id") String id, @QueryParam("first")
@DefaultValue("0") long first, @QueryParam("max") @DefaultValue("10") long max, @QueryParam("search") @DefaultValue("") String search) { @DefaultValue("0") int first, @QueryParam("max") @DefaultValue("10") int max, @QueryParam("search") @DefaultValue("") String search) {
ClientModel client = this.realm.getClientById(id); ClientModel client = this.realm.getClientById(id);
if (client == null) { if (client == null) {
throw new NotFoundException("Could not find client"); throw new NotFoundException("Could not find client");
} else { } else {
this.auth.clients().requireView(client); if(this.auth.clients().canManage() || !Profile.isFeatureEnabled(Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ)) {
return this.mapping(((Predicate<RoleModel>) client::hasDirectScope).negate(), first, max, search); this.auth.clients().requireManage();
Stream<String> excludedRoleIds = Stream.concat(client.getScopeMappingsStream(), client.getRolesStream()).filter(RoleModel::isClientRole).map(RoleModel::getId);
return searchForClientRolesByExcludedIds(realm, search, first, max, excludedRoleIds);
} else {
this.auth.clients().requireView(client);
Set<String> roleIds = getRoleIdsWithPermissions(MAP_ROLE_CLIENT_SCOPE_SCOPE, MAP_ROLES_CLIENT_SCOPE);
Stream.concat(client.getScopeMappingsStream(), client.getRolesStream()).forEach(role -> roleIds.remove(role.getId()));
return searchForClientRolesByIds(realm, roleIds.stream(), search, first, max);
}
} }
} }
@ -103,8 +123,8 @@ public class AvailableRoleMappingResource extends RoleMappingResource {
@Consumes({"application/json"}) @Consumes({"application/json"})
@Produces({"application/json"}) @Produces({"application/json"})
@Operation( @Operation(
summary = "List all composite client roles for this group", summary = "List all available client roles for this group",
description = "This endpoint returns all the client role mapping for a specific group" description = "This endpoint returns all available client roles a user can add to a specific group"
) )
@APIResponse( @APIResponse(
responseCode = "200", responseCode = "200",
@ -116,14 +136,22 @@ public class AvailableRoleMappingResource extends RoleMappingResource {
) )
)} )}
) )
public final List<ClientRole> listCompositeGroupRoleMappings(@PathParam("id") String id, @QueryParam("first") public final List<ClientRole> listAvailableGroupRoleMappings(@PathParam("id") String id, @QueryParam("first")
@DefaultValue("0") long first, @QueryParam("max") @DefaultValue("10") long max, @QueryParam("search") @DefaultValue("") String search) { @DefaultValue("0") int first, @QueryParam("max") @DefaultValue("10") int max, @QueryParam("search") @DefaultValue("") String search) {
GroupModel group = this.realm.getGroupById(id); GroupModel group = this.realm.getGroupById(id);
if (group == null) { if (group == null) {
throw new NotFoundException("Could not find group"); throw new NotFoundException("Could not find group");
} else { } else {
this.auth.groups().requireView(group); if(this.auth.users().canManage() || !Profile.isFeatureEnabled(Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ)) {
return this.mapping(((Predicate<RoleModel>) group::hasDirectRole).negate(), first, max, search); this.auth.users().requireManage();
Stream<String> excludedRoleIds = group.getRoleMappingsStream().filter(RoleModel::isClientRole).map(RoleModel::getId);
return searchForClientRolesByExcludedIds(realm, search, first, max, excludedRoleIds);
} else {
this.auth.groups().requireView(group);
Set<String> roleIds = getRoleIdsWithPermissions(MAP_ROLE_SCOPE, MAP_ROLES_SCOPE);
group.getRoleMappingsStream().forEach(role -> roleIds.remove(role.getId()));
return searchForClientRolesByIds(realm, roleIds.stream(), search, first, max);
}
} }
} }
@ -132,8 +160,8 @@ public class AvailableRoleMappingResource extends RoleMappingResource {
@Consumes({"application/json"}) @Consumes({"application/json"})
@Produces({"application/json"}) @Produces({"application/json"})
@Operation( @Operation(
summary = "List all composite client roles for this user", summary = "List all available client roles for this user",
description = "This endpoint returns all the client role mapping for a specific user" description = "This endpoint returns all the available client roles a user can add to a specific user"
) )
@APIResponse( @APIResponse(
responseCode = "200", responseCode = "200",
@ -145,17 +173,25 @@ public class AvailableRoleMappingResource extends RoleMappingResource {
) )
)} )}
) )
public final List<ClientRole> listCompositeUserRoleMappings(@PathParam("id") String id, @QueryParam("first") @DefaultValue("0") long first, public final List<ClientRole> listAvailableUserRoleMappings(@PathParam("id") String id, @QueryParam("first") @DefaultValue("0") int first,
@QueryParam("max") @DefaultValue("10") long max, @QueryParam("search") @DefaultValue("") String search) { @QueryParam("max") @DefaultValue("10") int max, @QueryParam("search") @DefaultValue("") String search) {
UserProvider users = Objects.requireNonNull(session).users(); UserProvider users = Objects.requireNonNull(session).users();
UserModel userModel = users.getUserById(this.realm, id); UserModel userModel = users.getUserById(this.realm, id);
if (userModel == null) { if (userModel == null) {
if (auth.users().canQuery()) throw new NotFoundException("User not found"); if (auth.users().canQuery()) throw new NotFoundException("User not found");
else throw new ForbiddenException(); else throw new ForbiddenException();
} else {
if (this.auth.users().canManage() || !Profile.isFeatureEnabled(Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ)) {
this.auth.users().requireManage();
Stream<String> excludedRoleIds = userModel.getRoleMappingsStream().filter(RoleModel::isClientRole).map(RoleModel::getId);
return searchForClientRolesByExcludedIds(realm, search, first, max, excludedRoleIds);
} else {
this.auth.users().requireView(userModel);
Set<String> roleIds = getRoleIdsWithPermissions(MAP_ROLE_SCOPE, MAP_ROLES_SCOPE);
userModel.getRoleMappingsStream().forEach(role -> roleIds.remove(role.getId()));
return searchForClientRolesByIds(realm, roleIds.stream(), search, first, max);
}
} }
this.auth.users().requireView(userModel);
return this.mapping(((Predicate<RoleModel>) userModel::hasDirectRole).negate(), first, max, search);
} }
@GET @GET
@ -163,8 +199,8 @@ public class AvailableRoleMappingResource extends RoleMappingResource {
@Consumes({"application/json"}) @Consumes({"application/json"})
@Produces({"application/json"}) @Produces({"application/json"})
@Operation( @Operation(
summary = "List all composite client roles", summary = "List all available client roles to map as composite role",
description = "This endpoint returns all the client role" description = "This endpoint returns all available client roles to map as composite role"
) )
@APIResponse( @APIResponse(
responseCode = "200", responseCode = "200",
@ -176,8 +212,32 @@ public class AvailableRoleMappingResource extends RoleMappingResource {
) )
)} )}
) )
public final List<ClientRole> listCompositeRoleMappings(@QueryParam("first") @DefaultValue("0") long first, public final List<ClientRole> listAvailableRoleMappings(@PathParam("id") String id, @QueryParam("first") @DefaultValue("0") int first,
@QueryParam("max") @DefaultValue("10") long max, @QueryParam("search") @DefaultValue("") String search) { @QueryParam("max") @DefaultValue("10") int max, @QueryParam("search") @DefaultValue("") String search) {
return this.mapping(o -> true, first, max, search); if (this.auth.users().canManage() || !Profile.isFeatureEnabled(Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ)) {
this.auth.users().requireManage();
return searchForClientRolesByExcludedIds(realm, search, first, max, Stream.of(id));
} else {
Set<String> roleIds = getRoleIdsWithPermissions(MAP_ROLE_COMPOSITE_SCOPE, MAP_ROLES_COMPOSITE_SCOPE);
roleIds.remove(id);
return searchForClientRolesByIds(realm, roleIds.stream(), search, first, max);
}
}
private Set<String> getRoleIdsWithPermissions(String roleResourceScope, String clientResourceScope) {
Set<String> roleIds = this.auth.roles().getRolesWithPermission(roleResourceScope);
Set<String> clientIds = this.auth.clients().getClientsWithPermission(clientResourceScope);
clientIds.stream().flatMap(cid -> realm.getClientById(cid).getRolesStream()).forEach(role -> roleIds.add(role.getId()));
return roleIds;
}
private List<ClientRole> searchForClientRolesByIds(RealmModel realm, Stream<String> includedIDs, String search, int first, int max) {
Stream<RoleModel> result = session.roles().searchForClientRolesStream(realm, includedIDs, search, first, max);
return result.map(role -> RoleMapper.convertToModel(role, realm)).collect(Collectors.toList());
}
private List<ClientRole> searchForClientRolesByExcludedIds(RealmModel realm, String search, int first, int max, Stream<String> excludedIds) {
Stream<RoleModel> result = session.roles().searchForClientRolesStream(realm, search, excludedIds, first, max);
return result.map(role -> RoleMapper.convertToModel(role, realm)).collect(Collectors.toList());
} }
} }

View file

@ -1,7 +1,10 @@
package org.keycloak.admin.ui.rest; package org.keycloak.admin.ui.rest;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream;
import jakarta.ws.rs.Consumes; import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.ForbiddenException; import jakarta.ws.rs.ForbiddenException;
import jakarta.ws.rs.GET; import jakarta.ws.rs.GET;
@ -24,16 +27,11 @@ import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
public class EffectiveRoleMappingResource extends RoleMappingResource { import static org.keycloak.admin.ui.rest.model.RoleMapper.convertToModel;
private KeycloakSession session;
private RealmModel realm;
private AdminPermissionEvaluator auth;
public class EffectiveRoleMappingResource extends RoleMappingResource {
public EffectiveRoleMappingResource(KeycloakSession session, RealmModel realm, AdminPermissionEvaluator auth) { public EffectiveRoleMappingResource(KeycloakSession session, RealmModel realm, AdminPermissionEvaluator auth) {
super(realm, auth); super(session, realm, auth);
this.realm = realm;
this.auth = auth;
this.session = session;
} }
@GET @GET
@ -59,9 +57,10 @@ public class EffectiveRoleMappingResource extends RoleMappingResource {
if (clientScope == null) { if (clientScope == null) {
throw new NotFoundException("Could not find client scope"); throw new NotFoundException("Could not find client scope");
} }
this.auth.clients().requireView(clientScope); this.auth.clients().requireView(clientScope);
return this.mapping(clientScope::hasScope, auth.roles()::canMapClientScope).collect(Collectors.toList()); return toSortedClientRoles(
addSubClientRoles(clientScope.getScopeMappingsStream())
.filter(auth.roles()::canMapClientScope));
} }
@GET @GET
@ -89,7 +88,9 @@ public class EffectiveRoleMappingResource extends RoleMappingResource {
} }
auth.clients().requireView(client); auth.clients().requireView(client);
return mapping(client::hasScope).collect(Collectors.toList()); return toSortedClientRoles(
addSubClientRoles(client.getScopeMappingsStream())
.filter(auth.roles()::canMapRole));
} }
@GET @GET
@ -117,7 +118,9 @@ public class EffectiveRoleMappingResource extends RoleMappingResource {
} }
auth.groups().requireView(group); auth.groups().requireView(group);
return mapping(group::hasRole).collect(Collectors.toList()); return toSortedClientRoles(
addSubClientRoles(addParents(group).flatMap(GroupModel::getRoleMappingsStream))
.filter(auth.roles()::canMapRole));
} }
@GET @GET
@ -144,9 +147,14 @@ public class EffectiveRoleMappingResource extends RoleMappingResource {
if (auth.users().canQuery()) throw new NotFoundException("User not found"); if (auth.users().canQuery()) throw new NotFoundException("User not found");
else throw new ForbiddenException(); else throw new ForbiddenException();
} }
auth.users().requireView(user); auth.users().requireView(user);
return mapping(user::hasRole).collect(Collectors.toList()); return toSortedClientRoles(
addSubClientRoles(Stream.concat(
user.getRoleMappingsStream(),
user.getGroupsStream()
.flatMap(g -> addParents(g))
.flatMap(GroupModel::getRoleMappingsStream)))
.filter(auth.roles()::canMapRole));
} }
@GET @GET
@ -170,7 +178,36 @@ public class EffectiveRoleMappingResource extends RoleMappingResource {
public final List<ClientRole> listCompositeRealmRoleMappings() { public final List<ClientRole> listCompositeRealmRoleMappings() {
auth.roles().requireList(realm); auth.roles().requireList(realm);
final RoleModel defaultRole = this.realm.getDefaultRole(); final RoleModel defaultRole = this.realm.getDefaultRole();
return mapping(o -> o.hasRole(defaultRole)).collect(Collectors.toList()); //this definitely does not return what the descriptions says
return toSortedClientRoles(
addSubClientRoles(Stream.of(defaultRole))
.filter(auth.roles()::canMapRole));
} }
private Stream<RoleModel> addSubClientRoles(Stream<RoleModel> roles) {
return addSubRoles(roles).filter(RoleModel::isClientRole);
}
private List<ClientRole> toSortedClientRoles(Stream<RoleModel> roles) {
return roles.map(roleModel -> convertToModel(roleModel, realm))
.sorted(Comparator.comparing(ClientRole::getClient).thenComparing(ClientRole::getRole))
.collect(Collectors.toList());
}
private Stream<RoleModel> addSubRoles(Stream<RoleModel> roles) {
return addSubRoles(roles, new HashSet<>());
}
private Stream<RoleModel> addSubRoles(Stream<RoleModel> roles, HashSet<RoleModel> visited) {
List<RoleModel> roleList = roles.collect(Collectors.toList());
visited.addAll(roleList);
return Stream.concat(roleList.stream(), roleList.stream().flatMap(r -> addSubRoles(r.getCompositesStream().filter(s -> !visited.contains(s)), visited)));
}
private Stream<GroupModel> addParents(GroupModel group) {
//no cycle check here, I hope that's fine
if (group.getParent() == null) {
return Stream.of(group);
}
return Stream.concat(Stream.of(group), addParents(group.getParent()));
}
} }

View file

@ -1,43 +1,17 @@
package org.keycloak.admin.ui.rest; package org.keycloak.admin.ui.rest;
import static org.keycloak.admin.ui.rest.model.RoleMapper.convertToModel; import org.keycloak.models.KeycloakSession;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.keycloak.admin.ui.rest.model.ClientRole;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleContainerModel;
import org.keycloak.models.RoleModel;
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
public abstract class RoleMappingResource { public abstract class RoleMappingResource {
private final RealmModel realm; protected final KeycloakSession session;
private final AdminPermissionEvaluator auth; protected final RealmModel realm;
protected final AdminPermissionEvaluator auth;
public RoleMappingResource(RealmModel realm, AdminPermissionEvaluator auth) { public RoleMappingResource(KeycloakSession session, RealmModel realm, AdminPermissionEvaluator auth) {
this.session = session;
this.realm = realm; this.realm = realm;
this.auth = auth; this.auth = auth;
} }
protected final Stream<ClientRole> mapping(Predicate<RoleModel> predicate) {
return realm.getClientsStream().flatMap(RoleContainerModel::getRolesStream).filter(predicate)
.filter(auth.roles()::canMapRole).map(roleModel -> convertToModel(roleModel, realm.getClientsStream()));
}
protected final Stream<ClientRole> mapping(Predicate<RoleModel> predicate, Predicate<RoleModel> authPredicate) {
return realm.getClientsStream().flatMap(RoleContainerModel::getRolesStream).filter(predicate)
.filter(authPredicate).map(roleModel -> convertToModel(roleModel, realm.getClientsStream()));
}
protected final List<ClientRole> mapping(Predicate<RoleModel> predicate, long first, long max, final String search) {
return mapping(predicate).filter(clientRole -> clientRole.getClient().contains(search) || clientRole.getRole().contains(search))
.skip(first).limit(max).collect(Collectors.toList());
}
protected final List<ClientRole> mapping(Predicate<RoleModel> predicate, Predicate<RoleModel> authPredicate, long first, long max, final String search) {
return mapping(predicate, authPredicate).filter(clientRole -> clientRole.getClient().contains(search) || clientRole.getRole().contains(search))
.skip(first).limit(max).collect(Collectors.toList());
}
} }

View file

@ -1,15 +1,16 @@
package org.keycloak.admin.ui.rest.model; package org.keycloak.admin.ui.rest.model;
import java.util.stream.Stream;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel; import org.keycloak.models.RoleModel;
public class RoleMapper { public class RoleMapper {
public static ClientRole convertToModel(RoleModel roleModel, RealmModel realm) {
public static ClientRole convertToModel(RoleModel roleModel, Stream<ClientModel> clients) { ClientModel clientModel = realm.getClientById(roleModel.getContainerId());
if (clientModel==null) {
throw new IllegalArgumentException("Could not find referenced client");
}
ClientRole clientRole = new ClientRole(roleModel.getId(), roleModel.getName(), roleModel.getDescription()); ClientRole clientRole = new ClientRole(roleModel.getId(), roleModel.getName(), roleModel.getDescription());
ClientModel clientModel = clients.filter(c -> roleModel.getContainerId().equals(c.getId())).findFirst()
.orElseThrow(() -> new IllegalArgumentException("Could not find referenced client"));
clientRole.setClientId(clientModel.getId()); clientRole.setClientId(clientModel.getId());
clientRole.setClient(clientModel.getClientId()); clientRole.setClient(clientModel.getClientId());
return clientRole; return clientRole;

View file

@ -71,4 +71,29 @@ public interface RoleLookupProvider {
* Never returns {@code null}. * Never returns {@code null}.
*/ */
Stream<RoleModel> searchForClientRolesStream(ClientModel client, String search, Integer first, Integer max); Stream<RoleModel> searchForClientRolesStream(ClientModel client, String search, Integer first, Integer max);
/**
* Case-insensitive search for client roles that contain the given string in its name or their client's public identifier (clientId - ({@code client_id} in OIDC or {@code entityID} in SAML)).
* @param realm Realm.
* @param ids Stream of ids to include in search. Ignored when {@code null}. Returns empty {@code Stream} when empty.
* @param search String to search by role's name or client's public identifier.
* @param first First result to return. Ignored if negative or {@code null}.
* @param max Maximum number of results to return. Ignored if negative or {@code null}.
* @return Stream of the client roles where role name or client public identifier contains given search string.
* Never returns {@code null}.
*/
Stream<RoleModel> searchForClientRolesStream(RealmModel realm, Stream<String> ids, String search, Integer first, Integer max);
/**
* Case-insensitive search for client roles that contain the given string in their name or their client's public identifier (clientId - ({@code client_id} in OIDC or {@code entityID} in SAML)).
*
* @param realm Realm.
* @param search String to search by role's name or client's public identifier.
* @param excludedIds Stream of ids to exclude. Ignored if empty or {@code null}.
* @param first First result to return. Ignored if negative or {@code null}.
* @param max Maximum number of results to return. Ignored if negative or {@code null}.
* @return Stream of the client roles where role name or client's public identifier contains given search string.
* Never returns {@code null}.
*/
Stream<RoleModel> searchForClientRolesStream(RealmModel realm, String search, Stream<String> excludedIds, Integer first, Integer max);
} }

View file

@ -20,6 +20,7 @@ import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientScopeModel; import org.keycloak.models.ClientScopeModel;
import java.util.Map; import java.util.Map;
import java.util.Set;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -81,4 +82,6 @@ public interface ClientPermissionEvaluator {
boolean canMapClientScopeRoles(ClientModel client); boolean canMapClientScopeRoles(ClientModel client);
Map<String, Boolean> getAccess(ClientModel client); Map<String, Boolean> getAccess(ClientModel client);
Set<String> getClientsWithPermission(String scope);
} }

View file

@ -24,17 +24,21 @@ import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.model.Resource; import org.keycloak.authorization.model.Resource;
import org.keycloak.authorization.model.ResourceServer; import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.authorization.model.Scope; import org.keycloak.authorization.model.Scope;
import org.keycloak.authorization.permission.ResourcePermission;
import org.keycloak.authorization.policy.evaluation.EvaluationContext; import org.keycloak.authorization.policy.evaluation.EvaluationContext;
import org.keycloak.authorization.store.ResourceStore;
import org.keycloak.models.AdminRoles; import org.keycloak.models.AdminRoles;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientScopeModel; import org.keycloak.models.ClientScopeModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.representations.idm.authorization.Permission;
import org.keycloak.services.ForbiddenException; import org.keycloak.services.ForbiddenException;
import org.keycloak.storage.StorageId; import org.keycloak.storage.StorageId;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
@ -56,12 +60,20 @@ class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionM
protected final RealmModel realm; protected final RealmModel realm;
protected final AuthorizationProvider authz; protected final AuthorizationProvider authz;
protected final MgmtPermissions root; protected final MgmtPermissions root;
protected final ResourceStore resourceStore;
private static final String RESOURCE_NAME_PREFIX = "client.resource.";
public ClientPermissions(KeycloakSession session, RealmModel realm, AuthorizationProvider authz, MgmtPermissions root) { public ClientPermissions(KeycloakSession session, RealmModel realm, AuthorizationProvider authz, MgmtPermissions root) {
this.session = session; this.session = session;
this.realm = realm; this.realm = realm;
this.authz = authz; this.authz = authz;
this.root = root; this.root = root;
if (authz != null) {
resourceStore = authz.getStoreFactory().getResourceStore();
} else {
resourceStore = null;
}
} }
private String getResourceName(ClientModel client) { private String getResourceName(ClientModel client) {
@ -644,5 +656,41 @@ class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionM
return map; return map;
} }
@Override
public Set<String> getClientsWithPermission(String scope) {
if (!root.isAdminSameRealm()) {
return Collections.emptySet();
}
ResourceServer server = root.realmResourceServer();
if (server == null) {
return Collections.emptySet();
}
Set<String> granted = new HashSet<>();
resourceStore.findByType(server, "Client", resource -> {
if (hasPermission(resource, scope)) {
granted.add(resource.getName().substring(RESOURCE_NAME_PREFIX.length()));
}
});
return granted;
}
private boolean hasPermission(Resource resource, String scope) {
ResourceServer server = root.realmResourceServer();
Collection<Permission> permissions = root.evaluatePermission(new ResourcePermission(resource, resource.getScopes(), server), server);
for (Permission permission : permissions) {
for (String s : permission.getScopes()) {
if (scope.equals(s)) {
return true;
}
}
}
return false;
}
} }

View file

@ -19,6 +19,8 @@ package org.keycloak.services.resources.admin.permissions;
import org.keycloak.models.RoleContainerModel; import org.keycloak.models.RoleContainerModel;
import org.keycloak.models.RoleModel; import org.keycloak.models.RoleModel;
import java.util.Set;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $ * @version $Revision: 1 $
@ -53,4 +55,5 @@ public interface RolePermissionEvaluator {
void requireView(RoleContainerModel container); void requireView(RoleContainerModel container);
Set<String> getRolesWithPermission(String scope);
} }

View file

@ -23,6 +23,7 @@ import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.model.Resource; import org.keycloak.authorization.model.Resource;
import org.keycloak.authorization.model.ResourceServer; import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.authorization.model.Scope; import org.keycloak.authorization.model.Scope;
import org.keycloak.authorization.permission.ResourcePermission;
import org.keycloak.authorization.store.ResourceStore; import org.keycloak.authorization.store.ResourceStore;
import org.keycloak.models.AdminRoles; import org.keycloak.models.AdminRoles;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
@ -32,8 +33,11 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleContainerModel; import org.keycloak.models.RoleContainerModel;
import org.keycloak.models.RoleModel; import org.keycloak.models.RoleModel;
import org.keycloak.representations.idm.authorization.DecisionStrategy; import org.keycloak.representations.idm.authorization.DecisionStrategy;
import org.keycloak.representations.idm.authorization.Permission;
import org.keycloak.services.ForbiddenException; import org.keycloak.services.ForbiddenException;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
@ -49,12 +53,19 @@ class RolePermissions implements RolePermissionEvaluator, RolePermissionManageme
protected final RealmModel realm; protected final RealmModel realm;
protected final AuthorizationProvider authz; protected final AuthorizationProvider authz;
protected final MgmtPermissions root; protected final MgmtPermissions root;
private final ResourceStore resourceStore;
private static final String RESOURCE_NAME_PREFIX = "role.resource.";
public RolePermissions(KeycloakSession session, RealmModel realm, AuthorizationProvider authz, MgmtPermissions root) { public RolePermissions(KeycloakSession session, RealmModel realm, AuthorizationProvider authz, MgmtPermissions root) {
this.session = session; this.session = session;
this.realm = realm; this.realm = realm;
this.authz = authz; this.authz = authz;
this.root = root; this.root = root;
if (authz != null) {
resourceStore = authz.getStoreFactory().getResourceStore();
} else {
resourceStore = null;
}
} }
@Override @Override
@ -529,6 +540,43 @@ class RolePermissions implements RolePermissionEvaluator, RolePermissionManageme
return Helper.createRolePolicy(authz, server, role, policyName); return Helper.createRolePolicy(authz, server, role, policyName);
} }
@Override
public Set<String> getRolesWithPermission(String scope) {
if (!root.isAdminSameRealm()) {
return Collections.emptySet();
}
ResourceServer server = root.realmResourceServer();
if (server == null) {
return Collections.emptySet();
}
Set<String> granted = new HashSet<>();
resourceStore.findByType(server, "Role", resource -> {
if (hasPermission(resource, scope)) {
granted.add(resource.getName().substring(RESOURCE_NAME_PREFIX.length()));
}
});
return granted;
}
private boolean hasPermission(Resource resource, String scope) {
ResourceServer server = root.realmResourceServer();
Collection<Permission> permissions = root.evaluatePermission(new ResourcePermission(resource, resource.getScopes(), server), server);
for (Permission permission : permissions) {
for (String s : permission.getScopes()) {
if (scope.equals(s)) {
return true;
}
}
}
return false;
}
private Scope mapRoleScope(ResourceServer server) { private Scope mapRoleScope(ResourceServer server) {
return authz.getStoreFactory().getScopeStore().findByName(server, MAP_ROLE_SCOPE); return authz.getStoreFactory().getScopeStore().findByName(server, MAP_ROLE_SCOPE);
} }
@ -607,6 +655,4 @@ class RolePermissions implements RolePermissionEvaluator, RolePermissionManageme
private static String getRoleResourceName(RoleModel role) { private static String getRoleResourceName(RoleModel role) {
return "role.resource." + role.getId(); return "role.resource." + role.getId();
} }
} }

View file

@ -81,6 +81,16 @@ public class HardcodedRoleStorageProvider implements RoleStorageProvider {
throw new UnsupportedOperationException("Not supported yet."); throw new UnsupportedOperationException("Not supported yet.");
} }
@Override
public Stream<RoleModel> searchForClientRolesStream(RealmModel realm, String search, Stream<String> excludedIds, Integer first, Integer max) {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public Stream<RoleModel> searchForClientRolesStream(RealmModel realm, Stream<String> ids, String search, Integer first, Integer max) {
throw new UnsupportedOperationException("Not supported yet.");
}
public class HardcodedRoleAdapter implements RoleModel { public class HardcodedRoleAdapter implements RoleModel {
private final RealmModel realm; private final RealmModel realm;