diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RoleResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RoleResource.java index 7af6daed28c6..8c095d87d3d0 100644 --- a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RoleResource.java +++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RoleResource.java @@ -17,6 +17,7 @@ package org.keycloak.admin.client.resource; +import jakarta.ws.rs.DefaultValue; import org.keycloak.representations.idm.GroupRepresentation; import org.keycloak.representations.idm.ManagementPermissionReference; import org.keycloak.representations.idm.ManagementPermissionRepresentation; @@ -131,7 +132,7 @@ public interface RoleResource { @Path("users") @Produces(MediaType.APPLICATION_JSON) List getUserMembers(@QueryParam("first") Integer firstResult, - @QueryParam("max") Integer maxResults); + @QueryParam("max") Integer maxResults); /** * Get role members. @@ -147,8 +148,8 @@ List getUserMembers(@QueryParam("first") Integer firstResult @Path("users") @Produces(MediaType.APPLICATION_JSON) List getUserMembers(@QueryParam("briefRepresentation") Boolean briefRepresentation, - @QueryParam("first") Integer firstResult, - @QueryParam("max") Integer maxResults); + @QueryParam("first") Integer firstResult, + @QueryParam("max") Integer maxResults); /** * Get role groups. @@ -173,7 +174,7 @@ List getUserMembers(@QueryParam("briefRepresentation") Boole @Path("groups") @Produces(MediaType.APPLICATION_JSON) Set getRoleGroupMembers(@QueryParam("first") Integer firstResult, - @QueryParam("max") Integer maxResults); + @QueryParam("max") Integer maxResults); /** * Get role members. @@ -205,4 +206,14 @@ Set getRoleGroupMembers(@QueryParam("first") Integer firstR @Deprecated Set getRoleUserMembers(@QueryParam("first") Integer firstResult, @QueryParam("max") Integer maxResults); + + @GET + @Path("parents") + @Produces(MediaType.APPLICATION_JSON) + public Set getParentsRoles(); + + @GET + @Path("parents") + @Produces(MediaType.APPLICATION_JSON) + public Set getParentsRoles(@QueryParam("briefRepresentation") @DefaultValue("true") boolean briefRepresentation); } 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 7ecc221de847..cff300616c2a 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 @@ -311,13 +311,24 @@ private void roleRemovalInvalidations(String roleId, String roleName, String rol } } - + private void invalidateRoleAndComposite(String id) { + invalidations.add(id); + RoleAdapter adapter = managedRoles.get(id); + + if (adapter != null) { + adapter.invalidate(); + adapter.invalidateComposites(); + } + } private void invalidateRole(String id) { invalidations.add(id); RoleAdapter adapter = managedRoles.get(id); - if (adapter != null) adapter.invalidate(); + + if (adapter != null) { + adapter.invalidate(); + } } private void addedRole(String roleId, String roleContainerId) { @@ -904,7 +915,7 @@ public RoleModel getClientRole(ClientModel client, String name) { public boolean removeRole(RoleModel role) { listInvalidations.add(role.getContainer().getId()); - invalidateRole(role.getId()); + invalidateRoleAndComposite(role.getId()); invalidationEvents.add(RoleRemovedEvent.create(role.getId(), role.getName(), role.getContainer().getId())); roleRemovalInvalidations(role.getId(), role.getName(), role.getContainer().getId()); diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RoleAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RoleAdapter.java index 533bb89335fb..71f292eeaf87 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RoleAdapter.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RoleAdapter.java @@ -44,6 +44,7 @@ public class RoleAdapter implements RoleModel { protected RealmCacheSession cacheSession; protected RealmModel realm; protected Set composites; + protected Set parents; private final Supplier modelSupplier; public RoleAdapter(CachedRole cached, RealmCacheSession session, RealmModel realm) { @@ -56,10 +57,18 @@ public RoleAdapter(CachedRole cached, RealmCacheSession session, RealmModel real protected void getDelegateForUpdate() { if (updated == null) { cacheSession.registerRoleInvalidation(cached.getId(), cached.getName(), getContainerId()); + updated = modelSupplier.get(); if (updated == null) throw new IllegalStateException("Not found in database"); } } + + protected void invalidateComposites() { + for (String roleId : cached.getComposites()) { + RoleModel role = realm.getRoleById(roleId); + cacheSession.registerRoleInvalidation(role.getId(), role.getName(), role.getContainerId()); + } + } protected boolean invalidated; @@ -121,6 +130,7 @@ public void addCompositeRole(RoleModel role) { @Override public void removeCompositeRole(RoleModel role) { getDelegateForUpdate(); + invalidateComposites(); updated.removeCompositeRole(role); } @@ -150,6 +160,31 @@ public Stream getCompositesStream(String search, Integer first, Integ return cacheSession.getRoleDelegate().getRolesStream(realm, cached.getComposites().stream(), search, first, max); } + + @Override + public void addParentRole(RoleModel role) { + getDelegateForUpdate(); + updated.addParentRole(role); + } + + @Override + public Stream getParentsStream() { + if (isUpdated()) return updated.getParentsStream(); + + if (parents == null) { + parents = new HashSet<>(); + for (String id : cached.getParents()) { + RoleModel role = realm.getRoleById(id); + if (role == null) { + cacheSession.clear(); + continue; + } + parents.add(role); + } + } + + return parents.stream(); + } @Override public boolean isClientRole() { @@ -248,5 +283,4 @@ public boolean equals(Object o) { public int hashCode() { return getId().hashCode(); } - } diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRole.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRole.java index 13684175058c..37a90f788c07 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRole.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRole.java @@ -38,7 +38,8 @@ public class CachedRole extends AbstractRevisioned implements InRealm { final protected String realm; final protected String description; final protected boolean composite; - final protected Set composites = new HashSet<>(); + final protected Set composites = new HashSet(); + final protected Set parents = new HashSet(); private final LazyLoader> attributes; public CachedRole(Long revision, RoleModel model, RealmModel realm) { @@ -50,6 +51,10 @@ public CachedRole(Long revision, RoleModel model, RealmModel realm) { if (composite) { composites.addAll(model.getCompositesStream().map(RoleModel::getId).collect(Collectors.toSet())); } + + parents.addAll(model.getParentsStream().map(RoleModel::getId).collect(Collectors.toSet())); + + attributes = new DefaultLazyLoader<>(roleModel -> new MultivaluedHashMap<>(roleModel.getAttributes()), MultivaluedHashMap::new); } @@ -72,6 +77,10 @@ public boolean isComposite() { public Set getComposites() { return composites; } + + public Set getParents() { + return parents; + } public MultivaluedHashMap getAttributes(Supplier roleModel) { return attributes.get(roleModel); diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RoleAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RoleAdapter.java index d56f55fd9375..654ba9f4ccd2 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/RoleAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RoleAdapter.java @@ -119,6 +119,21 @@ public Stream getCompositesStream(String search, Integer first, Integ getEntity().getCompositeRoles().stream().map(RoleEntity::getId), search, first, max); } + + @Override + public void addParentRole(RoleModel role) { + RoleEntity entity = toRoleEntity(role); + for (RoleEntity parent : getEntity().getParentRoles()) { + if (parent.equals(entity)) return; + } + getEntity().getParentRoles().add(entity); + } + + @Override + public Stream getParentsStream() { + Stream composites = getEntity().getParentRoles().stream().map(c -> new RoleAdapter(session, realm, em, c)); + return composites.filter(Objects::nonNull); + } @Override public boolean hasRole(RoleModel role) { diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RoleEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RoleEntity.java index d6bd128241ca..09bc5a2638df 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RoleEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RoleEntity.java @@ -97,7 +97,10 @@ public class RoleEntity { @ManyToMany(fetch = FetchType.LAZY, cascade = {}) @JoinTable(name = "COMPOSITE_ROLE", joinColumns = @JoinColumn(name = "COMPOSITE"), inverseJoinColumns = @JoinColumn(name = "CHILD_ROLE")) - private Set compositeRoles; + private Set compositeRoles = new HashSet<>(); + + @ManyToMany(fetch = FetchType.LAZY, cascade = {}, mappedBy = "compositeRoles") + private Set parentRoles = new HashSet<>(); // Explicitly not using OrphanRemoval as we're handling the removal manually through HQL but at the same time we still // want to remove elements from the entity's collection in a manual way. Without this, Hibernate would do a duplicit @@ -161,6 +164,14 @@ public Set getCompositeRoles() { public void setCompositeRoles(Set compositeRoles) { this.compositeRoles = compositeRoles; } + + public Set getParentRoles() { + return parentRoles; + } + + public void setParentRoles(Set parentRoles) { + this.parentRoles = parentRoles; + } public boolean isClientRole() { return clientRole; diff --git a/quarkus/server/src/main/java/org/keycloak/quarkus/_private/IDELauncher.java b/quarkus/server/src/main/java/org/keycloak/quarkus/_private/IDELauncher.java index 86d4dcea5a24..81e2a9091d87 100644 --- a/quarkus/server/src/main/java/org/keycloak/quarkus/_private/IDELauncher.java +++ b/quarkus/server/src/main/java/org/keycloak/quarkus/_private/IDELauncher.java @@ -48,6 +48,10 @@ public static void main(String[] args) { // users can still provide a different folder by setting the property when starting it from their IDE. Path path = Paths.get(System.getProperty("user.dir"), "target", "kc"); System.setProperty("kc.home.dir", path.toAbsolutePath().toString()); + System.setProperty("kc.db", "mariadb"); + System.setProperty("kc.db-url", "jdbc:mariadb://localhost:3306/keycloak"); + System.setProperty("kc.db-username", "keycloak"); + System.setProperty("kc.db-password", "keycloak"); } if (devArgs.isEmpty()) { diff --git a/server-spi/src/main/java/org/keycloak/models/RoleModel.java b/server-spi/src/main/java/org/keycloak/models/RoleModel.java index b0e91c5bdfcd..1a2ae95d907f 100755 --- a/server-spi/src/main/java/org/keycloak/models/RoleModel.java +++ b/server-spi/src/main/java/org/keycloak/models/RoleModel.java @@ -100,4 +100,8 @@ default String getFirstAttribute(String name) { Stream getAttributeStream(String name); Map> getAttributes(); + + void addParentRole(RoleModel role); + + Stream getParentsStream(); } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RoleByIdResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RoleByIdResource.java index 63e4c6a3955a..9f35a9cdbaa2 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/RoleByIdResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/RoleByIdResource.java @@ -16,6 +16,7 @@ */ package org.keycloak.services.resources.admin; +import jakarta.ws.rs.DefaultValue; import org.eclipse.microprofile.openapi.annotations.Operation; import org.eclipse.microprofile.openapi.annotations.extensions.Extension; import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; @@ -52,6 +53,7 @@ import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import java.util.List; +import java.util.Set; import java.util.stream.Stream; /** @@ -245,6 +247,7 @@ public Stream getClientRoleComposites(final @PathParam("role final @PathParam("clientUuid") String clientUuid) { RoleModel role = getRoleModel(id); + auth.roles().requireView(role); ClientModel clientModel = realm.getClientById(clientUuid); if (clientModel == null) { @@ -272,7 +275,7 @@ public void deleteComposites(final @Parameter(description = "Role id") @PathPara } /** - * Return object stating whether role Authorization permissions have been initialized or not and a reference + * Return object stating whether role Authoirzation permissions have been initialized or not and a reference * * * @param id @@ -303,6 +306,29 @@ public static ManagementPermissionReference toMgmtRef(RoleModel role, AdminPermi return ref; } + /** + * Get parents of the roles, thoses which have the given role as composite + * + * @param id Role id + * @param briefRepresentation if false, return a full representation of the roles with their attributes + * @return parents of the roles + */ + @Path("{role-id}/parents") + @GET + @NoCache + @Produces(MediaType.APPLICATION_JSON) + public Set getParentsRoles(final @PathParam("role-id") String id, + final @QueryParam("briefRepresentation") @DefaultValue("true") boolean briefRepresentation) { + RoleModel role = getRoleModel(id); + auth.roles().requireManage(role); + + if (role == null) { + throw new NotFoundException("Could not find role"); + } + + return getParentsRoles(role, briefRepresentation); + } + /** * Return object stating whether role Authorization permissions have been initialized or not and a reference * diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RoleContainerResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RoleContainerResource.java index d864a953126a..2c99405bfc52 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/RoleContainerResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/RoleContainerResource.java @@ -29,6 +29,7 @@ import org.keycloak.events.admin.ResourceType; import org.keycloak.models.ClientModel; import org.keycloak.models.Constants; +import org.keycloak.models.GroupModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ModelDuplicateException; import org.keycloak.models.RealmModel; @@ -60,13 +61,19 @@ import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.UriInfo; + +import java.util.ArrayList; +import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.function.Function; +import java.util.stream.Collectors; import java.util.stream.Stream; +import org.keycloak.services.ErrorResponseException; /** * @resource Roles @@ -310,7 +317,6 @@ public Response updateRole(final @Parameter(description = "role's name (not id!) @Consumes(MediaType.APPLICATION_JSON) @Tag(name = KeycloakOpenAPI.Admin.Tags.ROLES) @Operation( summary = "Add a composite to the role") - @APIResponse(responseCode = "204", description = "No Content") public void addComposites(final @Parameter(description = "role's name (not id!)") @PathParam("role-name") String roleName, List roles) { auth.roles().requireManage(roleContainer); RoleModel role = roleContainer.getRole(roleName); @@ -369,14 +375,14 @@ public Stream getRealmRoleComposites(final @Parameter(descri * @param clientUuid * @return */ - @Path("{role-name}/composites/clients/{client-uuid}") + @Path("{role-name}/composites/clients/{clientUuid}") @GET @NoCache @Produces(MediaType.APPLICATION_JSON) @Tag(name = KeycloakOpenAPI.Admin.Tags.ROLES) @Operation( summary = "Get client-level roles for the client that are in the role's composite") public Stream getClientRoleComposites(final @Parameter(description = "role's name (not id!)") @PathParam("role-name") String roleName, - final @PathParam("client-uuid") String clientUuid) { + final @PathParam("clientUuid") String clientUuid) { auth.roles().requireView(roleContainer); RoleModel role = roleContainer.getRole(roleName); if (role == null) { @@ -478,7 +484,6 @@ public ManagementPermissionReference setManagementPermissionsEnabled(final @Path * @param roleName the role name. * @param firstResult first result to return. Ignored if negative or {@code null}. * @param maxResults maximum number of results to return. Ignored if negative or {@code null}. - * @param briefRepresentation Boolean which defines whether brief representations are returned (default: false) * @return a non-empty {@code Stream} of users. */ @Path("{role-name}/users") @@ -490,24 +495,34 @@ public ManagementPermissionReference setManagementPermissionsEnabled(final @Path public Stream getUsersInRole(final @Parameter(description = "the role name.") @PathParam("role-name") String roleName, @Parameter(description = "Boolean which defines whether brief representations are returned (default: false)") @QueryParam("briefRepresentation") Boolean briefRepresentation, @Parameter(description = "first result to return. Ignored if negative or {@code null}.") @QueryParam("first") Integer firstResult, - @Parameter(description = "maximum number of results to return. Ignored if negative or {@code null}.") @QueryParam("max") Integer maxResults) { - + @Parameter(description = "maximum number of results to return. Ignored if negative or {@code null}.") @QueryParam("max") Integer maxResults, + @QueryParam("composite") @DefaultValue("false") boolean composite) { + auth.roles().requireView(roleContainer); - firstResult = firstResult != null ? firstResult : 0; - maxResults = maxResults != null ? maxResults : Constants.DEFAULT_MAX_RESULTS; - + + int effectiveFirstResult = Optional.ofNullable(firstResult).orElse(0); + int effectiveMaxResults = Optional.ofNullable(maxResults).orElse(Constants.DEFAULT_MAX_RESULTS); + RoleModel role = roleContainer.getRole(roleName); if (role == null) { throw new NotFoundException("Could not find role"); } - final Function toRepresentation = briefRepresentation != null && briefRepresentation + Function toRepresentation = Boolean.TRUE.equals(briefRepresentation) ? ModelToRepresentation::toBriefRepresentation : user -> ModelToRepresentation.toRepresentation(session, realm, user); - return session.users().getRoleMembersStream(realm, role, firstResult, maxResults) - .map(toRepresentation); + + if (!composite) { + return session.users() + .getRoleMembersStream(realm, role, effectiveFirstResult, effectiveMaxResults) + .map(toRepresentation); + } else { + Set userModels = new HashSet<>(); + getAllUsersInRole(role, new HashSet<>(), new HashSet<>(), userModels); + return userModels.stream().map(toRepresentation); + } } - + /** * Returns a stream of groups that have the specified role name * @@ -528,17 +543,64 @@ public Stream getGroupsInRole(final @Parameter(description @Parameter(description = "first result to return. Ignored if negative or {@code null}.") @QueryParam("first") Integer firstResult, @Parameter(description = "maximum number of results to return. Ignored if negative or {@code null}.") @QueryParam("max") Integer maxResults, @Parameter(description = "if false, return a full representation of the {@code GroupRepresentation} objects.") @QueryParam("briefRepresentation") @DefaultValue("true") boolean briefRepresentation) { - + auth.roles().requireView(roleContainer); firstResult = firstResult != null ? firstResult : 0; maxResults = maxResults != null ? maxResults : Constants.DEFAULT_MAX_RESULTS; - + RoleModel role = roleContainer.getRole(roleName); if (role == null) { throw new NotFoundException("Could not find role"); } - + return session.groups().getGroupsByRoleStream(realm, role, firstResult, maxResults) .map(g -> ModelToRepresentation.toRepresentation(g, !briefRepresentation)); - } + } + + protected void getAllUsersInRoleFromGroup(RoleModel role, GroupModel group, Set visitedRoles, + Set visitedGroups, Set users) { + + if (!visitedGroups.contains(group.getId())) { + List usersInGroup = session.users().getGroupMembersStream(realm, group).collect(Collectors.toList()); + + users.addAll(usersInGroup); + + // We define our current group as already visited + visitedGroups.add(group.getId()); + + Set subGroups = group.getSubGroupsStream().collect(Collectors.toSet()); + + for(GroupModel subGroup : subGroups) { + getAllUsersInRoleFromGroup(role, subGroup, visitedRoles, visitedGroups, users); + } + } + } + + protected void getAllUsersInRole(RoleModel role, Set visitedRoles, Set visitedGroups, + Set users) { + + if (!visitedRoles.contains(role.getId())) { + // We found the users directly assign to this role and we add them to our set of users + Set usersForCurrentRole = new HashSet( + session.users().getRoleMembersStream(realm, role).collect(Collectors.toSet())); + + users.addAll(usersForCurrentRole); + + // We define our current role as already visited + visitedRoles.add(role.getId()); + + Set compositesRole = role.getParentsStream().collect(Collectors.toSet()); + + for (RoleModel compositeRole : compositesRole) { + getAllUsersInRole(compositeRole, visitedRoles, visitedGroups, users); + } + + // We looks for groups directly assign to the role + List groups = session.groups().getGroupsByRoleStream(realm, role, -1, -1).collect(Collectors.toList()); + + for (GroupModel group : groups) { + getAllUsersInRoleFromGroup(role, group, visitedRoles, visitedGroups, users); + } + } + } } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RoleResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RoleResource.java index fba5835f4e41..0916d4d04ddd 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/RoleResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/RoleResource.java @@ -36,6 +36,9 @@ import java.util.Set; import java.util.stream.Stream; +import java.util.Collections; +import java.util.stream.Collectors; + /** * @resource Roles * @author Bill Burke @@ -121,6 +124,7 @@ protected void addComposites(AdminPermissionEvaluator auth, AdminEventBuilder ad throw new NotFoundException("Could not find composite role"); } auth.roles().requireMapComposite(composite); + composite.addParentRole(role); role.addCompositeRole(composite); } @@ -162,4 +166,12 @@ protected void deleteComposites(AdminEventBuilder adminEvent, UriInfo uriInfo, L adminEvent.operation(OperationType.DELETE).resourcePath(uriInfo).representation(roles).success(); } + + protected Set getParentsRoles(RoleModel role, boolean briefRepresentation) { + if(briefRepresentation) { + return role.getParentsStream().map(ModelToRepresentation::toBriefRepresentation).collect(Collectors.toSet()); + } + + return role.getParentsStream().map(ModelToRepresentation::toRepresentation).collect(Collectors.toSet()); + } } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java index 16921dac7fb1..7b63c80ac21a 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java @@ -49,6 +49,7 @@ import org.keycloak.models.ModelException; import org.keycloak.models.ModelIllegalStateException; import org.keycloak.models.RealmModel; +import org.keycloak.models.RoleModel; import org.keycloak.models.UserConsentModel; import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserLoginFailureModel; @@ -68,6 +69,7 @@ import org.keycloak.representations.idm.ErrorRepresentation; import org.keycloak.representations.idm.FederatedIdentityRepresentation; import org.keycloak.representations.idm.GroupRepresentation; +import org.keycloak.representations.idm.RoleRepresentation; import org.keycloak.representations.idm.UserConsentRepresentation; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.idm.UserSessionRepresentation; @@ -936,7 +938,6 @@ public Response executeActionsEmail(@Parameter(description = "Redirect uri") @Qu * * @param redirectUri Redirect uri * @param clientId Client id - * @param lifespan Number of seconds after which the generated token expires * @return */ @Path("send-verify-email") @@ -1154,4 +1155,25 @@ public SendEmailParams(String redirectUri, String clientId, Integer lifespan) { this.lifespan = lifespan; } } + + @GET + @NoCache + @Path("role-by-id/{roleId}") + @Produces(MediaType.APPLICATION_JSON) + public RoleRepresentation userHasRole(@PathParam("roleId") String roleId) { + + auth.users().requireView(user); + + RoleModel role = realm.getRoleById(roleId); + + if (role == null) { + throw new ErrorResponseException("not_found", "Role not found",Status.NOT_FOUND); + } + + if(user.hasRole(role)) { + return ModelToRepresentation.toRepresentation(role); + } + + throw new ErrorResponseException("access_denied", "Role not assigned to the user",Status.FORBIDDEN); + } } 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 93b3cf5d7fd4..b73fde8d076b 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 @@ -197,7 +197,16 @@ public void setAttribute(String name, List values) { public void removeAttribute(String name) { throw new ReadOnlyException("role is read only"); } - } + @Override + public void addParentRole(RoleModel role) { + throw new ReadOnlyException("role is read only"); + } + @Override + public Stream getParentsStream() { + return Stream.empty(); + } + } } + diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/ClientRolesTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/ClientRolesTest.java index c1ceccd2b56a..b815d289e907 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/ClientRolesTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/ClientRolesTest.java @@ -17,6 +17,7 @@ package org.keycloak.testsuite.admin.client; +import jakarta.ws.rs.ClientErrorException; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -30,6 +31,7 @@ import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.testsuite.Assert; import org.keycloak.testsuite.util.AdminEventPaths; +import org.keycloak.testsuite.util.RoleBuilder; import java.util.ArrayList; import java.util.Arrays; @@ -42,7 +44,6 @@ import java.util.Set; import java.util.stream.Collectors; import java.util.stream.IntStream; -import jakarta.ws.rs.ClientErrorException; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.hasSize; @@ -52,8 +53,6 @@ import static org.junit.Assert.assertTrue; import static org.keycloak.testsuite.auth.page.AuthRealm.TEST; -import org.keycloak.testsuite.util.RoleBuilder; - /** * @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc. */ @@ -260,91 +259,99 @@ private static List extractUsernames(Collection user @Test public void testSearchForRoles() { - + for(int i = 0; i<15; i++) { String roleName = "role"+i; RoleRepresentation role = makeRole(roleName); rolesRsc.create(role); - assertAdminEvents.assertEvent(getRealmId(), OperationType.CREATE, AdminEventPaths.clientRoleResourcePath(clientDbId,roleName), role, ResourceType.CLIENT_ROLE); - } - + assertAdminEvents.assertEvent(getRealmId(), OperationType.CREATE, AdminEventPaths.clientRoleResourcePath(clientDbId,roleName), role, ResourceType.CLIENT_ROLE); + } + String roleNameA = "abcdef"; RoleRepresentation roleA = makeRole(roleNameA); rolesRsc.create(roleA); - assertAdminEvents.assertEvent(getRealmId(), OperationType.CREATE, AdminEventPaths.clientRoleResourcePath(clientDbId,roleNameA), roleA, ResourceType.CLIENT_ROLE); - + assertAdminEvents.assertEvent(getRealmId(), OperationType.CREATE, AdminEventPaths.clientRoleResourcePath(clientDbId,roleNameA), roleA, ResourceType.CLIENT_ROLE); + String roleNameB = "defghi"; RoleRepresentation roleB = makeRole(roleNameB); rolesRsc.create(roleB); assertAdminEvents.assertEvent(getRealmId(), OperationType.CREATE, AdminEventPaths.clientRoleResourcePath(clientDbId,roleNameB), roleB, ResourceType.CLIENT_ROLE); - + List resultSearch = rolesRsc.list("def", -1, -1); assertEquals(2,resultSearch.size()); - + List resultSearch2 = rolesRsc.list("role", -1, -1); assertEquals(15,resultSearch2.size()); - + List resultSearchPagination = rolesRsc.list("role", 1, 5); assertEquals(5,resultSearchPagination.size()); } - + @Test public void testPaginationRoles() { - + for(int i = 0; i<15; i++) { String roleName = "role"+i; RoleRepresentation role = makeRole(roleName); rolesRsc.create(role); - assertAdminEvents.assertEvent(getRealmId(), OperationType.CREATE, AdminEventPaths.clientRoleResourcePath(clientDbId,roleName), role, ResourceType.CLIENT_ROLE); - } - + assertAdminEvents.assertEvent(getRealmId(), OperationType.CREATE, AdminEventPaths.clientRoleResourcePath(clientDbId,roleName), role, ResourceType.CLIENT_ROLE); + } + List resultSearchWithoutPagination = rolesRsc.list(); assertEquals(15,resultSearchWithoutPagination.size()); - + List resultSearchPagination = rolesRsc.list(1, 5); assertEquals(5,resultSearchPagination.size()); - + List resultSearchPaginationIncoherentParams = rolesRsc.list(1, null); assertTrue(resultSearchPaginationIncoherentParams.size() >= 15); } - + @Test public void testPaginationRolesCache() { - + for(int i = 0; i<5; i++) { String roleName = "paginaterole"+i; RoleRepresentation role = makeRole(roleName); rolesRsc.create(role); - assertAdminEvents.assertEvent(getRealmId(), OperationType.CREATE, AdminEventPaths.clientRoleResourcePath(clientDbId,roleName), role, ResourceType.CLIENT_ROLE); - } - - List resultBeforeAddingRoleToTestCache = rolesRsc.list(1, 1000); - + assertAdminEvents.assertEvent(getRealmId(), OperationType.CREATE, AdminEventPaths.clientRoleResourcePath(clientDbId,roleName), role, ResourceType.CLIENT_ROLE); + } + + List resultBeforeAddingRoleToTestCache = rolesRsc.list(1, 1000); + // after a first call which init the cache, we add a new role to see if the result change - + RoleRepresentation role = makeRole("anewrole"); rolesRsc.create(role); - assertAdminEvents.assertEvent(getRealmId(), OperationType.CREATE, AdminEventPaths.clientRoleResourcePath(clientDbId,"anewrole"), role, ResourceType.CLIENT_ROLE); - + assertAdminEvents.assertEvent(getRealmId(), OperationType.CREATE, AdminEventPaths.clientRoleResourcePath(clientDbId,"anewrole"), role, ResourceType.CLIENT_ROLE); + List resultafterAddingRoleToTestCache = rolesRsc.list(1, 1000); - + assertEquals(resultBeforeAddingRoleToTestCache.size()+1, resultafterAddingRoleToTestCache.size()); } - + @Test public void getRolesWithFullRepresentation() { for(int i = 0; i<5; i++) { String roleName = "attributesrole"+i; RoleRepresentation role = makeRole(roleName); - + Map> attributes = new HashMap>(); attributes.put("attribute1", Arrays.asList("value1","value2")); role.setAttributes(attributes); - + rolesRsc.create(role); - assertAdminEvents.assertEvent(getRealmId(), OperationType.CREATE, AdminEventPaths.clientRoleResourcePath(clientDbId,roleName), role, ResourceType.CLIENT_ROLE); + assertAdminEvents.assertEvent(getRealmId(), OperationType.CREATE, AdminEventPaths.clientRoleResourcePath(clientDbId,roleName), role, ResourceType.CLIENT_ROLE); + + // we have to update the role to set the attributes because + // the add role endpoint only care about name and description + RoleResource roleToUpdate = rolesRsc.get(roleName); + role.setId(roleToUpdate.toRepresentation().getId()); + + roleToUpdate.update(role); + assertAdminEvents.assertEvent(getRealmId(), OperationType.UPDATE, AdminEventPaths.clientRoleResourcePath(clientDbId,roleName), role, ResourceType.CLIENT_ROLE); } - + List roles = rolesRsc.list(false); assertTrue(roles.get(0).getAttributes().containsKey("attribute1")); } @@ -354,16 +361,112 @@ public void getRolesWithBriefRepresentation() { for(int i = 0; i<5; i++) { String roleName = "attributesrole"+i; RoleRepresentation role = makeRole(roleName); - + Map> attributes = new HashMap>(); attributes.put("attribute1", Arrays.asList("value1","value2")); role.setAttributes(attributes); - + rolesRsc.create(role); assertAdminEvents.assertEvent(getRealmId(), OperationType.CREATE, AdminEventPaths.clientRoleResourcePath(clientDbId,roleName), role, ResourceType.CLIENT_ROLE); + + // we have to update the role to set the attributes because + // the add role endpoint only care about name and description + RoleResource roleToUpdate = rolesRsc.get(roleName); + role.setId(roleToUpdate.toRepresentation().getId()); + + roleToUpdate.update(role); + assertAdminEvents.assertEvent(getRealmId(), OperationType.UPDATE, AdminEventPaths.clientRoleResourcePath(clientDbId,roleName), role, ResourceType.CLIENT_ROLE); } - + List roles = rolesRsc.list(); assertNull(roles.get(0).getAttributes()); } + + @Test + public void testParents() { + String roleAName = "role-parent-a"; + RoleRepresentation roleA = makeRole(roleAName); + rolesRsc.create(roleA); + assertAdminEvents.assertEvent(getRealmId(), OperationType.CREATE, + AdminEventPaths.clientRoleResourcePath(clientDbId, roleAName), roleA, ResourceType.CLIENT_ROLE); + + String roleBName = "role-parent-b"; + RoleRepresentation roleB = makeRole(roleBName); + rolesRsc.create(roleB); + assertAdminEvents.assertEvent(getRealmId(), OperationType.CREATE, + AdminEventPaths.clientRoleResourcePath(clientDbId, roleBName), roleB, ResourceType.CLIENT_ROLE); + + String roleCName = "role-parent-c"; + RoleRepresentation roleC = makeRole(roleCName); + testRealmResource().roles().create(roleC); + assertAdminEvents.assertEvent(getRealmId(), OperationType.CREATE, AdminEventPaths.roleResourcePath(roleCName), + roleC, ResourceType.REALM_ROLE); + + // We define A composite with B and C + List l = new LinkedList<>(); + l.add(rolesRsc.get(roleBName).toRepresentation()); + l.add(testRealmResource().roles().get(roleCName).toRepresentation()); + rolesRsc.get(roleAName).addComposites(l); + assertAdminEvents.assertEvent(getRealmId(), OperationType.CREATE, + AdminEventPaths.clientRoleResourceCompositesPath(clientDbId, roleAName), l, ResourceType.CLIENT_ROLE); + + // We define B composite with A and C + List lb = new LinkedList<>(); + lb.add(rolesRsc.get(roleAName).toRepresentation()); + lb.add(testRealmResource().roles().get(roleCName).toRepresentation()); + rolesRsc.get(roleBName).addComposites(lb); + assertAdminEvents.assertEvent(getRealmId(), OperationType.CREATE, + AdminEventPaths.clientRoleResourceCompositesPath(clientDbId, roleBName), lb, ResourceType.CLIENT_ROLE); + + // So C should have two "parents" which are A and B and C is not composit itself + + assertFalse(testRealmResource().roles().get(roleCName).toRepresentation().isComposite()); + Set parentsOfC = testRealmResource().roles().get(roleCName).getParentsRoles(); + + assertEquals(2, parentsOfC.size()); + Assert.assertNames(parentsOfC, roleAName, roleBName); + + // Now we unassign A and C from B to test the cache + rolesRsc.get(roleBName).deleteComposites(lb); + assertAdminEvents.assertEvent(getRealmId(), OperationType.DELETE, + AdminEventPaths.clientRoleResourceCompositesPath(clientDbId, roleBName), lb, ResourceType.CLIENT_ROLE); + + //So C should have one "parent" which is A + Set parentsOfCAfterCache = testRealmResource().roles().get(roleCName).getParentsRoles(); + + assertEquals(1, parentsOfCAfterCache.size()); + Assert.assertNames(parentsOfCAfterCache, roleAName); + + } + + @Test + public void testGetParentsAfterDeletetionOfAParentRole() { + String roleAName = "role-direct"; + RoleRepresentation roleA = makeRole(roleAName); + rolesRsc.create(roleA); + assertAdminEvents.assertEvent(getRealmId(), OperationType.CREATE, + AdminEventPaths.clientRoleResourcePath(clientDbId, roleAName), roleA, ResourceType.CLIENT_ROLE); + + String roleBName = "role-indrect"; + RoleRepresentation roleB = makeRole(roleBName); + rolesRsc.create(roleB); + assertAdminEvents.assertEvent(getRealmId(), OperationType.CREATE, + AdminEventPaths.clientRoleResourcePath(clientDbId, roleBName), roleB, ResourceType.CLIENT_ROLE); + + // We define B with direct A as composite role + List l = new LinkedList<>(); + l.add(rolesRsc.get(roleAName).toRepresentation()); + rolesRsc.get(roleBName).addComposites(l); + assertAdminEvents.assertEvent(getRealmId(), OperationType.CREATE, + AdminEventPaths.clientRoleResourceCompositesPath(clientDbId, roleBName), l, ResourceType.CLIENT_ROLE); + + Set parentsOfAAfterCache = rolesRsc.get(roleAName).getParentsRoles(); + assertEquals(1, parentsOfAAfterCache.size()); + Assert.assertNames(parentsOfAAfterCache, roleBName); + + rolesRsc.get(roleBName).remove(); + + Set parentsOfAAfterRemoveCache = rolesRsc.get(roleAName).getParentsRoles(); + assertEquals(0, parentsOfAAfterRemoveCache.size()); + } }