diff --git a/model/jpa/src/main/java/org/keycloak/storage/jpa/JpaUserFederatedStorageProvider.java b/model/jpa/src/main/java/org/keycloak/storage/jpa/JpaUserFederatedStorageProvider.java index 0d244a26d4..1a09f160e3 100644 --- a/model/jpa/src/main/java/org/keycloak/storage/jpa/JpaUserFederatedStorageProvider.java +++ b/model/jpa/src/main/java/org/keycloak/storage/jpa/JpaUserFederatedStorageProvider.java @@ -474,6 +474,15 @@ public class JpaUserFederatedStorageProvider implements return closing(paginateQuery(query, firstResult, max).getResultStream()); } + + @Override + public Stream getRoleMembersStream(RealmModel realm, RoleModel role, Integer firstResult, Integer max) { + TypedQuery query = em.createNamedQuery("fedRoleMembership", String.class); + query.setParameter("roleId", role.getId()); + query.setParameter("realmId", realm.getId()); + + return closing(paginateQuery(query, firstResult, max).getResultStream()); + } @Override public Stream getRequiredActionsStream(RealmModel realm, String userId) { diff --git a/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/FederatedUserRoleMappingEntity.java b/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/FederatedUserRoleMappingEntity.java index 3a44751e9e..240f759bd5 100755 --- a/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/FederatedUserRoleMappingEntity.java +++ b/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/FederatedUserRoleMappingEntity.java @@ -40,7 +40,7 @@ import java.io.Serializable; @NamedQuery(name="deleteFederatedUserRoleMappingsByRealmAndLink", query="delete from FederatedUserRoleMappingEntity mapping where mapping.userId IN (select u.id from UserEntity u where u.realmId=:realmId and u.federationLink=:link)"), @NamedQuery(name="deleteFederatedUserRoleMappingsByRole", query="delete from FederatedUserRoleMappingEntity m where m.roleId = :roleId"), @NamedQuery(name="deleteFederatedUserRoleMappingsByUser", query="delete from FederatedUserRoleMappingEntity m where m.userId = :userId and m.realmId = :realmId"), - + @NamedQuery(name="fedRoleMembership", query="select m.userId FROM FederatedUserRoleMappingEntity m where m.roleId = :roleId AND m.realmId = :realmId"), }) @Table(name="FED_USER_ROLE_MAPPING") @Entity diff --git a/model/legacy-private/src/main/java/org/keycloak/storage/UserStorageManager.java b/model/legacy-private/src/main/java/org/keycloak/storage/UserStorageManager.java index 6356da6d59..8fd65506b4 100755 --- a/model/legacy-private/src/main/java/org/keycloak/storage/UserStorageManager.java +++ b/model/legacy-private/src/main/java/org/keycloak/storage/UserStorageManager.java @@ -423,6 +423,10 @@ public class UserStorageManager extends AbstractStorageManager getUserById(realm, id)); + } return Stream.empty(); }, realm, firstResult, maxResults); return importValidation(realm, results); diff --git a/model/legacy/src/main/java/org/keycloak/storage/federated/UserRoleMappingsFederatedStorage.java b/model/legacy/src/main/java/org/keycloak/storage/federated/UserRoleMappingsFederatedStorage.java index a63e70028f..e99ee212d1 100644 --- a/model/legacy/src/main/java/org/keycloak/storage/federated/UserRoleMappingsFederatedStorage.java +++ b/model/legacy/src/main/java/org/keycloak/storage/federated/UserRoleMappingsFederatedStorage.java @@ -40,6 +40,17 @@ public interface UserRoleMappingsFederatedStorage { void deleteRoleMapping(RealmModel realm, String userId, RoleModel role); + /** + * Obtains the federated users that are members of the given {@code role} in the specified {@code realm}. + * + * @param realm a reference to the realm. + * @param role a reference to the role whose federated members are being searched. + * @param firstResult 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 a non-null {@code Stream} of federated user ids that are members of the role in the realm. + */ + Stream getRoleMembersStream(RealmModel realm, RoleModel role, Integer firstResult, Integer max); + /** * @deprecated This interface is no longer necessary; collection-based methods were removed from the parent interface * and therefore the parent interface can be used directly diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/UserStorageTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/UserStorageTest.java index 77df0a9dd1..2fcbf1e9fc 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/UserStorageTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/UserStorageTest.java @@ -1,6 +1,7 @@ package org.keycloak.testsuite.federation.storage; import org.apache.commons.io.FileUtils; +import org.hamcrest.Matchers; import org.jboss.arquillian.graphene.page.Page; import org.junit.After; import org.junit.Assert; @@ -20,6 +21,7 @@ import org.keycloak.credential.CredentialProvider; import org.keycloak.credential.CredentialProviderFactory; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; +import org.keycloak.models.RoleModel; import org.keycloak.models.UserModel; import org.keycloak.models.cache.CachedUserModel; import org.keycloak.models.credential.OTPCredentialModel; @@ -867,6 +869,40 @@ public class UserStorageTest extends AbstractAuthTest { adminClient.realms().create(repOrig); } + @Test + public void testRoleMembership() { + RoleRepresentation role1 = new RoleRepresentation(); + role1.setName("role1"); + RoleRepresentation role2 = new RoleRepresentation(); + role2.setName("role2"); + testRealmResource().roles().create(role1); + testRealmResource().roles().create(role2); + + UserRepresentation thor = ApiUtil.findUserByUsername(testRealmResource(), "thor"); + ApiUtil.assignRealmRoles(testRealmResource(), thor.getId(), "role1", "role2"); + + UserRepresentation zeus = ApiUtil.findUserByUsername(testRealmResource(), "zeus"); + ApiUtil.assignRealmRoles(testRealmResource(), zeus.getId(), "role1"); + + + testingClient.server().run(session -> { + RealmModel realm = session.realms().getRealmByName("test"); + RoleModel roleModel1 = session.roles().getRealmRole(realm, "role1"); + RoleModel roleModel2 = session.roles().getRealmRole(realm, "role2"); + + List users = session.users().getRoleMembersStream(realm, roleModel1).map(UserModel::getUsername).collect(Collectors.toList()); + Assert.assertEquals(2, users.size()); + Assert.assertThat(users, Matchers.containsInAnyOrder("thor", "zeus")); + + users = session.users().getRoleMembersStream(realm, roleModel2).map(UserModel::getUsername).collect(Collectors.toList()); + Assert.assertEquals(1, users.size()); + Assert.assertThat(users, Matchers.containsInAnyOrder("thor")); + }); + + testRealmResource().roles().get("role1").remove(); + testRealmResource().roles().get("role2").remove(); + } + @Test @Ignore public void testEntityRemovalHooksCascade() {