diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java index c2787139231d..fb8161ea4ad4 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java @@ -750,6 +750,16 @@ public Stream searchForClientRolesStream(ClientModel client, String s return getRoleDelegate().searchForClientRolesStream(client, search, first, max); } + @Override + public Stream searchForClientRolesStream(RealmModel realm, Stream ids, String search, Integer first, Integer max) { + return getRoleDelegate().searchForClientRolesStream(realm, ids, search, first, max); + } + + @Override + public Stream searchForClientRolesStream(RealmModel realm, String search, Stream excludedIds, Integer first, Integer max) { + return getRoleDelegate().searchForClientRolesStream(realm, search, excludedIds, first, max); + } + @Override public Stream searchForRolesStream(RealmModel realm, String search, Integer first, Integer max) { return getRoleDelegate().searchForRolesStream(realm, search, first, max); diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java index 09a57ffd3fd5..b4e90d8ed5ad 100644 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java @@ -327,6 +327,53 @@ public Stream getRolesStream(RealmModel realm, Stream ids, St .map(g -> session.roles().getRoleById(realm, g)); } + @Override + public Stream searchForClientRolesStream(RealmModel realm, Stream ids, String search, Integer first, Integer max) { + return searchForClientRolesStream(realm, ids, search, first, max, false); + } + @Override + public Stream searchForClientRolesStream(RealmModel realm, String search, Stream excludedIds, Integer first, Integer max) { + return searchForClientRolesStream(realm, excludedIds, search, first, max, true); + } + + private Stream searchForClientRolesStream(RealmModel realm, Stream ids, String search, Integer first, Integer max, boolean negateIds) { + List idList = null; + if(ids != null) { + idList = ids.collect(Collectors.toList()); + if(idList.isEmpty() && !negateIds) + return Stream.empty(); + } + CriteriaBuilder cb = em.getCriteriaBuilder(); + CriteriaQuery query = cb.createQuery(RoleEntity.class); + + Root roleRoot = query.from(RoleEntity.class); + Root clientRoot = query.from(ClientEntity.class); + + List 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 public Stream getClientRolesStream(ClientModel client, Integer first, Integer max) { TypedQuery query = em.createNamedQuery("getClientRoles", RoleEntity.class); diff --git a/model/legacy-private/src/main/java/org/keycloak/storage/RoleStorageManager.java b/model/legacy-private/src/main/java/org/keycloak/storage/RoleStorageManager.java index c53299ceb999..6720a16e26aa 100644 --- a/model/legacy-private/src/main/java/org/keycloak/storage/RoleStorageManager.java +++ b/model/legacy-private/src/main/java/org/keycloak/storage/RoleStorageManager.java @@ -244,6 +244,28 @@ public Stream searchForClientRolesStream(ClientModel client, String s return Stream.concat(local, ext); } + @Override + public Stream searchForClientRolesStream(RealmModel realm, Stream ids, String search, Integer first, Integer max) { + Stream local = localStorage().searchForClientRolesStream(realm, ids, search, first, max); + Stream 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 searchForClientRolesStream(RealmModel realm, String search, Stream excludedIds, Integer first, Integer max) { + Stream local = localStorage().searchForClientRolesStream(realm, search, excludedIds, first, max); + Stream 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 public void close() { } diff --git a/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/AuthenticationManagementResource.java b/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/AuthenticationManagementResource.java index 234ec652c98e..f90becab756f 100644 --- a/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/AuthenticationManagementResource.java +++ b/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/AuthenticationManagementResource.java @@ -28,16 +28,8 @@ public class AuthenticationManagementResource extends RoleMappingResource { - private final KeycloakSession session; - - private RealmModel realm; - private AdminPermissionEvaluator auth; - public AuthenticationManagementResource(KeycloakSession session, RealmModel realm, AdminPermissionEvaluator auth) { - super(realm, auth); - this.realm = realm; - this.auth = auth; - this.session = session; + super(session, realm, auth); } @GET diff --git a/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/AvailableRoleMappingResource.java b/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/AvailableRoleMappingResource.java index 7ff0bc0fb790..81264d67d0fb 100644 --- a/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/AvailableRoleMappingResource.java +++ b/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/AvailableRoleMappingResource.java @@ -2,7 +2,9 @@ import java.util.List; 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.DefaultValue; import jakarta.ws.rs.ForbiddenException; @@ -18,6 +20,8 @@ import org.eclipse.microprofile.openapi.annotations.media.Schema; import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; 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.ClientScopeModel; import org.keycloak.models.GroupModel; @@ -28,16 +32,16 @@ import org.keycloak.models.UserProvider; import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; -public class AvailableRoleMappingResource extends RoleMappingResource { - private final KeycloakSession session; - private final RealmModel realm; - private final AdminPermissionEvaluator auth; +import static org.keycloak.services.resources.admin.permissions.ClientPermissionManagement.MAP_ROLES_CLIENT_SCOPE; +import static org.keycloak.services.resources.admin.permissions.ClientPermissionManagement.MAP_ROLES_COMPOSITE_SCOPE; +import static org.keycloak.services.resources.admin.permissions.ClientPermissionManagement.MAP_ROLES_SCOPE; +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) { - super(realm, auth); - this.realm = realm; - this.auth = auth; - this.session = session; + super(session, realm, auth); } @GET @@ -45,8 +49,8 @@ public AvailableRoleMappingResource(KeycloakSession session, RealmModel realm, A @Consumes({"application/json"}) @Produces({"application/json"}) @Operation( - summary = "List all composite client roles for this client scope", - description = "This endpoint returns all the client role mapping for a specific client scope" + summary = "List all available client roles for this client scope", + description = "This endpoint returns all the client roles the user can add to a specific client scope" ) @APIResponse( responseCode = "200", @@ -58,14 +62,22 @@ public AvailableRoleMappingResource(KeycloakSession session, RealmModel realm, A ) )} ) - public final List listCompositeClientScopeRoleMappings(@PathParam("id") String id, @QueryParam("first") - @DefaultValue("0") long first, @QueryParam("max") @DefaultValue("10") long max, @QueryParam("search") @DefaultValue("") String search) { + public final List listAvailableClientScopeRoleMappings(@PathParam("id") String id, @QueryParam("first") + @DefaultValue("0") int first, @QueryParam("max") @DefaultValue("10") int max, @QueryParam("search") @DefaultValue("") String search) { ClientScopeModel scopeModel = this.realm.getClientScopeById(id); if (scopeModel == null) { throw new NotFoundException("Could not find client scope"); } else { - this.auth.clients().requireView(scopeModel); - return this.mapping(((Predicate) scopeModel::hasDirectScope).negate(), auth.roles()::canMapClientScope, first, max, search); + if(this.auth.clients().canManage() || !Profile.isFeatureEnabled(Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ)) { + this.auth.clients().requireManage(); + Stream excludedRoleIds = scopeModel.getScopeMappingsStream().filter(RoleModel::isClientRole).map(RoleModel::getId); + return searchForClientRolesByExcludedIds(realm, search, first, max, excludedRoleIds); + } else { + this.auth.clients().requireView(scopeModel); + Set 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 final List listCompositeClientScopeRoleMappings(@PathParam("i @Consumes({"application/json"}) @Produces({"application/json"}) @Operation( - summary = "List all composite client roles for this client", - description = "This endpoint returns all the client role mapping for a specific client" + summary = "List all available client roles for the scope mapping of this client", + description = "This endpoint returns all the client roles a user can add to the scope mapping of a specific client" ) @APIResponse( responseCode = "200", @@ -87,14 +99,22 @@ public final List listCompositeClientScopeRoleMappings(@PathParam("i ) )} ) - public final List listCompositeClientRoleMappings(@PathParam("id") String id, @QueryParam("first") - @DefaultValue("0") long first, @QueryParam("max") @DefaultValue("10") long max, @QueryParam("search") @DefaultValue("") String search) { + public final List listAvailableClientRoleMappings(@PathParam("id") String id, @QueryParam("first") + @DefaultValue("0") int first, @QueryParam("max") @DefaultValue("10") int max, @QueryParam("search") @DefaultValue("") String search) { ClientModel client = this.realm.getClientById(id); if (client == null) { throw new NotFoundException("Could not find client"); } else { - this.auth.clients().requireView(client); - return this.mapping(((Predicate) client::hasDirectScope).negate(), first, max, search); + if(this.auth.clients().canManage() || !Profile.isFeatureEnabled(Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ)) { + this.auth.clients().requireManage(); + Stream 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 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 final List listCompositeClientRoleMappings(@PathParam("id") @Consumes({"application/json"}) @Produces({"application/json"}) @Operation( - summary = "List all composite client roles for this group", - description = "This endpoint returns all the client role mapping for a specific group" + summary = "List all available client roles for this group", + description = "This endpoint returns all available client roles a user can add to a specific group" ) @APIResponse( responseCode = "200", @@ -116,14 +136,22 @@ public final List listCompositeClientRoleMappings(@PathParam("id") ) )} ) - public final List listCompositeGroupRoleMappings(@PathParam("id") String id, @QueryParam("first") - @DefaultValue("0") long first, @QueryParam("max") @DefaultValue("10") long max, @QueryParam("search") @DefaultValue("") String search) { + public final List listAvailableGroupRoleMappings(@PathParam("id") String id, @QueryParam("first") + @DefaultValue("0") int first, @QueryParam("max") @DefaultValue("10") int max, @QueryParam("search") @DefaultValue("") String search) { GroupModel group = this.realm.getGroupById(id); if (group == null) { throw new NotFoundException("Could not find group"); } else { - this.auth.groups().requireView(group); - return this.mapping(((Predicate) group::hasDirectRole).negate(), first, max, search); + if(this.auth.users().canManage() || !Profile.isFeatureEnabled(Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ)) { + this.auth.users().requireManage(); + Stream excludedRoleIds = group.getRoleMappingsStream().filter(RoleModel::isClientRole).map(RoleModel::getId); + return searchForClientRolesByExcludedIds(realm, search, first, max, excludedRoleIds); + } else { + this.auth.groups().requireView(group); + Set 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 final List listCompositeGroupRoleMappings(@PathParam("id") S @Consumes({"application/json"}) @Produces({"application/json"}) @Operation( - summary = "List all composite client roles for this user", - description = "This endpoint returns all the client role mapping for a specific user" + summary = "List all available client roles for this user", + description = "This endpoint returns all the available client roles a user can add to a specific user" ) @APIResponse( responseCode = "200", @@ -145,17 +173,25 @@ public final List listCompositeGroupRoleMappings(@PathParam("id") S ) )} ) - public final List listCompositeUserRoleMappings(@PathParam("id") String id, @QueryParam("first") @DefaultValue("0") long first, - @QueryParam("max") @DefaultValue("10") long max, @QueryParam("search") @DefaultValue("") String search) { + public final List listAvailableUserRoleMappings(@PathParam("id") String id, @QueryParam("first") @DefaultValue("0") int first, + @QueryParam("max") @DefaultValue("10") int max, @QueryParam("search") @DefaultValue("") String search) { UserProvider users = Objects.requireNonNull(session).users(); UserModel userModel = users.getUserById(this.realm, id); if (userModel == null) { if (auth.users().canQuery()) throw new NotFoundException("User not found"); else throw new ForbiddenException(); + } else { + if (this.auth.users().canManage() || !Profile.isFeatureEnabled(Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ)) { + this.auth.users().requireManage(); + Stream excludedRoleIds = userModel.getRoleMappingsStream().filter(RoleModel::isClientRole).map(RoleModel::getId); + return searchForClientRolesByExcludedIds(realm, search, first, max, excludedRoleIds); + } else { + this.auth.users().requireView(userModel); + Set 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) userModel::hasDirectRole).negate(), first, max, search); } @GET @@ -163,8 +199,8 @@ public final List listCompositeUserRoleMappings(@PathParam("id") Str @Consumes({"application/json"}) @Produces({"application/json"}) @Operation( - summary = "List all composite client roles", - description = "This endpoint returns all the client role" + summary = "List all available client roles to map as composite role", + description = "This endpoint returns all available client roles to map as composite role" ) @APIResponse( responseCode = "200", @@ -176,8 +212,32 @@ public final List listCompositeUserRoleMappings(@PathParam("id") Str ) )} ) - public final List listCompositeRoleMappings(@QueryParam("first") @DefaultValue("0") long first, - @QueryParam("max") @DefaultValue("10") long max, @QueryParam("search") @DefaultValue("") String search) { - return this.mapping(o -> true, first, max, search); + public final List listAvailableRoleMappings(@PathParam("id") String id, @QueryParam("first") @DefaultValue("0") int first, + @QueryParam("max") @DefaultValue("10") int max, @QueryParam("search") @DefaultValue("") String 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 roleIds = getRoleIdsWithPermissions(MAP_ROLE_COMPOSITE_SCOPE, MAP_ROLES_COMPOSITE_SCOPE); + roleIds.remove(id); + return searchForClientRolesByIds(realm, roleIds.stream(), search, first, max); + } + } + + private Set getRoleIdsWithPermissions(String roleResourceScope, String clientResourceScope) { + Set roleIds = this.auth.roles().getRolesWithPermission(roleResourceScope); + Set clientIds = this.auth.clients().getClientsWithPermission(clientResourceScope); + clientIds.stream().flatMap(cid -> realm.getClientById(cid).getRolesStream()).forEach(role -> roleIds.add(role.getId())); + return roleIds; + } + + private List searchForClientRolesByIds(RealmModel realm, Stream includedIDs, String search, int first, int max) { + Stream result = session.roles().searchForClientRolesStream(realm, includedIDs, search, first, max); + return result.map(role -> RoleMapper.convertToModel(role, realm)).collect(Collectors.toList()); + } + + private List searchForClientRolesByExcludedIds(RealmModel realm, String search, int first, int max, Stream excludedIds) { + Stream result = session.roles().searchForClientRolesStream(realm, search, excludedIds, first, max); + return result.map(role -> RoleMapper.convertToModel(role, realm)).collect(Collectors.toList()); } } diff --git a/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/EffectiveRoleMappingResource.java b/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/EffectiveRoleMappingResource.java index e7a96381e73b..03d66cff38aa 100644 --- a/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/EffectiveRoleMappingResource.java +++ b/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/EffectiveRoleMappingResource.java @@ -1,7 +1,10 @@ package org.keycloak.admin.ui.rest; +import java.util.Comparator; +import java.util.HashSet; import java.util.List; import java.util.stream.Collectors; +import java.util.stream.Stream; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.ForbiddenException; import jakarta.ws.rs.GET; @@ -24,16 +27,11 @@ import org.keycloak.models.UserModel; import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; -public class EffectiveRoleMappingResource extends RoleMappingResource { - private KeycloakSession session; - private RealmModel realm; - private AdminPermissionEvaluator auth; +import static org.keycloak.admin.ui.rest.model.RoleMapper.convertToModel; +public class EffectiveRoleMappingResource extends RoleMappingResource { public EffectiveRoleMappingResource(KeycloakSession session, RealmModel realm, AdminPermissionEvaluator auth) { - super(realm, auth); - this.realm = realm; - this.auth = auth; - this.session = session; + super(session, realm, auth); } @GET @@ -59,9 +57,10 @@ public final List listCompositeClientScopeRoleMappings(@PathParam("i if (clientScope == null) { throw new NotFoundException("Could not find client scope"); } - 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 @@ -89,7 +88,9 @@ public final List listCompositeClientsRoleMappings(@PathParam("id") } auth.clients().requireView(client); - return mapping(client::hasScope).collect(Collectors.toList()); + return toSortedClientRoles( + addSubClientRoles(client.getScopeMappingsStream()) + .filter(auth.roles()::canMapRole)); } @GET @@ -117,7 +118,9 @@ public final List listCompositeGroupsRoleMappings(@PathParam("id") S } auth.groups().requireView(group); - return mapping(group::hasRole).collect(Collectors.toList()); + return toSortedClientRoles( + addSubClientRoles(addParents(group).flatMap(GroupModel::getRoleMappingsStream)) + .filter(auth.roles()::canMapRole)); } @GET @@ -144,9 +147,14 @@ public final List listCompositeUsersRoleMappings(@PathParam("id") St if (auth.users().canQuery()) throw new NotFoundException("User not found"); else throw new ForbiddenException(); } - 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 @@ -170,7 +178,36 @@ public final List listCompositeUsersRoleMappings(@PathParam("id") St public final List listCompositeRealmRoleMappings() { auth.roles().requireList(realm); 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 addSubClientRoles(Stream roles) { + return addSubRoles(roles).filter(RoleModel::isClientRole); + } + + private List toSortedClientRoles(Stream roles) { + return roles.map(roleModel -> convertToModel(roleModel, realm)) + .sorted(Comparator.comparing(ClientRole::getClient).thenComparing(ClientRole::getRole)) + .collect(Collectors.toList()); + } + + private Stream addSubRoles(Stream roles) { + return addSubRoles(roles, new HashSet<>()); + } + private Stream addSubRoles(Stream roles, HashSet visited) { + List 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 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())); + } } diff --git a/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/RoleMappingResource.java b/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/RoleMappingResource.java index d157a6c7cb00..138a051efb8b 100644 --- a/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/RoleMappingResource.java +++ b/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/RoleMappingResource.java @@ -1,43 +1,17 @@ package org.keycloak.admin.ui.rest; -import static org.keycloak.admin.ui.rest.model.RoleMapper.convertToModel; - -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.KeycloakSession; import org.keycloak.models.RealmModel; -import org.keycloak.models.RoleContainerModel; -import org.keycloak.models.RoleModel; import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; public abstract class RoleMappingResource { - private final RealmModel realm; - private final AdminPermissionEvaluator auth; + protected final KeycloakSession session; + 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.auth = auth; } - - protected final Stream mapping(Predicate predicate) { - return realm.getClientsStream().flatMap(RoleContainerModel::getRolesStream).filter(predicate) - .filter(auth.roles()::canMapRole).map(roleModel -> convertToModel(roleModel, realm.getClientsStream())); - } - - protected final Stream mapping(Predicate predicate, Predicate authPredicate) { - return realm.getClientsStream().flatMap(RoleContainerModel::getRolesStream).filter(predicate) - .filter(authPredicate).map(roleModel -> convertToModel(roleModel, realm.getClientsStream())); - } - - protected final List mapping(Predicate 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 mapping(Predicate predicate, Predicate 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()); - } } diff --git a/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/model/RoleMapper.java b/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/model/RoleMapper.java index e72adf346010..bfe7050120cc 100644 --- a/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/model/RoleMapper.java +++ b/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/model/RoleMapper.java @@ -1,15 +1,16 @@ package org.keycloak.admin.ui.rest.model; -import java.util.stream.Stream; import org.keycloak.models.ClientModel; +import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; public class RoleMapper { - - public static ClientRole convertToModel(RoleModel roleModel, Stream clients) { + public static ClientRole convertToModel(RoleModel roleModel, RealmModel realm) { + 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()); - ClientModel clientModel = clients.filter(c -> roleModel.getContainerId().equals(c.getId())).findFirst() - .orElseThrow(() -> new IllegalArgumentException("Could not find referenced client")); clientRole.setClientId(clientModel.getId()); clientRole.setClient(clientModel.getClientId()); return clientRole; diff --git a/server-spi/src/main/java/org/keycloak/storage/role/RoleLookupProvider.java b/server-spi/src/main/java/org/keycloak/storage/role/RoleLookupProvider.java index 400d1e03b3a4..c65a295e0ac5 100644 --- a/server-spi/src/main/java/org/keycloak/storage/role/RoleLookupProvider.java +++ b/server-spi/src/main/java/org/keycloak/storage/role/RoleLookupProvider.java @@ -71,4 +71,29 @@ public interface RoleLookupProvider { * Never returns {@code null}. */ Stream 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 searchForClientRolesStream(RealmModel realm, Stream 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 searchForClientRolesStream(RealmModel realm, String search, Stream excludedIds, Integer first, Integer max); } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissionEvaluator.java b/services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissionEvaluator.java index 100c06d46da8..ad3f94f90003 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissionEvaluator.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissionEvaluator.java @@ -20,6 +20,7 @@ import org.keycloak.models.ClientScopeModel; import java.util.Map; +import java.util.Set; /** * @author Bill Burke @@ -81,4 +82,6 @@ public interface ClientPermissionEvaluator { boolean canMapClientScopeRoles(ClientModel client); Map getAccess(ClientModel client); + + Set getClientsWithPermission(String scope); } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissions.java b/services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissions.java index ce39376ee1fd..12dd0b647e4a 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissions.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissions.java @@ -24,17 +24,21 @@ import org.keycloak.authorization.model.Resource; import org.keycloak.authorization.model.ResourceServer; import org.keycloak.authorization.model.Scope; +import org.keycloak.authorization.permission.ResourcePermission; import org.keycloak.authorization.policy.evaluation.EvaluationContext; +import org.keycloak.authorization.store.ResourceStore; import org.keycloak.models.AdminRoles; import org.keycloak.models.ClientModel; import org.keycloak.models.ClientScopeModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; +import org.keycloak.representations.idm.authorization.Permission; import org.keycloak.services.ForbiddenException; import org.keycloak.storage.StorageId; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; @@ -56,12 +60,20 @@ class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionM protected final RealmModel realm; protected final AuthorizationProvider authz; 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) { this.session = session; this.realm = realm; this.authz = authz; this.root = root; + if (authz != null) { + resourceStore = authz.getStoreFactory().getResourceStore(); + } else { + resourceStore = null; + } } private String getResourceName(ClientModel client) { @@ -644,5 +656,41 @@ public Map getAccess(ClientModel client) { return map; } + @Override + public Set getClientsWithPermission(String scope) { + if (!root.isAdminSameRealm()) { + return Collections.emptySet(); + } + + ResourceServer server = root.realmResourceServer(); + + if (server == null) { + return Collections.emptySet(); + } + + Set 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 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; + } } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/permissions/RolePermissionEvaluator.java b/services/src/main/java/org/keycloak/services/resources/admin/permissions/RolePermissionEvaluator.java index a4a8b71c1948..501d163595d6 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/permissions/RolePermissionEvaluator.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/permissions/RolePermissionEvaluator.java @@ -19,6 +19,8 @@ import org.keycloak.models.RoleContainerModel; import org.keycloak.models.RoleModel; +import java.util.Set; + /** * @author Bill Burke * @version $Revision: 1 $ @@ -53,4 +55,5 @@ public interface RolePermissionEvaluator { void requireView(RoleContainerModel container); + Set getRolesWithPermission(String scope); } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/permissions/RolePermissions.java b/services/src/main/java/org/keycloak/services/resources/admin/permissions/RolePermissions.java index 9252541c9533..d9850c26033d 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/permissions/RolePermissions.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/permissions/RolePermissions.java @@ -23,6 +23,7 @@ import org.keycloak.authorization.model.Resource; import org.keycloak.authorization.model.ResourceServer; import org.keycloak.authorization.model.Scope; +import org.keycloak.authorization.permission.ResourcePermission; import org.keycloak.authorization.store.ResourceStore; import org.keycloak.models.AdminRoles; import org.keycloak.models.ClientModel; @@ -32,8 +33,11 @@ import org.keycloak.models.RoleContainerModel; import org.keycloak.models.RoleModel; import org.keycloak.representations.idm.authorization.DecisionStrategy; +import org.keycloak.representations.idm.authorization.Permission; import org.keycloak.services.ForbiddenException; +import java.util.Collection; +import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.Map; @@ -49,12 +53,19 @@ class RolePermissions implements RolePermissionEvaluator, RolePermissionManageme protected final RealmModel realm; protected final AuthorizationProvider authz; 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) { this.session = session; this.realm = realm; this.authz = authz; this.root = root; + if (authz != null) { + resourceStore = authz.getStoreFactory().getResourceStore(); + } else { + resourceStore = null; + } } @Override @@ -529,6 +540,43 @@ public Policy rolePolicy(ResourceServer server, RoleModel role) { return Helper.createRolePolicy(authz, server, role, policyName); } + @Override + public Set getRolesWithPermission(String scope) { + if (!root.isAdminSameRealm()) { + return Collections.emptySet(); + } + + ResourceServer server = root.realmResourceServer(); + + if (server == null) { + return Collections.emptySet(); + } + + Set 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 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) { return authz.getStoreFactory().getScopeStore().findByName(server, MAP_ROLE_SCOPE); } @@ -607,6 +655,4 @@ private String getMapCompositePermissionName(RoleModel role) { private static String getRoleResourceName(RoleModel role) { return "role.resource." + role.getId(); } - - } diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/HardcodedRoleStorageProvider.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/HardcodedRoleStorageProvider.java index 928f6f497539..93b3cf5d7fd4 100644 --- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/HardcodedRoleStorageProvider.java +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/HardcodedRoleStorageProvider.java @@ -81,6 +81,16 @@ public Stream searchForClientRolesStream(ClientModel client, String s throw new UnsupportedOperationException("Not supported yet."); } + @Override + public Stream searchForClientRolesStream(RealmModel realm, String search, Stream excludedIds, Integer first, Integer max) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public Stream searchForClientRolesStream(RealmModel realm, Stream ids, String search, Integer first, Integer max) { + throw new UnsupportedOperationException("Not supported yet."); + } + public class HardcodedRoleAdapter implements RoleModel { private final RealmModel realm;