From 750e942267c6ad9fd0ad9d88333df49ac5e184a1 Mon Sep 17 00:00:00 2001 From: Hynek Mlnarik Date: Mon, 7 Nov 2016 15:10:00 +0100 Subject: [PATCH] KEYCLOAK-3469 Make role mappers account for user groups --- .../org/keycloak/models/RoleMapperModel.java | 17 ++ .../AbstractUserRoleMappingMapper.java | 105 +++++++++--- .../mappers/OIDCAttributeMapperHelper.java | 3 + .../mappers/UserClientRoleMappingMapper.java | 54 +++++-- .../mappers/UserRealmRoleMappingMapper.java | 17 +- .../keycloak/testsuite/admin/UserTest.java | 2 +- .../testsuite/admin/group/GroupTest.java | 2 +- .../oauth/OIDCProtocolMappersTest.java | 153 +++++++++++++++++- .../base/src/test/resources/testrealm.json | 105 ++++++++++++ 9 files changed, 401 insertions(+), 57 deletions(-) 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 85f1fd3dfa41..46c31988b4f4 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 @@ * @version $Revision: 1 $ */ public interface RoleMapperModel { + /** + * Returns set of realm roles that are directly set to this object. + * @return see description + */ Set 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 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 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 de4d0548b3e8..4de3720422fd 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 @@ abstract class AbstractUserRoleMappingMapper extends AbstractOIDCProtocolMapper implements OIDCAccessTokenMapper, OIDCIDTokenMapper, UserInfoTokenMapper { /** - * Returns the role names extracted from the given {@code roleModels} while recursively traversing "Composite Roles". - *

- * Optionally prefixes each role name with the given {@code prefix}. - *

- * - * @param roleModels - * @param prefix the prefix to apply, may be {@literal null} + * Returns a stream with roles that come from: + *
    + *
  • Direct assignment of the role to the user
  • + *
  • Direct assignment of the role to any group of the user or any of its parent group
  • + *
  • Composite roles are expanded recursively, the composite role itself is also contained in the returned stream
  • + *
+ * @param user User to enumerate the roles for + * @return + */ + public static Stream 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 flattenRoleModelToRoleNames(Set roleModels, String prefix) { + private static Stream groupAndItsParentsStream(GroupModel group) { + Stream.Builder sb = Stream.builder(); + while (group != null) { + sb.add(group); + group = group.getParent(); + } + return sb.build(); + } - Set 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 expandCompositeRolesStream(RoleModel role) { + Stream.Builder sb = Stream.builder(); - Deque stack = new ArrayDeque<>(roleModels); - while (!stack.isEmpty()) { + Deque 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 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 clientUserRoles = getAllUserRolesStream(user).filter(restriction); - roleNames.add(roleName); + boolean dontLimitScope = userSession.getClientSessions().stream().anyMatch(cs -> cs.getClient().isFullScopeAllowed()); + if (! dontLimitScope) { + Set clientRoles = userSession.getClientSessions().stream() + .flatMap(cs -> cs.getClient().getScopeMappings().stream()) + .collect(Collectors.toSet()); + + clientUserRoles = clientUserRoles.filter(clientRoles::contains); } - return roleNames; + Set 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 99b261074b50..8b64aef891ca 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 static void mapClaim(IDToken token, ProtocolMapperModel mappingModel, Obj if (attributeValue == null) return; String protocolClaim = mappingModel.getConfig().get(TOKEN_CLAIM_NAME); + if (protocolClaim == null) { + return; + } String[] split = protocolClaim.split("\\."); Map 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 01d47e13d7c9..5a88c2add8d3 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 CONFIG_PROPERTIES = new ArrayList(); + private static final List CONFIG_PROPERTIES = new ArrayList<>(); static { @@ -60,6 +63,7 @@ public class UserClientRoleMappingMapper extends AbstractUserRoleMappingMapper { OIDCAttributeMapperHelper.addAttributeConfig(CONFIG_PROPERTIES, UserClientRoleMappingMapper.class); } + @Override public List getConfigProperties() { return CONFIG_PROPERTIES; } @@ -84,23 +88,51 @@ public String getHelpText() { 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 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 clientRoleMappings = user.getClientRoleMappings(clientModel); + if (client == null) { + return RoleModel::isClientRole; + } - String rolePrefix = mappingModel.getConfig().get(ProtocolMapperUtils.USER_MODEL_CLIENT_ROLE_MAPPING_ROLE_PREFIX); - Set 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 clientRoleMappings = client.getRoles(); + if (fullScopeAllowed) { + return clientRoleMappings::contains; + } + + Set scopeMappings = new HashSet<>(); + + if (useTemplateScope) { + Set templateScopeMappings = template.getScopeMappings(); + if (templateScopeMappings != null) { + scopeMappings.addAll(templateScopeMappings); + } } - } + Set 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 ef9818227b09..f978b0815150 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 CONFIG_PROPERTIES = new ArrayList(); + private static final List CONFIG_PROPERTIES = new ArrayList<>(); static { @@ -54,6 +49,7 @@ public class UserRealmRoleMappingMapper extends AbstractUserRoleMappingMapper { OIDCAttributeMapperHelper.addAttributeConfig(CONFIG_PROPERTIES, UserRealmRoleMappingMapper.class); } + @Override public List getConfigProperties() { return CONFIG_PROPERTIES; } @@ -78,17 +74,12 @@ public String getHelpText() { 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 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/UserTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java index 81af8c684316..545fbff0f10f 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 void roleMappings() { // 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/admin/group/GroupTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/group/GroupTest.java index 7e61f511db00..1f0274e9cba0 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 void roleMappings() { // 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/oauth/OIDCProtocolMappersTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OIDCProtocolMappersTest.java index d317af7de10d..ac86d2023a0f 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.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 void testUserRoleToAttributeMappers() throws Exception { // Verify attribute is filled Map roleMappings = (Map)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 roleMappings = (Map)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 roleMappings = (Map)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 roleMappings = (Map)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 roleMappings = (Map)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 c0b2b6c6dce2..b0e87679b3f2 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": [ @@ -108,6 +127,16 @@ "adminUrl": "http://localhost:8180/auth/realms/master/app/admin", "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, @@ -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" } ] } @@ -325,6 +395,31 @@ "attributes": { "level2Attribute": ["true"] + } + } + ] + }, + { + "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"] } ] },