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:
parent
203eb3421a
commit
030f42ec83
14 changed files with 380 additions and 102 deletions
|
@ -750,6 +750,16 @@ public class RealmCacheSession implements CacheRealmProvider {
|
|||
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
|
||||
public Stream<RoleModel> searchForRolesStream(RealmModel realm, String search, Integer first, Integer max) {
|
||||
return getRoleDelegate().searchForRolesStream(realm, search, first, max);
|
||||
|
|
|
@ -327,6 +327,53 @@ public class JpaRealmProvider implements RealmProvider, ClientProvider, ClientSc
|
|||
.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
|
||||
public Stream<RoleModel> getClientRolesStream(ClientModel client, Integer first, Integer max) {
|
||||
TypedQuery<RoleEntity> query = em.createNamedQuery("getClientRoles", RoleEntity.class);
|
||||
|
|
|
@ -244,6 +244,28 @@ public class RoleStorageManager implements RoleProvider {
|
|||
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
|
||||
public void close() {
|
||||
}
|
||||
|
|
|
@ -28,16 +28,8 @@ import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluato
|
|||
|
||||
|
||||
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
|
||||
|
|
|
@ -2,7 +2,9 @@ package org.keycloak.admin.ui.rest;
|
|||
|
||||
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.Content;
|
|||
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.UserModel;
|
|||
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 class AvailableRoleMappingResource extends RoleMappingResource {
|
|||
@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 class AvailableRoleMappingResource extends RoleMappingResource {
|
|||
)
|
||||
)}
|
||||
)
|
||||
public final List<ClientRole> 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<ClientRole> 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<RoleModel>) 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<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"})
|
||||
@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 class AvailableRoleMappingResource extends RoleMappingResource {
|
|||
)
|
||||
)}
|
||||
)
|
||||
public final List<ClientRole> 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<ClientRole> 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<RoleModel>) client::hasDirectScope).negate(), first, max, search);
|
||||
if(this.auth.clients().canManage() || !Profile.isFeatureEnabled(Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ)) {
|
||||
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"})
|
||||
@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 class AvailableRoleMappingResource extends RoleMappingResource {
|
|||
)
|
||||
)}
|
||||
)
|
||||
public final List<ClientRole> 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<ClientRole> 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<RoleModel>) group::hasDirectRole).negate(), first, max, search);
|
||||
if(this.auth.users().canManage() || !Profile.isFeatureEnabled(Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ)) {
|
||||
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"})
|
||||
@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 class AvailableRoleMappingResource extends RoleMappingResource {
|
|||
)
|
||||
)}
|
||||
)
|
||||
public final List<ClientRole> 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<ClientRole> 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<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
|
||||
|
@ -163,8 +199,8 @@ public class AvailableRoleMappingResource extends RoleMappingResource {
|
|||
@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 class AvailableRoleMappingResource extends RoleMappingResource {
|
|||
)
|
||||
)}
|
||||
)
|
||||
public final List<ClientRole> 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<ClientRole> 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<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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.RoleModel;
|
|||
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 class EffectiveRoleMappingResource extends RoleMappingResource {
|
|||
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 class EffectiveRoleMappingResource extends RoleMappingResource {
|
|||
}
|
||||
|
||||
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 class EffectiveRoleMappingResource extends RoleMappingResource {
|
|||
}
|
||||
|
||||
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 class EffectiveRoleMappingResource extends RoleMappingResource {
|
|||
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 class EffectiveRoleMappingResource extends RoleMappingResource {
|
|||
public final List<ClientRole> 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<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()));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<ClientModel> 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;
|
||||
|
|
|
@ -71,4 +71,29 @@ public interface RoleLookupProvider {
|
|||
* Never returns {@code null}.
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ import org.keycloak.models.ClientModel;
|
|||
import org.keycloak.models.ClientScopeModel;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
|
@ -81,4 +82,6 @@ public interface ClientPermissionEvaluator {
|
|||
boolean canMapClientScopeRoles(ClientModel client);
|
||||
|
||||
Map<String, Boolean> getAccess(ClientModel client);
|
||||
|
||||
Set<String> getClientsWithPermission(String scope);
|
||||
}
|
||||
|
|
|
@ -24,17 +24,21 @@ import org.keycloak.authorization.model.Policy;
|
|||
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 @@ class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionM
|
|||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -19,6 +19,8 @@ package org.keycloak.services.resources.admin.permissions;
|
|||
import org.keycloak.models.RoleContainerModel;
|
||||
import org.keycloak.models.RoleModel;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
|
@ -53,4 +55,5 @@ public interface RolePermissionEvaluator {
|
|||
|
||||
void requireView(RoleContainerModel container);
|
||||
|
||||
Set<String> getRolesWithPermission(String scope);
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ import org.keycloak.authorization.model.Policy;
|
|||
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.RealmModel;
|
|||
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 @@ class RolePermissions implements RolePermissionEvaluator, RolePermissionManageme
|
|||
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) {
|
||||
return authz.getStoreFactory().getScopeStore().findByName(server, MAP_ROLE_SCOPE);
|
||||
}
|
||||
|
@ -607,6 +655,4 @@ class RolePermissions implements RolePermissionEvaluator, RolePermissionManageme
|
|||
private static String getRoleResourceName(RoleModel role) {
|
||||
return "role.resource." + role.getId();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -81,6 +81,16 @@ public class HardcodedRoleStorageProvider implements RoleStorageProvider {
|
|||
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 {
|
||||
|
||||
private final RealmModel realm;
|
||||
|
|
Loading…
Reference in a new issue