Details
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 41cb41d..00e41f3 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
@@ -176,7 +176,7 @@ public class RoleAdapter implements RoleModel {
@Override
public boolean hasRole(RoleModel role) {
- return this.equals(role) || KeycloakModelUtils.searchFor(role, this);
+ return this.equals(role) || KeycloakModelUtils.searchFor(role, this, new HashSet<>());
}
@Override
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 10e6252..39e24c3 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
@@ -128,7 +128,7 @@ public class RoleAdapter implements RoleModel, JpaModel<RoleEntity> {
@Override
public boolean hasRole(RoleModel role) {
- return this.equals(role) || KeycloakModelUtils.searchFor(role, this);
+ return this.equals(role) || KeycloakModelUtils.searchFor(role, this, new HashSet<>());
}
@Override
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RoleAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RoleAdapter.java
index e141ff8..cf3c78f 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RoleAdapter.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RoleAdapter.java
@@ -172,7 +172,7 @@ public class RoleAdapter extends AbstractMongoAdapter<MongoRoleEntity> implement
@Override
public boolean hasRole(RoleModel role) {
- return this.equals(role) || KeycloakModelUtils.searchFor(role, this);
+ return this.equals(role) || KeycloakModelUtils.searchFor(role, this, new HashSet<>());
}
public MongoRoleEntity getRole() {
diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java b/server-spi-private/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java
index a258cd7..695fe38 100755
--- a/server-spi-private/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java
+++ b/server-spi-private/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java
@@ -177,14 +177,23 @@ public final class KeycloakModelUtils {
* @param visited set of already visited roles (used for recursion)
* @return true if "role" is descendant of "composite"
*/
- public static boolean searchFor(RoleModel role, RoleModel composite) {
- return composite.isComposite() && (
- composite.getComposites().contains(role) ||
- composite.getComposites().stream()
- .filter(x -> x.isComposite() && x.hasRole(role))
+ public static boolean searchFor(RoleModel role, RoleModel composite, Set<String> visited) {
+ if (visited.contains(composite.getId())) {
+ return false;
+ }
+
+ visited.add(composite.getId());
+
+ if (!composite.isComposite()) {
+ return false;
+ }
+
+ Set<RoleModel> compositeRoles = composite.getComposites();
+ return compositeRoles.contains(role) ||
+ compositeRoles.stream()
+ .filter(x -> x.isComposite() && searchFor(role, x, visited))
.findFirst()
- .isPresent()
- );
+ .isPresent();
}
/**
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/composites/CompositeRoleTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/composites/CompositeRoleTest.java
index e94edce..4cf3904 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/composites/CompositeRoleTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/composites/CompositeRoleTest.java
@@ -17,7 +17,6 @@
package org.keycloak.testsuite.composites;
import org.jboss.arquillian.graphene.page.Page;
-import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.keycloak.OAuth2Constants;
@@ -28,6 +27,7 @@ import org.keycloak.common.enums.SslRequired;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.RoleRepresentation;
+import org.keycloak.testsuite.Assert;
import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.util.ClientBuilder;
@@ -345,4 +345,22 @@ public class CompositeRoleTest extends AbstractCompositeKeycloakTest {
Assert.assertEquals(200, refreshResponse.getStatusCode());
}
+
+ // KEYCLOAK-4274
+ @Test
+ public void testRecursiveComposites() throws Exception {
+ // This will create recursive composite mappings between "REALM_COMPOSITE_1" and "REALM_ROLE_1"
+ RoleRepresentation realmComposite1 = testRealm().roles().get("REALM_COMPOSITE_1").toRepresentation();
+ testRealm().roles().get("REALM_ROLE_1").addComposites(Collections.singletonList(realmComposite1));
+
+ UserResource userResource = ApiUtil.findUserByUsernameId(testRealm(), "REALM_COMPOSITE_1_USER");
+ List<RoleRepresentation> realmRoles = userResource.roles().realmLevel().listEffective();
+ Assert.assertNames(realmRoles, "REALM_COMPOSITE_1", "REALM_ROLE_1");
+
+ userResource = ApiUtil.findUserByUsernameId(testRealm(), "REALM_ROLE_1_USER");
+ realmRoles = userResource.roles().realmLevel().listEffective();
+ Assert.assertNames(realmRoles, "REALM_COMPOSITE_1", "REALM_ROLE_1");
+
+ }
+
}