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 417d41b95d0b..c4025739cdf5 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 @@ -54,6 +54,7 @@ import java.security.PublicKey; import java.security.SecureRandom; import java.security.cert.X509Certificate; +import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; @@ -409,14 +410,25 @@ public static List resolveAttribute(GroupModel group, String name) { } - public static List resolveAttribute(UserModel user, String name) { + public static Collection resolveAttribute(UserModel user, String name, boolean aggregateAttrs) { List values = user.getAttribute(name); - if (!values.isEmpty()) return values; + Set aggrValues = new HashSet(); + if (!values.isEmpty()) { + if (!aggregateAttrs) { + return values; + } + aggrValues.addAll(values); + } for (GroupModel group : user.getGroups()) { values = resolveAttribute(group, name); - if (values != null) return values; + if (values != null && !values.isEmpty()) { + if (!aggregateAttrs) { + return values; + } + aggrValues.addAll(values); + } } - return Collections.emptyList(); + return aggrValues; } diff --git a/services/src/main/java/org/keycloak/protocol/ProtocolMapperUtils.java b/services/src/main/java/org/keycloak/protocol/ProtocolMapperUtils.java index 4db53938f356..fd1133ebfedc 100755 --- a/services/src/main/java/org/keycloak/protocol/ProtocolMapperUtils.java +++ b/services/src/main/java/org/keycloak/protocol/ProtocolMapperUtils.java @@ -44,6 +44,7 @@ public class ProtocolMapperUtils { public static final String USER_ATTRIBUTE = "user.attribute"; public static final String USER_SESSION_NOTE = "user.session.note"; public static final String MULTIVALUED = "multivalued"; + public static final String AGGREGATE_ATTRS = "aggregate.attrs"; public static final String USER_MODEL_PROPERTY_LABEL = "usermodel.prop.label"; public static final String USER_MODEL_PROPERTY_HELP_TEXT = "usermodel.prop.tooltip"; public static final String USER_MODEL_ATTRIBUTE_LABEL = "usermodel.attr.label"; @@ -64,7 +65,9 @@ public class ProtocolMapperUtils { public static final String USER_SESSION_MODEL_NOTE_LABEL = "userSession.modelNote.label"; public static final String USER_SESSION_MODEL_NOTE_HELP_TEXT = "userSession.modelNote.tooltip"; public static final String MULTIVALUED_LABEL = "multivalued.label"; + public static final String AGGREGATE_ATTRS_LABEL = "aggregate.attrs.label"; public static final String MULTIVALUED_HELP_TEXT = "multivalued.tooltip"; + public static final String AGGREGATE_ATTRS_HELP_TEXT = "aggregate.attrs.tooltip"; // Role name mapper can move some roles to different positions public static final int PRIORITY_ROLE_NAMES_MAPPER = 10; diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserAttributeMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserAttributeMapper.java index 12a1c1cb4d2f..920059bf01fb 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserAttributeMapper.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserAttributeMapper.java @@ -26,6 +26,7 @@ import org.keycloak.representations.IDToken; import java.util.ArrayList; +import java.util.Collection; import java.util.List; /** @@ -57,6 +58,12 @@ public class UserAttributeMapper extends AbstractOIDCProtocolMapper implements O property.setType(ProviderConfigProperty.BOOLEAN_TYPE); configProperties.add(property); + property = new ProviderConfigProperty(); + property.setName(ProtocolMapperUtils.AGGREGATE_ATTRS); + property.setLabel(ProtocolMapperUtils.AGGREGATE_ATTRS_LABEL); + property.setHelpText(ProtocolMapperUtils.AGGREGATE_ATTRS_HELP_TEXT); + property.setType(ProviderConfigProperty.BOOLEAN_TYPE); + configProperties.add(property); } public static final String PROVIDER_ID = "oidc-usermodel-attribute-mapper"; @@ -90,7 +97,8 @@ protected void setClaim(IDToken token, ProtocolMapperModel mappingModel, UserSes UserModel user = userSession.getUser(); String attributeName = mappingModel.getConfig().get(ProtocolMapperUtils.USER_ATTRIBUTE); - List attributeValue = KeycloakModelUtils.resolveAttribute(user, attributeName); + boolean aggregateAttrs = Boolean.valueOf(mappingModel.getConfig().get(ProtocolMapperUtils.AGGREGATE_ATTRS)); + Collection attributeValue = KeycloakModelUtils.resolveAttribute(user, attributeName, aggregateAttrs); if (attributeValue == null) return; OIDCAttributeMapperHelper.mapClaim(token, mappingModel, attributeValue); } @@ -99,6 +107,15 @@ public static ProtocolMapperModel createClaimMapper(String name, String userAttribute, String tokenClaimName, String claimType, boolean accessToken, boolean idToken, boolean multivalued) { + return createClaimMapper(name, userAttribute, tokenClaimName, claimType, + accessToken, idToken, multivalued, false); + } + + public static ProtocolMapperModel createClaimMapper(String name, + String userAttribute, + String tokenClaimName, String claimType, + boolean accessToken, boolean idToken, + boolean multivalued, boolean aggregateAttrs) { ProtocolMapperModel mapper = OIDCAttributeMapperHelper.createClaimMapper(name, userAttribute, tokenClaimName, claimType, accessToken, idToken, @@ -107,6 +124,9 @@ public static ProtocolMapperModel createClaimMapper(String name, if (multivalued) { mapper.getConfig().put(ProtocolMapperUtils.MULTIVALUED, "true"); } + if (aggregateAttrs) { + mapper.getConfig().put(ProtocolMapperUtils.AGGREGATE_ATTRS, "true"); + } return mapper; } diff --git a/services/src/main/java/org/keycloak/protocol/saml/mappers/AttributeStatementHelper.java b/services/src/main/java/org/keycloak/protocol/saml/mappers/AttributeStatementHelper.java index d61966e00dc2..7b97b9aacf23 100755 --- a/services/src/main/java/org/keycloak/protocol/saml/mappers/AttributeStatementHelper.java +++ b/services/src/main/java/org/keycloak/protocol/saml/mappers/AttributeStatementHelper.java @@ -26,6 +26,7 @@ import org.keycloak.saml.common.constants.JBossSAMLURIConstants; import java.util.ArrayList; +import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -53,7 +54,7 @@ public static void addAttribute(AttributeStatementType attributeStatement, Proto } public static void addAttributes(AttributeStatementType attributeStatement, ProtocolMapperModel mappingModel, - List attributeValues) { + Collection attributeValues) { AttributeType attribute = createAttributeType(mappingModel); attributeValues.forEach(attribute::addAttributeValue); diff --git a/services/src/main/java/org/keycloak/protocol/saml/mappers/UserAttributeStatementMapper.java b/services/src/main/java/org/keycloak/protocol/saml/mappers/UserAttributeStatementMapper.java index ab8a5e490961..0eb92e2a4b46 100755 --- a/services/src/main/java/org/keycloak/protocol/saml/mappers/UserAttributeStatementMapper.java +++ b/services/src/main/java/org/keycloak/protocol/saml/mappers/UserAttributeStatementMapper.java @@ -27,6 +27,7 @@ import org.keycloak.protocol.ProtocolMapperUtils; import org.keycloak.provider.ProviderConfigProperty; +import java.util.Collection; import java.util.ArrayList; import java.util.List; @@ -48,6 +49,12 @@ public class UserAttributeStatementMapper extends AbstractSAMLProtocolMapper imp configProperties.add(property); AttributeStatementHelper.setConfigProperties(configProperties); + property = new ProviderConfigProperty(); + property.setName(ProtocolMapperUtils.AGGREGATE_ATTRS); + property.setLabel(ProtocolMapperUtils.AGGREGATE_ATTRS_LABEL); + property.setHelpText(ProtocolMapperUtils.AGGREGATE_ATTRS_HELP_TEXT); + property.setType(ProviderConfigProperty.BOOLEAN_TYPE); + configProperties.add(property); } public static final String PROVIDER_ID = "saml-user-attribute-mapper"; @@ -80,7 +87,8 @@ public String getHelpText() { public void transformAttributeStatement(AttributeStatementType attributeStatement, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) { UserModel user = userSession.getUser(); String attributeName = mappingModel.getConfig().get(ProtocolMapperUtils.USER_ATTRIBUTE); - List attributeValues = KeycloakModelUtils.resolveAttribute(user, attributeName); + boolean aggregateAttrs = Boolean.valueOf(mappingModel.getConfig().get(ProtocolMapperUtils.AGGREGATE_ATTRS)); + Collection attributeValues = KeycloakModelUtils.resolveAttribute(user, attributeName, aggregateAttrs); if (attributeValues.isEmpty()) return; AttributeStatementHelper.addAttributes(attributeStatement, mappingModel, attributeValues); } diff --git a/testsuite/integration-arquillian/test-apps/servlets/src/main/java/org/keycloak/testsuite/adapter/servlet/SendUsernameServlet.java b/testsuite/integration-arquillian/test-apps/servlets/src/main/java/org/keycloak/testsuite/adapter/servlet/SendUsernameServlet.java index e7955266b69a..d4a7f641564c 100755 --- a/testsuite/integration-arquillian/test-apps/servlets/src/main/java/org/keycloak/testsuite/adapter/servlet/SendUsernameServlet.java +++ b/testsuite/integration-arquillian/test-apps/servlets/src/main/java/org/keycloak/testsuite/adapter/servlet/SendUsernameServlet.java @@ -202,6 +202,10 @@ private String getAttributes() { for (String attr : principal.getAttributes("hardcoded-attribute")) { output += attr + ","; } + output += "
group-attribute: "; + for (String attr : principal.getAttributes("group-attribute")) { + output += attr + ","; + } return output; } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/SAMLServletAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/SAMLServletAdapterTest.java index 961121722d4f..47a2b9b500d0 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/SAMLServletAdapterTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/SAMLServletAdapterTest.java @@ -43,6 +43,9 @@ import java.net.URL; import java.security.KeyPair; import java.security.PublicKey; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; @@ -100,6 +103,7 @@ import org.keycloak.admin.client.resource.ClientResource; import org.keycloak.admin.client.resource.ProtocolMappersResource; import org.keycloak.admin.client.resource.RoleScopeResource; +import org.keycloak.admin.client.resource.UserResource; import org.keycloak.common.util.Base64; import org.keycloak.common.util.KeyUtils; import org.keycloak.common.util.PemUtils; @@ -116,6 +120,7 @@ import org.keycloak.protocol.saml.mappers.RoleListMapper; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ComponentRepresentation; +import org.keycloak.representations.idm.GroupRepresentation; import org.keycloak.representations.idm.ProtocolMapperRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RoleRepresentation; @@ -140,6 +145,7 @@ import org.keycloak.testsuite.page.AbstractPage; import org.keycloak.testsuite.saml.AbstractSamlTest; import org.keycloak.testsuite.updaters.ClientAttributeUpdater; +import org.keycloak.testsuite.util.ProtocolMapperUtil; import org.keycloak.testsuite.util.SamlClient; import org.keycloak.testsuite.util.SamlClient.Binding; import org.keycloak.testsuite.util.SamlClientBuilder; @@ -1158,6 +1164,233 @@ public void testRelayStateEncoding() throws Exception { Assert.assertThat(pageSource, not(containsString("SAML response: null"))); } + private static List parseCommaSeparatedAttributes(String body, String attribute) { + int start = body.indexOf(attribute) + attribute.length(); + if (start == -1) { + return Collections.emptyList(); + } + int end = body.indexOf(System.getProperty("line.separator"), start); + if (end == -1) { + end = body.length(); + } + String values = body.substring(start, end); + String[] parts = values.split(","); + return Arrays.asList(parts); + } + + @Test + public void testUserAttributeStatementMapperUserGroupsAggregate() throws Exception { + UserResource userResource = ApiUtil.findUserByUsernameId(testRealmResource(), "bburke"); + UserRepresentation user = userResource.toRepresentation(); + user.setAttributes(new HashMap<>()); + user.getAttributes().put("group-value", Arrays.asList("user-value1")); + userResource.update(user); + GroupRepresentation group1 = new GroupRepresentation(); + group1.setName("group1"); + group1.setAttributes(new HashMap<>()); + group1.getAttributes().put("group-value", Arrays.asList("value1", "value2")); + testRealmResource().groups().add(group1); + group1 = testRealmResource().getGroupByPath("/group1"); + userResource.joinGroup(group1.getId()); + + ClientResource clientResource = ApiUtil.findClientResourceByClientId(testRealmResource(), AbstractSamlTest.SAML_CLIENT_ID_EMPLOYEE_2); + ProtocolMappersResource protocolMappersResource = clientResource.getProtocolMappers(); + + Map config = new LinkedHashMap<>(); + config.put("attribute.nameformat", "Basic"); + config.put("user.attribute", "group-value"); + config.put("attribute.name", "group-attribute"); + config.put("aggregate.attrs", "true"); + createProtocolMapper(protocolMappersResource, "group-value", "saml", "saml-user-attribute-mapper", config); + + try { + employee2ServletPage.navigateTo(); + assertCurrentUrlStartsWith(testRealmSAMLPostLoginPage); + testRealmSAMLPostLoginPage.form().login("bburke", "password"); + + driver.navigate().to(employee2ServletPage.toString() + "/getAttributes"); + waitForPageToLoad(); + + String body = driver.findElement(By.xpath("//body")).getText(); + List values = parseCommaSeparatedAttributes(body, " group-attribute: "); + Assert.assertEquals(3, values.size()); + Assert.assertTrue(values.contains("user-value1")); + Assert.assertTrue(values.contains("value1")); + Assert.assertTrue(values.contains("value2")); + + employee2ServletPage.logout(); + checkLoggedOut(employee2ServletPage, testRealmSAMLPostLoginPage); + } finally { + // revert + user.getAttributes().remove("group-value"); + userResource.update(user); + userResource.leaveGroup(group1.getId()); + testRealmResource().groups().group(group1.getId()).remove(); + ProtocolMapperRepresentation mapper = ProtocolMapperUtil.getMapperByNameAndProtocol(protocolMappersResource, "saml", "group-value"); + protocolMappersResource.delete(mapper.getId()); + } + } + + @Test + public void testUserAttributeStatementMapperUserGroupsNoAggregate() throws Exception { + UserResource userResource = ApiUtil.findUserByUsernameId(testRealmResource(), "bburke"); + UserRepresentation user = userResource.toRepresentation(); + user.setAttributes(new HashMap<>()); + user.getAttributes().put("group-value", Arrays.asList("user-value1")); + userResource.update(user); + GroupRepresentation group1 = new GroupRepresentation(); + group1.setName("group1"); + group1.setAttributes(new HashMap<>()); + group1.getAttributes().put("group-value", Arrays.asList("value1", "value2")); + testRealmResource().groups().add(group1); + group1 = testRealmResource().getGroupByPath("/group1"); + userResource.joinGroup(group1.getId()); + + ClientResource clientResource = ApiUtil.findClientResourceByClientId(testRealmResource(), AbstractSamlTest.SAML_CLIENT_ID_EMPLOYEE_2); + ProtocolMappersResource protocolMappersResource = clientResource.getProtocolMappers(); + + Map config = new LinkedHashMap<>(); + config.put("attribute.nameformat", "Basic"); + config.put("user.attribute", "group-value"); + config.put("attribute.name", "group-attribute"); + createProtocolMapper(protocolMappersResource, "group-value", "saml", "saml-user-attribute-mapper", config); + + try { + employee2ServletPage.navigateTo(); + assertCurrentUrlStartsWith(testRealmSAMLPostLoginPage); + testRealmSAMLPostLoginPage.form().login("bburke", "password"); + + driver.navigate().to(employee2ServletPage.toString() + "/getAttributes"); + waitForPageToLoad(); + + String body = driver.findElement(By.xpath("//body")).getText(); + List values = parseCommaSeparatedAttributes(body, " group-attribute: "); + Assert.assertEquals(1, values.size()); + Assert.assertTrue(values.contains("user-value1")); + + employee2ServletPage.logout(); + checkLoggedOut(employee2ServletPage, testRealmSAMLPostLoginPage); + } finally { + // revert + user.getAttributes().remove("group-value"); + userResource.update(user); + userResource.leaveGroup(group1.getId()); + testRealmResource().groups().group(group1.getId()).remove(); + ProtocolMapperRepresentation mapper = ProtocolMapperUtil.getMapperByNameAndProtocol(protocolMappersResource, "saml", "group-value"); + protocolMappersResource.delete(mapper.getId()); + } + } + + @Test + public void testUserAttributeStatementMapperGroupsAggregate() throws Exception { + UserResource userResource = ApiUtil.findUserByUsernameId(testRealmResource(), "bburke"); + GroupRepresentation group1 = new GroupRepresentation(); + group1.setName("group1"); + group1.setAttributes(new HashMap<>()); + group1.getAttributes().put("group-value", Arrays.asList("value1", "value2")); + testRealmResource().groups().add(group1); + group1 = testRealmResource().getGroupByPath("/group1"); + userResource.joinGroup(group1.getId()); + GroupRepresentation group2 = new GroupRepresentation(); + group2.setName("group2"); + group2.setAttributes(new HashMap<>()); + group2.getAttributes().put("group-value", Arrays.asList("value2", "value3")); + testRealmResource().groups().add(group2); + group2 = testRealmResource().getGroupByPath("/group2"); + userResource.joinGroup(group2.getId()); + + ClientResource clientResource = ApiUtil.findClientResourceByClientId(testRealmResource(), AbstractSamlTest.SAML_CLIENT_ID_EMPLOYEE_2); + ProtocolMappersResource protocolMappersResource = clientResource.getProtocolMappers(); + + Map config = new LinkedHashMap<>(); + config.put("attribute.nameformat", "Basic"); + config.put("user.attribute", "group-value"); + config.put("attribute.name", "group-attribute"); + config.put("aggregate.attrs", "true"); + createProtocolMapper(protocolMappersResource, "group-value", "saml", "saml-user-attribute-mapper", config); + + try { + employee2ServletPage.navigateTo(); + assertCurrentUrlStartsWith(testRealmSAMLPostLoginPage); + testRealmSAMLPostLoginPage.form().login("bburke", "password"); + + driver.navigate().to(employee2ServletPage.toString() + "/getAttributes"); + waitForPageToLoad(); + + String body = driver.findElement(By.xpath("//body")).getText(); + List values = parseCommaSeparatedAttributes(body, " group-attribute: "); + Assert.assertEquals(3, values.size()); + Assert.assertTrue(values.contains("value1")); + Assert.assertTrue(values.contains("value2")); + Assert.assertTrue(values.contains("value3")); + + employee2ServletPage.logout(); + checkLoggedOut(employee2ServletPage, testRealmSAMLPostLoginPage); + } finally { + // revert + userResource.leaveGroup(group1.getId()); + testRealmResource().groups().group(group1.getId()).remove(); + userResource.leaveGroup(group2.getId()); + testRealmResource().groups().group(group2.getId()).remove(); + ProtocolMapperRepresentation mapper = ProtocolMapperUtil.getMapperByNameAndProtocol(protocolMappersResource, "saml", "group-value"); + protocolMappersResource.delete(mapper.getId()); + } + } + + @Test + public void testUserAttributeStatementMapperGroupsNoAggregate() throws Exception { + UserResource userResource = ApiUtil.findUserByUsernameId(testRealmResource(), "bburke"); + GroupRepresentation group1 = new GroupRepresentation(); + group1.setName("group1"); + group1.setAttributes(new HashMap<>()); + group1.getAttributes().put("group-value", Arrays.asList("value1", "value2")); + testRealmResource().groups().add(group1); + group1 = testRealmResource().getGroupByPath("/group1"); + userResource.joinGroup(group1.getId()); + GroupRepresentation group2 = new GroupRepresentation(); + group2.setName("group2"); + group2.setAttributes(new HashMap<>()); + group2.getAttributes().put("group-value", Arrays.asList("value2", "value3")); + testRealmResource().groups().add(group2); + group2 = testRealmResource().getGroupByPath("/group2"); + userResource.joinGroup(group2.getId()); + + ClientResource clientResource = ApiUtil.findClientResourceByClientId(testRealmResource(), AbstractSamlTest.SAML_CLIENT_ID_EMPLOYEE_2); + ProtocolMappersResource protocolMappersResource = clientResource.getProtocolMappers(); + + Map config = new LinkedHashMap<>(); + config.put("attribute.nameformat", "Basic"); + config.put("user.attribute", "group-value"); + config.put("attribute.name", "group-attribute"); + createProtocolMapper(protocolMappersResource, "group-value", "saml", "saml-user-attribute-mapper", config); + + try { + employee2ServletPage.navigateTo(); + assertCurrentUrlStartsWith(testRealmSAMLPostLoginPage); + testRealmSAMLPostLoginPage.form().login("bburke", "password"); + + driver.navigate().to(employee2ServletPage.toString() + "/getAttributes"); + waitForPageToLoad(); + + String body = driver.findElement(By.xpath("//body")).getText(); + List values = parseCommaSeparatedAttributes(body, " group-attribute: "); + Assert.assertEquals(2, values.size()); + Assert.assertTrue((values.contains("value1") && values.contains("value2")) + || (values.contains("value2") && values.contains("value3"))); + + employee2ServletPage.logout(); + checkLoggedOut(employee2ServletPage, testRealmSAMLPostLoginPage); + } finally { + // revert + userResource.leaveGroup(group1.getId()); + testRealmResource().groups().group(group1.getId()).remove(); + userResource.leaveGroup(group2.getId()); + testRealmResource().groups().group(group2.getId()).remove(); + ProtocolMapperRepresentation mapper = ProtocolMapperUtil.getMapperByNameAndProtocol(protocolMappersResource, "saml", "group-value"); + protocolMappersResource.delete(mapper.getId()); + } + } + @Test public void testAttributes() throws Exception { ClientResource clientResource = ApiUtil.findClientResourceByClientId(testRealmResource(), AbstractSamlTest.SAML_CLIENT_ID_EMPLOYEE_2); 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 25efa56c1c75..27b2d4778f4e 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 @@ -54,6 +54,7 @@ import java.nio.charset.StandardCharsets; import java.util.Arrays; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -117,6 +118,11 @@ private void deleteMappers(ProtocolMappersResource protocolMappers) { if (mapper != null) { protocolMappers.delete(mapper.getId()); } + + mapper = ProtocolMapperUtil.getMapperByNameAndProtocol(protocolMappers, OIDCLoginProtocol.LOGIN_PROTOCOL, "group-value"); + if (mapper != null) { + protocolMappers.delete(mapper.getId()); + } } @Override @@ -734,6 +740,374 @@ public void testUserGroupRoleToAttributeMappersScopedClientNotSet() throws Excep deleteMappers(protocolMappers); } + @Test + public void testGroupAttributeUserOneGroupNoMultivalueNoAggregate() throws Exception { + // get the user + UserResource userResource = findUserByUsernameId(adminClient.realm("test"), "test-user@localhost"); + UserRepresentation user = userResource.toRepresentation(); + user.setAttributes(new HashMap<>()); + user.getAttributes().put("group-value", Arrays.asList("user-value1", "user-value2")); + userResource.update(user); + // create a group1 with two values + GroupRepresentation group1 = new GroupRepresentation(); + group1.setName("group1"); + group1.setAttributes(new HashMap<>()); + group1.getAttributes().put("group-value", Arrays.asList("value1", "value2")); + adminClient.realm("test").groups().add(group1); + group1 = adminClient.realm("test").getGroupByPath("/group1"); + userResource.joinGroup(group1.getId()); + // create the attribute mapper + ProtocolMappersResource protocolMappers = findClientResourceByClientId(adminClient.realm("test"), "test-app").getProtocolMappers(); + protocolMappers.createMapper(createClaimMapper("group-value", "group-value", "group-value", "String", true, true, false, false)).close(); + + try { + // test it + OAuthClient.AccessTokenResponse response = browserLogin("password", "test-user@localhost", "password"); + + IDToken idToken = oauth.verifyIDToken(response.getIdToken()); + assertNotNull(idToken.getOtherClaims()); + assertNotNull(idToken.getOtherClaims().get("group-value")); + assertTrue(idToken.getOtherClaims().get("group-value") instanceof String); + assertTrue("user-value1".equals(idToken.getOtherClaims().get("group-value")) || + "user-value2".equals(idToken.getOtherClaims().get("group-value"))); + } finally { + // revert + user.getAttributes().remove("group-value"); + userResource.update(user); + userResource.leaveGroup(group1.getId()); + adminClient.realm("test").groups().group(group1.getId()).remove(); + deleteMappers(protocolMappers); + } + } + + @Test + public void testGroupAttributeUserOneGroupMultivalueNoAggregate() throws Exception { + // get the user + UserResource userResource = findUserByUsernameId(adminClient.realm("test"), "test-user@localhost"); + UserRepresentation user = userResource.toRepresentation(); + user.setAttributes(new HashMap<>()); + user.getAttributes().put("group-value", Arrays.asList("user-value1", "user-value2")); + userResource.update(user); + // create a group1 with two values + GroupRepresentation group1 = new GroupRepresentation(); + group1.setName("group1"); + group1.setAttributes(new HashMap<>()); + group1.getAttributes().put("group-value", Arrays.asList("value1", "value2")); + adminClient.realm("test").groups().add(group1); + group1 = adminClient.realm("test").getGroupByPath("/group1"); + userResource.joinGroup(group1.getId()); + // create the attribute mapper + ProtocolMappersResource protocolMappers = findClientResourceByClientId(adminClient.realm("test"), "test-app").getProtocolMappers(); + protocolMappers.createMapper(createClaimMapper("group-value", "group-value", "group-value", "String", true, true, true, false)).close(); + + try { + // test it + OAuthClient.AccessTokenResponse response = browserLogin("password", "test-user@localhost", "password"); + + IDToken idToken = oauth.verifyIDToken(response.getIdToken()); + assertNotNull(idToken.getOtherClaims()); + assertNotNull(idToken.getOtherClaims().get("group-value")); + assertTrue(idToken.getOtherClaims().get("group-value") instanceof List); + assertEquals(2, ((List) idToken.getOtherClaims().get("group-value")).size()); + assertTrue(((List) idToken.getOtherClaims().get("group-value")).contains("user-value1")); + assertTrue(((List) idToken.getOtherClaims().get("group-value")).contains("user-value2")); + } finally { + // revert + user.getAttributes().remove("group-value"); + userResource.update(user); + userResource.leaveGroup(group1.getId()); + adminClient.realm("test").groups().group(group1.getId()).remove(); + deleteMappers(protocolMappers); + } + } + + @Test + public void testGroupAttributeUserOneGroupMultivalueAggregate() throws Exception { + // get the user + UserResource userResource = findUserByUsernameId(adminClient.realm("test"), "test-user@localhost"); + UserRepresentation user = userResource.toRepresentation(); + user.setAttributes(new HashMap<>()); + user.getAttributes().put("group-value", Arrays.asList("user-value1", "user-value2")); + userResource.update(user); + // create a group1 with two values + GroupRepresentation group1 = new GroupRepresentation(); + group1.setName("group1"); + group1.setAttributes(new HashMap<>()); + group1.getAttributes().put("group-value", Arrays.asList("value1", "value2")); + adminClient.realm("test").groups().add(group1); + group1 = adminClient.realm("test").getGroupByPath("/group1"); + userResource.joinGroup(group1.getId()); + // create the attribute mapper + ProtocolMappersResource protocolMappers = findClientResourceByClientId(adminClient.realm("test"), "test-app").getProtocolMappers(); + protocolMappers.createMapper(createClaimMapper("group-value", "group-value", "group-value", "String", true, true, true, true)).close(); + + try { + // test it + OAuthClient.AccessTokenResponse response = browserLogin("password", "test-user@localhost", "password"); + + IDToken idToken = oauth.verifyIDToken(response.getIdToken()); + assertNotNull(idToken.getOtherClaims()); + assertNotNull(idToken.getOtherClaims().get("group-value")); + assertTrue(idToken.getOtherClaims().get("group-value") instanceof List); + assertEquals(4, ((List) idToken.getOtherClaims().get("group-value")).size()); + assertTrue(((List) idToken.getOtherClaims().get("group-value")).contains("user-value1")); + assertTrue(((List) idToken.getOtherClaims().get("group-value")).contains("user-value2")); + assertTrue(((List) idToken.getOtherClaims().get("group-value")).contains("value1")); + assertTrue(((List) idToken.getOtherClaims().get("group-value")).contains("value2")); + } finally { + // revert + user.getAttributes().remove("group-value"); + userResource.update(user); + userResource.leaveGroup(group1.getId()); + adminClient.realm("test").groups().group(group1.getId()).remove(); + deleteMappers(protocolMappers); + } + } + + @Test + public void testGroupAttributeOneGroupNoMultivalueNoAggregate() throws Exception { + // get the user + UserResource userResource = findUserByUsernameId(adminClient.realm("test"), "test-user@localhost"); + // create a group1 with two values + GroupRepresentation group1 = new GroupRepresentation(); + group1.setName("group1"); + group1.setAttributes(new HashMap<>()); + group1.getAttributes().put("group-value", Arrays.asList("value1", "value2")); + adminClient.realm("test").groups().add(group1); + group1 = adminClient.realm("test").getGroupByPath("/group1"); + userResource.joinGroup(group1.getId()); + // create the attribute mapper + ProtocolMappersResource protocolMappers = findClientResourceByClientId(adminClient.realm("test"), "test-app").getProtocolMappers(); + protocolMappers.createMapper(createClaimMapper("group-value", "group-value", "group-value", "String", true, true, false, false)).close(); + + try { + // test it + OAuthClient.AccessTokenResponse response = browserLogin("password", "test-user@localhost", "password"); + + IDToken idToken = oauth.verifyIDToken(response.getIdToken()); + assertNotNull(idToken.getOtherClaims()); + assertNotNull(idToken.getOtherClaims().get("group-value")); + assertTrue(idToken.getOtherClaims().get("group-value") instanceof String); + assertTrue("value1".equals(idToken.getOtherClaims().get("group-value")) + || "value2".equals(idToken.getOtherClaims().get("group-value"))); + } finally { + // revert + userResource.leaveGroup(group1.getId()); + adminClient.realm("test").groups().group(group1.getId()).remove(); + deleteMappers(protocolMappers); + } + } + + @Test + public void testGroupAttributeOneGroupMultiValueNoAggregate() throws Exception { + // get the user + UserResource userResource = findUserByUsernameId(adminClient.realm("test"), "test-user@localhost"); + // create a group1 with two values + GroupRepresentation group1 = new GroupRepresentation(); + group1.setName("group1"); + group1.setAttributes(new HashMap<>()); + group1.getAttributes().put("group-value", Arrays.asList("value1", "value2")); + adminClient.realm("test").groups().add(group1); + group1 = adminClient.realm("test").getGroupByPath("/group1"); + userResource.joinGroup(group1.getId()); + + // create the attribute mapper + ProtocolMappersResource protocolMappers = findClientResourceByClientId(adminClient.realm("test"), "test-app").getProtocolMappers(); + protocolMappers.createMapper(createClaimMapper("group-value", "group-value", "group-value", "String", true, true, true, false)).close(); + + try { + // test it + OAuthClient.AccessTokenResponse response = browserLogin("password", "test-user@localhost", "password"); + + IDToken idToken = oauth.verifyIDToken(response.getIdToken()); + assertNotNull(idToken.getOtherClaims()); + assertNotNull(idToken.getOtherClaims().get("group-value")); + assertTrue(idToken.getOtherClaims().get("group-value") instanceof List); + assertEquals(2, ((List) idToken.getOtherClaims().get("group-value")).size()); + assertTrue(((List) idToken.getOtherClaims().get("group-value")).contains("value1")); + assertTrue(((List) idToken.getOtherClaims().get("group-value")).contains("value2")); + } finally { + // revert + userResource.leaveGroup(group1.getId()); + adminClient.realm("test").groups().group(group1.getId()).remove(); + deleteMappers(protocolMappers); + } + } + + @Test + public void testGroupAttributeOneGroupMultiValueAggregate() throws Exception { + // get the user + UserResource userResource = findUserByUsernameId(adminClient.realm("test"), "test-user@localhost"); + // create a group1 with two values + GroupRepresentation group1 = new GroupRepresentation(); + group1.setName("group1"); + group1.setAttributes(new HashMap<>()); + group1.getAttributes().put("group-value", Arrays.asList("value1", "value2")); + adminClient.realm("test").groups().add(group1); + group1 = adminClient.realm("test").getGroupByPath("/group1"); + userResource.joinGroup(group1.getId()); + + // create the attribute mapper + ProtocolMappersResource protocolMappers = findClientResourceByClientId(adminClient.realm("test"), "test-app").getProtocolMappers(); + protocolMappers.createMapper(createClaimMapper("group-value", "group-value", "group-value", "String", true, true, true, true)).close(); + + try { + // test it + OAuthClient.AccessTokenResponse response = browserLogin("password", "test-user@localhost", "password"); + + IDToken idToken = oauth.verifyIDToken(response.getIdToken()); + assertNotNull(idToken.getOtherClaims()); + assertNotNull(idToken.getOtherClaims().get("group-value")); + assertTrue(idToken.getOtherClaims().get("group-value") instanceof List); + assertEquals(2, ((List) idToken.getOtherClaims().get("group-value")).size()); + assertTrue(((List) idToken.getOtherClaims().get("group-value")).contains("value1")); + assertTrue(((List) idToken.getOtherClaims().get("group-value")).contains("value2")); + } finally { + // revert + userResource.leaveGroup(group1.getId()); + adminClient.realm("test").groups().group(group1.getId()).remove(); + deleteMappers(protocolMappers); + } + } + + @Test + public void testGroupAttributeTwoGroupNoMultivalueNoAggregate() throws Exception { + // get the user + UserResource userResource = findUserByUsernameId(adminClient.realm("test"), "test-user@localhost"); + // create two groups with two values (one is the same value) + GroupRepresentation group1 = new GroupRepresentation(); + group1.setName("group1"); + group1.setAttributes(new HashMap<>()); + group1.getAttributes().put("group-value", Arrays.asList("value1", "value2")); + adminClient.realm("test").groups().add(group1); + group1 = adminClient.realm("test").getGroupByPath("/group1"); + userResource.joinGroup(group1.getId()); + GroupRepresentation group2 = new GroupRepresentation(); + group2.setName("group2"); + group2.setAttributes(new HashMap<>()); + group2.getAttributes().put("group-value", Arrays.asList("value2", "value3")); + adminClient.realm("test").groups().add(group2); + group2 = adminClient.realm("test").getGroupByPath("/group2"); + userResource.joinGroup(group2.getId()); + + // create the attribute mapper + ProtocolMappersResource protocolMappers = findClientResourceByClientId(adminClient.realm("test"), "test-app").getProtocolMappers(); + protocolMappers.createMapper(createClaimMapper("group-value", "group-value", "group-value", "String", true, true, false, false)).close(); + + try { + // test it + OAuthClient.AccessTokenResponse response = browserLogin("password", "test-user@localhost", "password"); + + IDToken idToken = oauth.verifyIDToken(response.getIdToken()); + assertNotNull(idToken.getOtherClaims()); + assertNotNull(idToken.getOtherClaims().get("group-value")); + assertTrue(idToken.getOtherClaims().get("group-value") instanceof String); + assertTrue("value1".equals(idToken.getOtherClaims().get("group-value")) + || "value2".equals(idToken.getOtherClaims().get("group-value")) + || "value3".equals(idToken.getOtherClaims().get("group-value"))); + } finally { + // revert + userResource.leaveGroup(group1.getId()); + adminClient.realm("test").groups().group(group1.getId()).remove(); + userResource.leaveGroup(group2.getId()); + adminClient.realm("test").groups().group(group2.getId()).remove(); + deleteMappers(protocolMappers); + } + } + + @Test + public void testGroupAttributeTwoGroupMultiValueNoAggregate() throws Exception { + // get the user + UserResource userResource = findUserByUsernameId(adminClient.realm("test"), "test-user@localhost"); + // create two groups with two values (one is the same value) + GroupRepresentation group1 = new GroupRepresentation(); + group1.setName("group1"); + group1.setAttributes(new HashMap<>()); + group1.getAttributes().put("group-value", Arrays.asList("value1", "value2")); + adminClient.realm("test").groups().add(group1); + group1 = adminClient.realm("test").getGroupByPath("/group1"); + userResource.joinGroup(group1.getId()); + GroupRepresentation group2 = new GroupRepresentation(); + group2.setName("group2"); + group2.setAttributes(new HashMap<>()); + group2.getAttributes().put("group-value", Arrays.asList("value2", "value3")); + adminClient.realm("test").groups().add(group2); + group2 = adminClient.realm("test").getGroupByPath("/group2"); + userResource.joinGroup(group2.getId()); + + // create the attribute mapper + ProtocolMappersResource protocolMappers = findClientResourceByClientId(adminClient.realm("test"), "test-app").getProtocolMappers(); + protocolMappers.createMapper(createClaimMapper("group-value", "group-value", "group-value", "String", true, true, true, false)).close(); + + try { + // test it + OAuthClient.AccessTokenResponse response = browserLogin("password", "test-user@localhost", "password"); + + IDToken idToken = oauth.verifyIDToken(response.getIdToken()); + assertNotNull(idToken.getOtherClaims()); + assertNotNull(idToken.getOtherClaims().get("group-value")); + assertTrue(idToken.getOtherClaims().get("group-value") instanceof List); + assertEquals(2, ((List) idToken.getOtherClaims().get("group-value")).size()); + assertTrue((((List) idToken.getOtherClaims().get("group-value")).contains("value1") + && ((List) idToken.getOtherClaims().get("group-value")).contains("value2")) + || (((List) idToken.getOtherClaims().get("group-value")).contains("value2") + && ((List) idToken.getOtherClaims().get("group-value")).contains("value3"))); + } finally { + // revert + userResource.leaveGroup(group1.getId()); + adminClient.realm("test").groups().group(group1.getId()).remove(); + userResource.leaveGroup(group2.getId()); + adminClient.realm("test").groups().group(group2.getId()).remove(); + deleteMappers(protocolMappers); + } + } + + @Test + public void testGroupAttributeTwoGroupMultiValueAggregate() throws Exception { + // get the user + UserResource userResource = findUserByUsernameId(adminClient.realm("test"), "test-user@localhost"); + // create two groups with two values (one is the same value) + GroupRepresentation group1 = new GroupRepresentation(); + group1.setName("group1"); + group1.setAttributes(new HashMap<>()); + group1.getAttributes().put("group-value", Arrays.asList("value1", "value2")); + adminClient.realm("test").groups().add(group1); + group1 = adminClient.realm("test").getGroupByPath("/group1"); + userResource.joinGroup(group1.getId()); + GroupRepresentation group2 = new GroupRepresentation(); + group2.setName("group2"); + group2.setAttributes(new HashMap<>()); + group2.getAttributes().put("group-value", Arrays.asList("value2", "value3")); + adminClient.realm("test").groups().add(group2); + group2 = adminClient.realm("test").getGroupByPath("/group2"); + userResource.joinGroup(group2.getId()); + + // create the attribute mapper + ProtocolMappersResource protocolMappers = findClientResourceByClientId(adminClient.realm("test"), "test-app").getProtocolMappers(); + protocolMappers.createMapper(createClaimMapper("group-value", "group-value", "group-value", "String", true, true, true, true)).close(); + + try { + // test it + OAuthClient.AccessTokenResponse response = browserLogin("password", "test-user@localhost", "password"); + + IDToken idToken = oauth.verifyIDToken(response.getIdToken()); + assertNotNull(idToken.getOtherClaims()); + assertNotNull(idToken.getOtherClaims().get("group-value")); + assertTrue(idToken.getOtherClaims().get("group-value") instanceof List); + assertEquals(3, ((List) idToken.getOtherClaims().get("group-value")).size()); + assertTrue(((List) idToken.getOtherClaims().get("group-value")).contains("value1")); + assertTrue(((List) idToken.getOtherClaims().get("group-value")).contains("value2")); + assertTrue(((List) idToken.getOtherClaims().get("group-value")).contains("value3")); + } finally { + // revert + userResource.leaveGroup(group1.getId()); + adminClient.realm("test").groups().group(group1.getId()).remove(); + userResource.leaveGroup(group2.getId()); + adminClient.realm("test").groups().group(group2.getId()).remove(); + deleteMappers(protocolMappers); + } + } + private void assertRoles(List actualRoleList, String ...expectedRoles){ Assert.assertNames(actualRoleList, expectedRoles); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/ProtocolMapperUtil.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/ProtocolMapperUtil.java index 16e807b3474e..b9f0d6863c00 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/ProtocolMapperUtil.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/ProtocolMapperUtil.java @@ -87,7 +87,17 @@ public static ProtocolMapperRepresentation createClaimMapper(String name, String tokenClaimName, String claimType, boolean accessToken, boolean idToken, boolean multivalued) { return ModelToRepresentation.toRepresentation(UserAttributeMapper.createClaimMapper(name, userAttribute, tokenClaimName, - claimType, accessToken, idToken, multivalued)); + claimType, accessToken, idToken, multivalued, false)); + + } + + public static ProtocolMapperRepresentation createClaimMapper(String name, + String userAttribute, + String tokenClaimName, String claimType, + boolean accessToken, boolean idToken, + boolean multivalued, boolean aggregateAttrs) { + return ModelToRepresentation.toRepresentation(UserAttributeMapper.createClaimMapper(name, userAttribute, tokenClaimName, + claimType, accessToken, idToken, multivalued, aggregateAttrs)); } diff --git a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties index 8a7382aeca7b..175a5a1e662f 100644 --- a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties +++ b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties @@ -206,6 +206,8 @@ userSession.modelNote.label=User Session Note userSession.modelNote.tooltip=Name of stored user session note within the UserSessionModel.note map. multivalued.label=Multivalued multivalued.tooltip=Indicates if attribute supports multiple values. If true, then the list of all values of this attribute will be set as claim. If false, then just first value will be set as claim +aggregate.attrs.label=Aggregate attribute values +aggregate.attrs.tooltip=Indicates if attribute values should be aggregated with the group attributes. If using OpenID Connect mapper the multivalued option needs to be enabled too in order to get all the values. Duplicated values are discarded and the order of values is not guaranteed with this option. selectRole.label=Select Role selectRole.tooltip=Enter role in the textbox to the left, or click this button to browse and select the role you want. tokenClaimName.label=Token Claim Name @@ -1537,4 +1539,4 @@ advanced-client-settings.tooltip=Expand this section to configure advanced setti tls-client-certificate-bound-access-tokens=OAuth 2.0 Mutual TLS Certificate Bound Access Tokens Enabled tls-client-certificate-bound-access-tokens.tooltip=This enables support for OAuth 2.0 Mutual TLS Certificate Bound Access Tokens, which means that keycloak bind an access token and a refresh token with a X.509 certificate of a token requesting client exchanged in mutual TLS between keycloak's Token Endpoint and this client. These tokens can be treated as Holder-of-Key tokens instead of bearer tokens. subjectdn=Subject DN -subjectdn-tooltip=A regular expression for validating Subject DN in the Client Certificate. Use "(.*?)(?:$)" to match all kind of expressions. \ No newline at end of file +subjectdn-tooltip=A regular expression for validating Subject DN in the Client Certificate. Use "(.*?)(?:$)" to match all kind of expressions.