keycloak-aplcache

Merge pull request #3479 from hmlnarik/KEYCLOAK-3469-UserRealmRoleMapper KEYCLOAK-3469

11/18/2016 6:21:56 AM
2.4.0.Test

Details

diff --git a/server-spi/src/main/java/org/keycloak/models/RoleMapperModel.java b/server-spi/src/main/java/org/keycloak/models/RoleMapperModel.java
index 85f1fd3..46c3198 100755
--- a/server-spi/src/main/java/org/keycloak/models/RoleMapperModel.java
+++ b/server-spi/src/main/java/org/keycloak/models/RoleMapperModel.java
@@ -24,8 +24,17 @@ import java.util.Set;
  * @version $Revision: 1 $
  */
 public interface RoleMapperModel {
+    /**
+     * Returns set of realm roles that are directly set to this object.
+     * @return see description
+     */
     Set<RoleModel> getRealmRoleMappings();
 
+    /**
+     * Returns set of client roles that are directly set to this object for the given client.
+     * @param app Client to get the roles for
+     * @return see description
+     */
     Set<RoleModel> getClientRoleMappings(ClientModel app);
 
     /**
@@ -48,7 +57,15 @@ public interface RoleMapperModel {
      */
     void grantRole(RoleModel role);
 
+    /**
+     * Returns set of all role (both realm all client) that are directly set to this object.
+     * @return
+     */
     Set<RoleModel> getRoleMappings();
 
+    /**
+     * Removes the given role mapping from this object.
+     * @param role Role to remove
+     */
     void deleteRoleMapping(RoleModel role);
 }
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/AbstractUserRoleMappingMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/AbstractUserRoleMappingMapper.java
index de4d054..4de3720 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/AbstractUserRoleMappingMapper.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/AbstractUserRoleMappingMapper.java
@@ -17,18 +17,19 @@
 
 package org.keycloak.protocol.oidc.mappers;
 
-import org.keycloak.models.ClientSessionModel;
-import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.GroupModel;
 import org.keycloak.models.ProtocolMapperModel;
 import org.keycloak.models.RoleModel;
+import org.keycloak.models.UserModel;
 import org.keycloak.models.UserSessionModel;
-import org.keycloak.representations.AccessToken;
 import org.keycloak.representations.IDToken;
 
 import java.util.ArrayDeque;
 import java.util.Deque;
-import java.util.LinkedHashSet;
 import java.util.Set;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
 /**
  * Base class for mapping of user role mappings to an ID and Access Token claim.
@@ -38,39 +39,95 @@ import java.util.Set;
 abstract class AbstractUserRoleMappingMapper extends AbstractOIDCProtocolMapper implements OIDCAccessTokenMapper, OIDCIDTokenMapper, UserInfoTokenMapper {
 
     /**
-     * Returns the role names extracted from the given {@code roleModels} while recursively traversing "Composite Roles".
-     * <p>
-     * Optionally prefixes each role name with the given {@code prefix}.
-     * </p>
-     *
-     * @param roleModels
-     * @param prefix     the prefix to apply, may be {@literal null}
+     * Returns a stream with roles that come from:
+     * <ul>
+     * <li>Direct assignment of the role to the user</li>
+     * <li>Direct assignment of the role to any group of the user or any of its parent group</li>
+     * <li>Composite roles are expanded recursively, the composite role itself is also contained in the returned stream</li>
+     * </ul>
+     * @param user User to enumerate the roles for
+     * @return
+     */
+    public static Stream<RoleModel> getAllUserRolesStream(UserModel user) {
+        return Stream.concat(
+          user.getRoleMappings().stream(),
+          user.getGroups().stream()
+            .flatMap(g -> groupAndItsParentsStream(g))
+            .flatMap(g -> g.getRoleMappings().stream()))
+          .flatMap(role -> expandCompositeRolesStream(role));
+    }
+
+    /**
+     * Returns stream of the given group and its parents (recursively).
+     * @param group
      * @return
      */
-    protected Set<String> flattenRoleModelToRoleNames(Set<RoleModel> roleModels, String prefix) {
+    private static Stream<GroupModel> groupAndItsParentsStream(GroupModel group) {
+        Stream.Builder<GroupModel> sb = Stream.builder();
+        while (group != null) {
+            sb.add(group);
+            group = group.getParent();
+        }
+        return sb.build();
+    }
 
-        Set<String> roleNames = new LinkedHashSet<>();
+    /**
+     * Recursively expands composite roles into their composite.
+     * @param role
+     * @return Stream of containing all of the composite roles and their components.
+     */
+    private static Stream<RoleModel> expandCompositeRolesStream(RoleModel role) {
+        Stream.Builder<RoleModel> sb = Stream.builder();
 
-        Deque<RoleModel> stack = new ArrayDeque<>(roleModels);
-        while (!stack.isEmpty()) {
+        Deque<RoleModel> stack = new ArrayDeque<>();
+        stack.add(role);
 
+        while (! stack.isEmpty()) {
             RoleModel current = stack.pop();
+            sb.add(current);
 
             if (current.isComposite()) {
-                for (RoleModel compositeRoleModel : current.getComposites()) {
-                    stack.push(compositeRoleModel);
-                }
+                stack.addAll(current.getComposites());
             }
+        }
 
-            String roleName = current.getName();
+        return sb.build();
+    }
 
-            if (prefix != null && !prefix.trim().isEmpty()) {
-                roleName = prefix.trim() + roleName;
-            }
+    /**
+     * Retrieves all roles of the current user based on direct roles set to the user, its groups and their parent groups.
+     * Then it recursively expands all composite roles, and restricts according to the given predicate {@code restriction}.
+     * If the current client sessions is restricted (i.e. no client found in active user session has full scope allowed),
+     * the final list of roles is also restricted by the client scope. Finally, the list is mapped to the token into
+     * a claim.
+     *
+     * @param token
+     * @param mappingModel
+     * @param userSession
+     * @param restriction
+     * @param prefix
+     */
+    protected static void setClaim(IDToken token, ProtocolMapperModel mappingModel, UserSessionModel userSession,
+      Predicate<RoleModel> restriction, String prefix) {
+        String rolePrefix = prefix == null ? "" : prefix;
+        UserModel user = userSession.getUser();
+
+        // get a set of all realm roles assigned to the user or its group
+        Stream<RoleModel> clientUserRoles = getAllUserRolesStream(user).filter(restriction);
 
-            roleNames.add(roleName);
+        boolean dontLimitScope = userSession.getClientSessions().stream().anyMatch(cs -> cs.getClient().isFullScopeAllowed());
+        if (! dontLimitScope) {
+            Set<RoleModel> clientRoles = userSession.getClientSessions().stream()
+              .flatMap(cs -> cs.getClient().getScopeMappings().stream())
+              .collect(Collectors.toSet());
+
+            clientUserRoles = clientUserRoles.filter(clientRoles::contains);
         }
 
-        return roleNames;
+        Set<String> realmRoleNames = clientUserRoles
+          .map(m -> rolePrefix + m.getName())
+          .collect(Collectors.toSet());
+
+        OIDCAttributeMapperHelper.mapClaim(token, mappingModel, realmRoleNames);
     }
 }
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/OIDCAttributeMapperHelper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/OIDCAttributeMapperHelper.java
index 99b2610..8b64aef 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/OIDCAttributeMapperHelper.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/OIDCAttributeMapperHelper.java
@@ -100,6 +100,9 @@ public class OIDCAttributeMapperHelper {
         if (attributeValue == null) return;
 
         String protocolClaim = mappingModel.getConfig().get(TOKEN_CLAIM_NAME);
+        if (protocolClaim == null) {
+            return;
+        }
         String[] split = protocolClaim.split("\\.");
         Map<String, Object> jsonObject = token.getOtherClaims();
         for (int i = 0; i < split.length; i++) {
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserClientRoleMappingMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserClientRoleMappingMapper.java
index 01d47e1..5a88c2a 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserClientRoleMappingMapper.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserClientRoleMappingMapper.java
@@ -18,17 +18,20 @@
 package org.keycloak.protocol.oidc.mappers;
 
 import org.keycloak.models.ClientModel;
+import org.keycloak.models.ClientTemplateModel;
 import org.keycloak.models.ProtocolMapperModel;
+import org.keycloak.models.RealmModel;
 import org.keycloak.models.RoleModel;
-import org.keycloak.models.UserModel;
 import org.keycloak.models.UserSessionModel;
 import org.keycloak.protocol.ProtocolMapperUtils;
 import org.keycloak.provider.ProviderConfigProperty;
 import org.keycloak.representations.IDToken;
 
 import java.util.ArrayList;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
+import java.util.function.Predicate;
 
 /**
  * Allows mapping of user client role mappings to an ID and Access Token claim.
@@ -39,7 +42,7 @@ public class UserClientRoleMappingMapper extends AbstractUserRoleMappingMapper {
 
     public static final String PROVIDER_ID = "oidc-usermodel-client-role-mapper";
 
-    private static final List<ProviderConfigProperty> CONFIG_PROPERTIES = new ArrayList<ProviderConfigProperty>();
+    private static final List<ProviderConfigProperty> CONFIG_PROPERTIES = new ArrayList<>();
 
     static {
 
@@ -60,6 +63,7 @@ public class UserClientRoleMappingMapper extends AbstractUserRoleMappingMapper {
         OIDCAttributeMapperHelper.addAttributeConfig(CONFIG_PROPERTIES, UserClientRoleMappingMapper.class);
     }
 
+    @Override
     public List<ProviderConfigProperty> getConfigProperties() {
         return CONFIG_PROPERTIES;
     }
@@ -84,23 +88,51 @@ public class UserClientRoleMappingMapper extends AbstractUserRoleMappingMapper {
         return "Map a user client role to a token claim.";
     }
 
+    @Override
     protected void setClaim(IDToken token, ProtocolMapperModel mappingModel, UserSessionModel userSession) {
+        String clientId = mappingModel.getConfig().get(ProtocolMapperUtils.USER_MODEL_CLIENT_ROLE_MAPPING_CLIENT_ID);
+        String rolePrefix = mappingModel.getConfig().get(ProtocolMapperUtils.USER_MODEL_CLIENT_ROLE_MAPPING_ROLE_PREFIX);
+
+        setClaim(token, mappingModel, userSession, getClientRoleFilter(clientId, userSession), rolePrefix);
+    }
 
-        UserModel user = userSession.getUser();
+    private static Predicate<RoleModel> getClientRoleFilter(String clientId, UserSessionModel userSession) {
+        if (clientId == null) {
+            return RoleModel::isClientRole;
+        }
 
-        String clientId = mappingModel.getConfig().get(ProtocolMapperUtils.USER_MODEL_CLIENT_ROLE_MAPPING_CLIENT_ID);
-        if (clientId != null) {
+        RealmModel clientRealm = userSession.getRealm();
+        ClientModel client = clientRealm.getClientByClientId(clientId.trim());
 
-            ClientModel clientModel = userSession.getRealm().getClientByClientId(clientId.trim());
-            Set<RoleModel> clientRoleMappings = user.getClientRoleMappings(clientModel);
+        if (client == null) {
+            return RoleModel::isClientRole;
+        }
 
-            String rolePrefix = mappingModel.getConfig().get(ProtocolMapperUtils.USER_MODEL_CLIENT_ROLE_MAPPING_ROLE_PREFIX);
-            Set<String> clientRoleNames = flattenRoleModelToRoleNames(clientRoleMappings, rolePrefix);
+        ClientTemplateModel template = client.getClientTemplate();
+        boolean useTemplateScope = template != null && client.useTemplateScope();
+        boolean fullScopeAllowed = (useTemplateScope && template.isFullScopeAllowed()) || client.isFullScopeAllowed();
 
-            OIDCAttributeMapperHelper.mapClaim(token, mappingModel, clientRoleNames);
+        Set<RoleModel> clientRoleMappings = client.getRoles();
+        if (fullScopeAllowed) {
+            return clientRoleMappings::contains;
+        }
+
+        Set<RoleModel> scopeMappings = new HashSet<>();
+
+        if (useTemplateScope) {
+            Set<RoleModel> templateScopeMappings = template.getScopeMappings();
+            if (templateScopeMappings != null) {
+                scopeMappings.addAll(templateScopeMappings);
+            }
         }
-    }
 
+        Set<RoleModel> clientScopeMappings = client.getScopeMappings();
+        if (clientScopeMappings != null) {
+            scopeMappings.addAll(clientScopeMappings);
+        }
+
+        return role -> clientRoleMappings.contains(role) && scopeMappings.contains(role);
+    }
 
     public static ProtocolMapperModel create(String clientId, String clientRolePrefix,
                                              String name,
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserRealmRoleMappingMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserRealmRoleMappingMapper.java
index ef98182..f978b08 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserRealmRoleMappingMapper.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserRealmRoleMappingMapper.java
@@ -18,18 +18,13 @@
 package org.keycloak.protocol.oidc.mappers;
 
 import org.keycloak.models.ProtocolMapperModel;
-import org.keycloak.models.UserModel;
 import org.keycloak.models.UserSessionModel;
 import org.keycloak.protocol.ProtocolMapperUtils;
-import org.keycloak.protocol.oidc.OIDCLoginProtocol;
 import org.keycloak.provider.ProviderConfigProperty;
 import org.keycloak.representations.IDToken;
 
 import java.util.ArrayList;
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
-import java.util.Set;
 
 /**
  * Allows mapping of user realm role mappings to an ID and Access Token claim.
@@ -40,7 +35,7 @@ public class UserRealmRoleMappingMapper extends AbstractUserRoleMappingMapper {
 
     public static final String PROVIDER_ID = "oidc-usermodel-realm-role-mapper";
 
-    private static final List<ProviderConfigProperty> CONFIG_PROPERTIES = new ArrayList<ProviderConfigProperty>();
+    private static final List<ProviderConfigProperty> CONFIG_PROPERTIES = new ArrayList<>();
 
     static {
 
@@ -54,6 +49,7 @@ public class UserRealmRoleMappingMapper extends AbstractUserRoleMappingMapper {
         OIDCAttributeMapperHelper.addAttributeConfig(CONFIG_PROPERTIES, UserRealmRoleMappingMapper.class);
     }
 
+    @Override
     public List<ProviderConfigProperty> getConfigProperties() {
         return CONFIG_PROPERTIES;
     }
@@ -78,17 +74,12 @@ public class UserRealmRoleMappingMapper extends AbstractUserRoleMappingMapper {
         return "Map a user realm role to a token claim.";
     }
 
+    @Override
     protected void setClaim(IDToken token, ProtocolMapperModel mappingModel, UserSessionModel userSession) {
-
-        UserModel user = userSession.getUser();
-
         String rolePrefix = mappingModel.getConfig().get(ProtocolMapperUtils.USER_MODEL_REALM_ROLE_MAPPING_ROLE_PREFIX);
-        Set<String> realmRoleNames = flattenRoleModelToRoleNames(user.getRealmRoleMappings(), rolePrefix);
-
-        OIDCAttributeMapperHelper.mapClaim(token, mappingModel, realmRoleNames);
+        AbstractUserRoleMappingMapper.setClaim(token, mappingModel, userSession, role -> ! role.isClientRole(), rolePrefix);
     }
 
-
     public static ProtocolMapperModel create(String realmRolePrefix,
                                              String name,
                                              String tokenClaimName, boolean accessToken, boolean idToken) {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/group/GroupTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/group/GroupTest.java
index 7e61f51..1f0274e 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/group/GroupTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/group/GroupTest.java
@@ -438,7 +438,7 @@ public class GroupTest extends AbstractGroupTest {
 
         // List realm roles
         assertNames(roles.realmLevel().listAll(), "realm-role", "realm-composite");
-        assertNames(roles.realmLevel().listAvailable(), "admin", "offline_access", Constants.AUTHZ_UMA_AUTHORIZATION, "user", "customer-user-premium");
+        assertNames(roles.realmLevel().listAvailable(), "admin", "offline_access", Constants.AUTHZ_UMA_AUTHORIZATION, "user", "customer-user-premium", "realm-composite-role", "sample-realm-role");
         assertNames(roles.realmLevel().listEffective(), "realm-role", "realm-composite", "realm-child");
 
         // List client roles
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java
index 81af8c6..545fbff 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java
@@ -817,7 +817,7 @@ public class UserTest extends AbstractAdminTest {
 
         // List realm roles
         assertNames(roles.realmLevel().listAll(), "realm-role", "realm-composite", "user", "offline_access", Constants.AUTHZ_UMA_AUTHORIZATION);
-        assertNames(roles.realmLevel().listAvailable(), "admin", "customer-user-premium");
+        assertNames(roles.realmLevel().listAvailable(), "admin", "customer-user-premium", "realm-composite-role", "sample-realm-role");
         assertNames(roles.realmLevel().listEffective(), "realm-role", "realm-composite", "realm-child", "user", "offline_access", Constants.AUTHZ_UMA_AUTHORIZATION);
 
         // List client roles
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OIDCProtocolMappersTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OIDCProtocolMappersTest.java
index d317af7..ac86d20 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OIDCProtocolMappersTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OIDCProtocolMappersTest.java
@@ -43,10 +43,8 @@ import org.keycloak.testsuite.util.ClientManager;
 import org.keycloak.testsuite.util.OAuthClient;
 import org.keycloak.testsuite.util.ProtocolMapperUtil;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
+import static org.hamcrest.Matchers.*;
+import static org.junit.Assert.*;
 import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
 import static org.keycloak.testsuite.admin.ApiUtil.findClientByClientId;
 import static org.keycloak.testsuite.admin.ApiUtil.findClientResourceByClientId;
@@ -222,11 +220,152 @@ public class OIDCProtocolMappersTest extends AbstractKeycloakTest {
 
         // Verify attribute is filled
         Map<String, Object> roleMappings = (Map<String, Object>)idToken.getOtherClaims().get("roles-custom");
-        Assert.assertEquals(2, roleMappings.size());
+        Assert.assertThat(roleMappings.keySet(), containsInAnyOrder("realm", "test-app"));
         String realmRoleMappings = (String) roleMappings.get("realm");
         String testAppMappings = (String) roleMappings.get("test-app");
-        Assert.assertTrue(realmRoleMappings.contains("pref.user"));
-        Assert.assertEquals("[customer-user]", testAppMappings);
+        assertRoles(realmRoleMappings,
+          "pref.user",                      // from direct assignment in user definition
+          "pref.offline_access"             // from direct assignment in user definition
+        );
+        assertRoles(testAppMappings,
+          "customer-user"                   // from direct assignment in user definition
+        );
+    }
+
+
+    @Test
+    public void testUserGroupRoleToAttributeMappers() throws Exception {
+        // Add mapper for realm roles
+        String clientId = "test-app";
+        ProtocolMapperRepresentation realmMapper = ProtocolMapperUtil.createUserRealmRoleMappingMapper("pref.", "Realm roles mapper", "roles-custom.realm", true, true);
+        ProtocolMapperRepresentation clientMapper = ProtocolMapperUtil.createUserClientRoleMappingMapper(clientId, "ta.", "Client roles mapper", "roles-custom.test-app", true, true);
+
+        ProtocolMappersResource protocolMappers = ApiUtil.findClientResourceByClientId(adminClient.realm("test"), clientId).getProtocolMappers();
+        protocolMappers.createMapper(Arrays.asList(realmMapper, clientMapper));
+
+        // Login user
+        OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("password", "rich.roles@redhat.com", "password");
+        IDToken idToken = oauth.verifyIDToken(response.getIdToken());
+
+        // Verify attribute is filled
+        Map<String, Object> roleMappings = (Map<String, Object>)idToken.getOtherClaims().get("roles-custom");
+        Assert.assertThat(roleMappings.keySet(), containsInAnyOrder("realm", clientId));
+        String realmRoleMappings = (String) roleMappings.get("realm");
+        String testAppMappings = (String) roleMappings.get(clientId);
+        assertRoles(realmRoleMappings,
+          "pref.admin",                     // from direct assignment to /roleRichGroup/level2group
+          "pref.user",                      // from parent group of /roleRichGroup/level2group, i.e. from /roleRichGroup
+          "pref.customer-user-premium",     // from client role customer-admin-composite-role - realm role for test-app
+          "pref.realm-composite-role",      // from parent group of /roleRichGroup/level2group, i.e. from /roleRichGroup
+          "pref.sample-realm-role"          // from realm role realm-composite-role
+        );
+        assertRoles(testAppMappings,
+          "ta.customer-user",                  // from direct assignment to /roleRichGroup/level2group
+          "ta.customer-admin-composite-role",  // from direct assignment to /roleRichGroup/level2group
+          "ta.customer-admin",                 // from client role customer-admin-composite-role - client role for test-app
+          "ta.sample-client-role"              // from realm role realm-composite-role - client role for test-app
+        );
+    }
+
+    @Test
+    public void testUserGroupRoleToAttributeMappersNotScopedOtherApp() throws Exception {
+        String clientId = "test-app-authz";
+        ProtocolMapperRepresentation realmMapper = ProtocolMapperUtil.createUserRealmRoleMappingMapper("pref.", "Realm roles mapper", "roles-custom.realm", true, true);
+        ProtocolMapperRepresentation clientMapper = ProtocolMapperUtil.createUserClientRoleMappingMapper(clientId, null, "Client roles mapper", "roles-custom." + clientId, true, true);
+
+        ProtocolMappersResource protocolMappers = ApiUtil.findClientResourceByClientId(adminClient.realm("test"), clientId).getProtocolMappers();
+        protocolMappers.createMapper(Arrays.asList(realmMapper, clientMapper));
+
+        // Login user
+        ClientManager.realm(adminClient.realm("test")).clientId(clientId).directAccessGrant(true);
+        oauth.clientId(clientId);
+        OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("secret", "rich.roles@redhat.com", "password");
+        IDToken idToken = oauth.verifyIDToken(response.getIdToken());
+
+        // Verify attribute is filled
+        Map<String, Object> roleMappings = (Map<String, Object>)idToken.getOtherClaims().get("roles-custom");
+        Assert.assertThat(roleMappings.keySet(), containsInAnyOrder("realm", clientId));
+        String realmRoleMappings = (String) roleMappings.get("realm");
+        String testAppAuthzMappings = (String) roleMappings.get(clientId);
+        assertRoles(realmRoleMappings,
+          "pref.admin",                     // from direct assignment to /roleRichGroup/level2group
+          "pref.user",                      // from parent group of /roleRichGroup/level2group, i.e. from /roleRichGroup
+          "pref.customer-user-premium",     // from client role customer-admin-composite-role - realm role for test-app
+          "pref.realm-composite-role",      // from parent group of /roleRichGroup/level2group, i.e. from /roleRichGroup
+          "pref.sample-realm-role"          // from realm role realm-composite-role
+        );
+        assertRoles(testAppAuthzMappings);  // There is no client role defined for test-app-authz
+    }
+
+    @Test
+    public void testUserGroupRoleToAttributeMappersScoped() throws Exception {
+        String clientId = "test-app-scope";
+        ProtocolMapperRepresentation realmMapper = ProtocolMapperUtil.createUserRealmRoleMappingMapper("pref.", "Realm roles mapper", "roles-custom.realm", true, true);
+        ProtocolMapperRepresentation clientMapper = ProtocolMapperUtil.createUserClientRoleMappingMapper(clientId, null, "Client roles mapper", "roles-custom.test-app-scope", true, true);
+
+        ProtocolMappersResource protocolMappers = ApiUtil.findClientResourceByClientId(adminClient.realm("test"), clientId).getProtocolMappers();
+        protocolMappers.createMapper(Arrays.asList(realmMapper, clientMapper));
+
+        // Login user
+        ClientManager.realm(adminClient.realm("test")).clientId(clientId).directAccessGrant(true);
+        oauth.clientId(clientId);
+        OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("password", "rich.roles@redhat.com", "password");
+        IDToken idToken = oauth.verifyIDToken(response.getIdToken());
+
+        // Verify attribute is filled
+        Map<String, Object> roleMappings = (Map<String, Object>)idToken.getOtherClaims().get("roles-custom");
+        Assert.assertThat(roleMappings.keySet(), containsInAnyOrder("realm", clientId));
+        String realmRoleMappings = (String) roleMappings.get("realm");
+        String testAppScopeMappings = (String) roleMappings.get(clientId);
+        assertRoles(realmRoleMappings,
+          "pref.admin",                     // from direct assignment to /roleRichGroup/level2group
+          "pref.user"                       // from parent group of /roleRichGroup/level2group, i.e. from /roleRichGroup
+        );
+        assertRoles(testAppScopeMappings,
+          "test-app-allowed-by-scope"       // from direct assignment to roleRichUser, present as scope allows it
+        );
+    }
+
+    @Test
+    public void testUserGroupRoleToAttributeMappersScopedClientNotSet() throws Exception {
+        String clientId = "test-app-scope";
+        ProtocolMapperRepresentation realmMapper = ProtocolMapperUtil.createUserRealmRoleMappingMapper("pref.", "Realm roles mapper", "roles-custom.realm", true, true);
+        ProtocolMapperRepresentation clientMapper = ProtocolMapperUtil.createUserClientRoleMappingMapper(null, null, "Client roles mapper", "roles-custom.test-app-scope", true, true);
+
+        ProtocolMappersResource protocolMappers = ApiUtil.findClientResourceByClientId(adminClient.realm("test"), clientId).getProtocolMappers();
+        protocolMappers.createMapper(Arrays.asList(realmMapper, clientMapper));
+
+        // Login user
+        ClientManager.realm(adminClient.realm("test")).clientId(clientId).directAccessGrant(true);
+        oauth.clientId(clientId);
+        OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("password", "rich.roles@redhat.com", "password");
+        IDToken idToken = oauth.verifyIDToken(response.getIdToken());
+
+        // Verify attribute is filled
+        Map<String, Object> roleMappings = (Map<String, Object>)idToken.getOtherClaims().get("roles-custom");
+        Assert.assertThat(roleMappings.keySet(), containsInAnyOrder("realm", clientId));
+        String realmRoleMappings = (String) roleMappings.get("realm");
+        String testAppScopeMappings = (String) roleMappings.get(clientId);
+        assertRoles(realmRoleMappings,
+          "pref.admin",                     // from direct assignment to /roleRichGroup/level2group
+          "pref.user"                       // from parent group of /roleRichGroup/level2group, i.e. from /roleRichGroup
+        );
+        assertRoles(testAppScopeMappings,
+          "test-app-allowed-by-scope",      // from direct assignment to roleRichUser, present as scope allows it
+          "customer-admin-composite-role"   // from direct assignment to /roleRichGroup/level2group, present as scope allows it
+        );
+    }
+
+    private void assertRoles(String actualRoleString, String...expectedRoles) {
+        String[] roles;
+        Assert.assertThat(actualRoleString.matches("^\\[.*\\]$"), is(true));
+        roles = actualRoleString.substring(1, actualRoleString.length() - 1).split(",\\s*");
+
+        if (expectedRoles == null || expectedRoles.length == 0) {
+            Assert.assertThat(roles, arrayContainingInAnyOrder(""));
+        } else {
+            Assert.assertThat(roles, arrayContainingInAnyOrder(expectedRoles));
+        }
     }
 
 
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/testrealm.json b/testsuite/integration-arquillian/tests/base/src/test/resources/testrealm.json
index c0b2b6c..b0e8767 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/resources/testrealm.json
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/testrealm.json
@@ -85,6 +85,21 @@
       "groups": [
         "/topGroup/level2group"
       ]
+    },
+    {
+      "username" : "roleRichUser",
+      "enabled": true,
+      "email" : "rich.roles@redhat.com",
+      "credentials" : [
+        { "type" : "password",
+          "value" : "password" }
+      ],
+      "groups": [
+        "/roleRichGroup/level2group"
+      ],
+      "clientRoles": {
+        "test-app-scope": [ "test-app-allowed-by-scope", "test-app-disallowed-by-scope" ]
+      }
     }
   ],
   "scopeMappings": [
@@ -95,6 +110,10 @@
     {
       "client": "test-app",
       "roles": ["user"]
+    },
+    {
+      "client": "test-app-scope",
+      "roles": ["user", "admin"]
     }
   ],
   "clients": [
@@ -109,6 +128,16 @@
       "secret": "password"
     },
     {
+      "clientId" : "test-app-scope",
+      "enabled": true,
+
+      "redirectUris": [
+        "http://localhost:8180/auth/realms/master/app/*"
+      ],
+      "secret": "password",
+      "fullScopeAllowed": "false"
+    },
+    {
       "clientId" : "third-party",
       "enabled": true,
       "consentRequired": true,
@@ -290,6 +319,22 @@
       {
         "name": "customer-user-premium",
         "description": "Have User Premium privileges"
+      },
+      {
+        "name": "sample-realm-role",
+        "description": "Sample realm role"
+      },
+      {
+        "name": "realm-composite-role",
+        "description": "Realm composite role containing client role",
+        "composite" : true,
+        "composites" : {
+          "realm" : [ "sample-realm-role" ],
+          "client" : {
+            "test-app" : [ "sample-client-role" ],
+            "account" : [ "view-profile" ]
+          }
+        }
       }
     ],
     "client" : {
@@ -301,6 +346,31 @@
         {
           "name": "customer-admin",
           "description": "Have Customer Admin privileges"
+        },
+        {
+          "name": "sample-client-role",
+          "description": "Sample client role"
+        },
+        {
+          "name": "customer-admin-composite-role",
+          "description": "Have Customer Admin privileges via composite role",
+          "composite" : true,
+          "composites" : {
+            "realm" : [ "customer-user-premium" ],
+            "client" : {
+              "test-app" : [ "customer-admin" ]
+            }
+          }
+        }
+      ],
+      "test-app-scope" : [
+        {
+          "name": "test-app-allowed-by-scope",
+          "description": "Role allowed by scope in test-app-scope"
+        },
+        {
+          "name": "test-app-disallowed-by-scope",
+          "description": "Role disallowed by scope in test-app-scope"
         }
       ]
     }
@@ -328,6 +398,31 @@
           }
         }
       ]
+    },
+    {
+      "name": "roleRichGroup",
+      "attributes": {
+        "topAttribute": ["true"]
+
+      },
+      "realmRoles": ["user", "realm-composite-role"],
+      "clientRoles": {
+        "account": ["manage-account"]
+      },
+
+      "subGroups": [
+        {
+          "name": "level2group",
+          "realmRoles": ["admin"],
+          "clientRoles": {
+            "test-app": ["customer-user", "customer-admin-composite-role"]
+          },
+          "attributes": {
+            "level2Attribute": ["true"]
+
+          }
+        }
+      ]
     }
   ],
 
@@ -337,6 +432,16 @@
       {
         "client": "third-party",
         "roles": ["customer-user"]
+      },
+      {
+        "client": "test-app-scope",
+        "roles": ["customer-admin-composite-role"]
+      }
+    ],
+    "test-app-scope": [
+      {
+        "client": "test-app-scope",
+        "roles": ["test-app-allowed-by-scope"]
       }
     ]
   },