From dd9ad305cad6084ba5af357eaaa8857a22695103 Mon Sep 17 00:00:00 2001 From: Benjamin Weimer Date: Tue, 24 Sep 2019 09:57:16 +0200 Subject: [PATCH] KEYCLOAK-12757 New Identity Provider Mapper "Advanced Claim to Role Mapper" with following features * Regex support for claim values. * Support for multiple claims. --- .../models/IdentityProviderMapperModel.java | 41 +++ .../provider/ProviderConfigProperty.java | 5 + .../oidc/mappers/AbstractClaimMapper.java | 2 +- .../mappers/AdvancedClaimToRoleMapper.java | 159 +++++++++ ...oak.broker.provider.IdentityProviderMapper | 1 + .../testsuite/admin/IdentityProviderTest.java | 4 +- .../AbstractUserAttributeMapperTest.java | 21 +- .../broker/KcOidcBrokerConfiguration.java | 24 +- .../broker/KcSamlBrokerConfiguration.java | 4 +- .../OidcAdvancedClaimToRoleMapperTest.java | 319 ++++++++++++++++++ .../broker/OidcUserAttributeMapperTest.java | 2 +- .../broker/SamlUserAttributeMapperTest.java | 2 +- .../theme/base/admin/resources/js/app.js | 68 ++++ .../templates/kc-provider-config.html | 31 ++ 14 files changed, 661 insertions(+), 22 deletions(-) create mode 100755 services/src/main/java/org/keycloak/broker/oidc/mappers/AdvancedClaimToRoleMapper.java create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/OidcAdvancedClaimToRoleMapperTest.java diff --git a/server-spi/src/main/java/org/keycloak/models/IdentityProviderMapperModel.java b/server-spi/src/main/java/org/keycloak/models/IdentityProviderMapperModel.java index d4e6b7e495..17082d8b3f 100755 --- a/server-spi/src/main/java/org/keycloak/models/IdentityProviderMapperModel.java +++ b/server-spi/src/main/java/org/keycloak/models/IdentityProviderMapperModel.java @@ -17,8 +17,14 @@ package org.keycloak.models; +import com.fasterxml.jackson.core.type.TypeReference; +import org.keycloak.util.JsonSerialization; + +import java.io.IOException; import java.io.Serializable; +import java.util.List; import java.util.Map; +import java.util.stream.Collectors; /** * Specifies a mapping from broker login to user data. @@ -28,6 +34,9 @@ import java.util.Map; */ public class IdentityProviderMapperModel implements Serializable { + private static final TypeReference> MAP_TYPE_REPRESENTATION = new TypeReference>() { + }; + protected String id; protected String name; protected String identityProviderAlias; @@ -75,6 +84,17 @@ public class IdentityProviderMapperModel implements Serializable { this.config = config; } + public Map getConfigMap(String configKey) { + String configMap = config.get(configKey); + + try { + List map = JsonSerialization.readValue(configMap, MAP_TYPE_REPRESENTATION); + return map.stream().collect(Collectors.toMap(StringPair::getKey, StringPair::getValue)); + } catch (IOException e) { + throw new RuntimeException("Could not deserialize json: " + configMap, e); + } + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -91,4 +111,25 @@ public class IdentityProviderMapperModel implements Serializable { public int hashCode() { return id.hashCode(); } + + static class StringPair { + private String key; + private String value; + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + } } diff --git a/server-spi/src/main/java/org/keycloak/provider/ProviderConfigProperty.java b/server-spi/src/main/java/org/keycloak/provider/ProviderConfigProperty.java index 9b470ecacc..3abda27e47 100755 --- a/server-spi/src/main/java/org/keycloak/provider/ProviderConfigProperty.java +++ b/server-spi/src/main/java/org/keycloak/provider/ProviderConfigProperty.java @@ -57,6 +57,11 @@ public class ProviderConfigProperty { */ public static final String TEXT_TYPE="Text"; + /** + * Configure multiple (key, value) pairs + */ + public static final String MAP_TYPE ="Map"; + protected String name; protected String label; protected String helpText; diff --git a/services/src/main/java/org/keycloak/broker/oidc/mappers/AbstractClaimMapper.java b/services/src/main/java/org/keycloak/broker/oidc/mappers/AbstractClaimMapper.java index b9b8ed9ffb..3490e969d9 100755 --- a/services/src/main/java/org/keycloak/broker/oidc/mappers/AbstractClaimMapper.java +++ b/services/src/main/java/org/keycloak/broker/oidc/mappers/AbstractClaimMapper.java @@ -118,7 +118,7 @@ public abstract class AbstractClaimMapper extends AbstractIdentityProviderMapper } else if (value instanceof List) { List list = (List)value; for (Object val : list) { - if (valueEquals(desiredValue, val)) return true; + if (valueEquals(desiredValue, val)) return true; } } else if (value instanceof JsonNode) { try { diff --git a/services/src/main/java/org/keycloak/broker/oidc/mappers/AdvancedClaimToRoleMapper.java b/services/src/main/java/org/keycloak/broker/oidc/mappers/AdvancedClaimToRoleMapper.java new file mode 100755 index 0000000000..7892175fc3 --- /dev/null +++ b/services/src/main/java/org/keycloak/broker/oidc/mappers/AdvancedClaimToRoleMapper.java @@ -0,0 +1,159 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.broker.oidc.mappers; + +import org.keycloak.broker.oidc.KeycloakOIDCIdentityProviderFactory; +import org.keycloak.broker.oidc.OIDCIdentityProviderFactory; +import org.keycloak.broker.provider.BrokeredIdentityContext; +import org.keycloak.broker.provider.ConfigConstants; +import org.keycloak.broker.provider.IdentityBrokerException; +import org.keycloak.models.IdentityProviderMapperModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.RoleModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.provider.ProviderConfigProperty; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * @author Bill Burke, Benjamin Weimer + * @version $Revision: 1 $ + */ +public class AdvancedClaimToRoleMapper extends AbstractClaimMapper { + + public static final String CLAIM_PROPERTY_NAME = "claims"; + public static final String ARE_CLAIM_VALUES_REGEX_PROPERTY_NAME = "are.claim.values.regex"; + + public static final String[] COMPATIBLE_PROVIDERS = {KeycloakOIDCIdentityProviderFactory.PROVIDER_ID, OIDCIdentityProviderFactory.PROVIDER_ID}; + + private static final List configProperties = new ArrayList(); + + static { + ProviderConfigProperty claimsProperty = new ProviderConfigProperty(); + claimsProperty.setName(CLAIM_PROPERTY_NAME); + claimsProperty.setLabel("Claims"); + claimsProperty.setHelpText("Name and value of the claims to search for in token. You can reference nested claims using a '.', i.e. 'address.locality'. To use dot (.) literally, escape it with backslash (\\.)"); + claimsProperty.setType(ProviderConfigProperty.MAP_TYPE); + configProperties.add(claimsProperty); + ProviderConfigProperty isClaimValueRegexProperty = new ProviderConfigProperty(); + isClaimValueRegexProperty.setName(ARE_CLAIM_VALUES_REGEX_PROPERTY_NAME); + isClaimValueRegexProperty.setLabel("Regex Claim Values"); + isClaimValueRegexProperty.setHelpText("If enabled claim values are interpreted as regular expressions."); + isClaimValueRegexProperty.setType(ProviderConfigProperty.BOOLEAN_TYPE); + configProperties.add(isClaimValueRegexProperty); + ProviderConfigProperty roleProperty = new ProviderConfigProperty(); + roleProperty.setName(ConfigConstants.ROLE); + roleProperty.setLabel("Role"); + roleProperty.setHelpText("Role to grant to user if claim is present. Click 'Select Role' button to browse roles, or just type it in the textbox. To reference an application role the syntax is appname.approle, i.e. myapp.myrole"); + roleProperty.setType(ProviderConfigProperty.ROLE_TYPE); + configProperties.add(roleProperty); + } + + public static final String PROVIDER_ID = "oidc-advanced-role-idp-mapper"; + + + @Override + public List getConfigProperties() { + return configProperties; + } + + @Override + public String getId() { + return PROVIDER_ID; + } + + @Override + public String[] getCompatibleProviders() { + return COMPATIBLE_PROVIDERS; + } + + @Override + public String getDisplayCategory() { + return "Role Importer"; + } + + @Override + public String getDisplayType() { + return "Advanced Claim to Role"; + } + + @Override + public void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { + String roleName = mapperModel.getConfig().get(ConfigConstants.ROLE); + if (hasAllClaimValues(mapperModel, context)) { + RoleModel role = KeycloakModelUtils.getRoleFromString(realm, roleName); + if (role == null) throw new IdentityBrokerException("Unable to find role: " + roleName); + user.grantRole(role); + } + } + + @Override + public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { + String roleName = mapperModel.getConfig().get(ConfigConstants.ROLE); + if (!hasAllClaimValues(mapperModel, context)) { + RoleModel role = KeycloakModelUtils.getRoleFromString(realm, roleName); + if (role == null) throw new IdentityBrokerException("Unable to find role: " + roleName); + user.deleteRoleMapping(role); + } + + } + + @Override + public String getHelpText() { + return "If all claims exists, grant the user the specified realm or application role."; + } + + protected boolean hasAllClaimValues(IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { + Map claims = mapperModel.getConfigMap(CLAIM_PROPERTY_NAME); + Boolean areClaimValuesRegex = Boolean.valueOf(mapperModel.getConfig().get(ARE_CLAIM_VALUES_REGEX_PROPERTY_NAME)); + + for (Map.Entry claim : claims.entrySet()) { + Object value = getClaimValue(context, claim.getKey()); + + boolean claimValuesMismatch = !(areClaimValuesRegex ? valueMatchesRegex(claim.getValue(), value) : valueEquals(claim.getValue(), value)); + if (claimValuesMismatch) { + return false; + } + } + + return true; + } + + public boolean valueMatchesRegex(String regex, Object value) { + if (value instanceof List) { + List list = (List) value; + for (Object val : list) { + if (valueMatchesRegex(regex, val)) { + return true; + } + } + } else { + if (value != null) { + String stringValue = value.toString(); + if (stringValue != null && stringValue.matches(regex)) { + return true; + } + } + } + return false; + } +} diff --git a/services/src/main/resources/META-INF/services/org.keycloak.broker.provider.IdentityProviderMapper b/services/src/main/resources/META-INF/services/org.keycloak.broker.provider.IdentityProviderMapper index 378f05eb9d..4275976031 100755 --- a/services/src/main/resources/META-INF/services/org.keycloak.broker.provider.IdentityProviderMapper +++ b/services/src/main/resources/META-INF/services/org.keycloak.broker.provider.IdentityProviderMapper @@ -19,6 +19,7 @@ org.keycloak.broker.provider.HardcodedRoleMapper org.keycloak.broker.provider.HardcodedAttributeMapper org.keycloak.broker.provider.HardcodedUserSessionAttributeMapper org.keycloak.broker.oidc.mappers.ClaimToRoleMapper +org.keycloak.broker.oidc.mappers.AdvancedClaimToRoleMapper org.keycloak.broker.oidc.mappers.ExternalKeycloakRoleToRoleMapper org.keycloak.broker.oidc.mappers.UserAttributeMapper org.keycloak.broker.oidc.mappers.UsernameTemplateMapper diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/IdentityProviderTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/IdentityProviderTest.java index 3266ead71a..b09158b163 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/IdentityProviderTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/IdentityProviderTest.java @@ -361,12 +361,12 @@ public class IdentityProviderTest extends AbstractAdminTest { create(createRep("keycloak-oidc", "keycloak-oidc")); provider = realm.identityProviders().get("keycloak-oidc"); mapperTypes = provider.getMapperTypes(); - assertMapperTypes(mapperTypes, "keycloak-oidc-role-to-role-idp-mapper", "oidc-user-attribute-idp-mapper", "oidc-role-idp-mapper", "oidc-username-idp-mapper"); + assertMapperTypes(mapperTypes, "keycloak-oidc-role-to-role-idp-mapper", "oidc-user-attribute-idp-mapper", "oidc-role-idp-mapper", "oidc-username-idp-mapper", "oidc-advanced-role-idp-mapper"); create(createRep("oidc", "oidc")); provider = realm.identityProviders().get("oidc"); mapperTypes = provider.getMapperTypes(); - assertMapperTypes(mapperTypes, "oidc-user-attribute-idp-mapper", "oidc-role-idp-mapper", "oidc-username-idp-mapper"); + assertMapperTypes(mapperTypes, "oidc-user-attribute-idp-mapper", "oidc-role-idp-mapper", "oidc-username-idp-mapper", "oidc-advanced-role-idp-mapper"); create(createRep("saml", "saml")); provider = realm.identityProviders().get("saml"); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractUserAttributeMapperTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractUserAttributeMapperTest.java index 4f3b29e332..11fb3fdb0f 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractUserAttributeMapperTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractUserAttributeMapperTest.java @@ -32,7 +32,6 @@ public abstract class AbstractUserAttributeMapperTest extends AbstractBaseBroker protected static final String MAPPED_ATTRIBUTE_NAME = "mapped-user-attribute"; protected static final String MAPPED_ATTRIBUTE_FRIENDLY_NAME = "mapped-user-attribute-friendly"; - protected static final String ATTRIBUTE_TO_MAP_NAME = "user-attribute"; protected static final String ATTRIBUTE_TO_MAP_FRIENDLY_NAME = "user-attribute-friendly"; private static final Set PROTECTED_NAMES = ImmutableSet.builder().add("email").add("lastName").add("firstName").build(); @@ -40,7 +39,7 @@ public abstract class AbstractUserAttributeMapperTest extends AbstractBaseBroker .put("dotted.email", "dotted.email") .put("nested.email", "nested.email") .put(ATTRIBUTE_TO_MAP_FRIENDLY_NAME, MAPPED_ATTRIBUTE_FRIENDLY_NAME) - .put(ATTRIBUTE_TO_MAP_NAME, MAPPED_ATTRIBUTE_NAME) + .put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, MAPPED_ATTRIBUTE_NAME) .build(); protected abstract Iterable createIdentityProviderMappers(); @@ -188,10 +187,10 @@ public abstract class AbstractUserAttributeMapperTest extends AbstractBaseBroker @Test public void testBasicMappingSingleValue() { testValueMapping(ImmutableMap.>builder() - .put(ATTRIBUTE_TO_MAP_NAME, ImmutableList.builder().add("value 1").build()) + .put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.builder().add("value 1").build()) .build(), ImmutableMap.>builder() - .put(ATTRIBUTE_TO_MAP_NAME, ImmutableList.builder().add("second value").build()) + .put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.builder().add("second value").build()) .build() ); } @@ -214,10 +213,10 @@ public abstract class AbstractUserAttributeMapperTest extends AbstractBaseBroker @Test public void testBasicMappingClearValue() { testValueMapping(ImmutableMap.>builder() - .put(ATTRIBUTE_TO_MAP_NAME, ImmutableList.builder().add("value 1").build()) + .put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.builder().add("value 1").build()) .build(), ImmutableMap.>builder() - .put(ATTRIBUTE_TO_MAP_NAME, ImmutableList.builder().build()) + .put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.builder().build()) .build() ); } @@ -225,7 +224,7 @@ public abstract class AbstractUserAttributeMapperTest extends AbstractBaseBroker @Test public void testBasicMappingRemoveValue() { testValueMapping(ImmutableMap.>builder() - .put(ATTRIBUTE_TO_MAP_NAME, ImmutableList.builder().add("value 1").build()) + .put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.builder().add("value 1").build()) .build(), ImmutableMap.>builder() .build() @@ -235,10 +234,10 @@ public abstract class AbstractUserAttributeMapperTest extends AbstractBaseBroker @Test public void testBasicMappingMultipleValues() { testValueMapping(ImmutableMap.>builder() - .put(ATTRIBUTE_TO_MAP_NAME, ImmutableList.builder().add("value 1").add("value 2").build()) + .put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.builder().add("value 1").add("value 2").build()) .build(), ImmutableMap.>builder() - .put(ATTRIBUTE_TO_MAP_NAME, ImmutableList.builder().add("second value").add("second value 2").build()) + .put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.builder().add("second value").add("second value 2").build()) .build() ); } @@ -248,7 +247,7 @@ public abstract class AbstractUserAttributeMapperTest extends AbstractBaseBroker testValueMapping(ImmutableMap.>builder() .build(), ImmutableMap.>builder() - .put(ATTRIBUTE_TO_MAP_NAME, ImmutableList.builder().add("second value").add("second value 2").build()) + .put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.builder().add("second value").add("second value 2").build()) .build() ); } @@ -256,7 +255,7 @@ public abstract class AbstractUserAttributeMapperTest extends AbstractBaseBroker @Test public void testDeleteBasicMappingMultipleValues() { testValueMapping(ImmutableMap.>builder() - .put(ATTRIBUTE_TO_MAP_NAME, ImmutableList.builder().add("second value").add("second value 2").build()) + .put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.builder().add("second value").add("second value 2").build()) .build(), ImmutableMap.>builder() .build() diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerConfiguration.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerConfiguration.java index 94cfbb1d34..016b29faf5 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerConfiguration.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerConfiguration.java @@ -21,13 +21,15 @@ import static org.keycloak.testsuite.broker.BrokerTestConstants.*; import static org.keycloak.testsuite.broker.BrokerTestTools.*; /** - * * @author hmlnarik */ public class KcOidcBrokerConfiguration implements BrokerConfiguration { public static final KcOidcBrokerConfiguration INSTANCE = new KcOidcBrokerConfiguration(); + protected static final String ATTRIBUTE_TO_MAP_NAME = "user-attribute"; + protected static final String ATTRIBUTE_TO_MAP_NAME_2 = "user-attribute-2"; + @Override public RealmRepresentation createProviderRealm() { RealmRepresentation realm = new RealmRepresentation(); @@ -106,15 +108,29 @@ public class KcOidcBrokerConfiguration implements BrokerConfiguration { userAttrMapper.setProtocolMapper(UserAttributeMapper.PROVIDER_ID); Map userAttrMapperConfig = userAttrMapper.getConfig(); - userAttrMapperConfig.put(ProtocolMapperUtils.USER_ATTRIBUTE, AbstractUserAttributeMapperTest.ATTRIBUTE_TO_MAP_NAME); - userAttrMapperConfig.put(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME, AbstractUserAttributeMapperTest.ATTRIBUTE_TO_MAP_NAME); + userAttrMapperConfig.put(ProtocolMapperUtils.USER_ATTRIBUTE, ATTRIBUTE_TO_MAP_NAME); + userAttrMapperConfig.put(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME, ATTRIBUTE_TO_MAP_NAME); userAttrMapperConfig.put(OIDCAttributeMapperHelper.JSON_TYPE, ProviderConfigProperty.STRING_TYPE); userAttrMapperConfig.put(OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN, "true"); userAttrMapperConfig.put(OIDCAttributeMapperHelper.INCLUDE_IN_ID_TOKEN, "true"); userAttrMapperConfig.put(OIDCAttributeMapperHelper.INCLUDE_IN_USERINFO, "true"); userAttrMapperConfig.put(ProtocolMapperUtils.MULTIVALUED, "true"); - client.setProtocolMappers(Arrays.asList(emailMapper, userAttrMapper, nestedAttrMapper, dottedAttrMapper)); + ProtocolMapperRepresentation userAttrMapper2 = new ProtocolMapperRepresentation(); + userAttrMapper2.setName("attribute - name - 2"); + userAttrMapper2.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); + userAttrMapper2.setProtocolMapper(UserAttributeMapper.PROVIDER_ID); + + Map userAttrMapperConfig2 = userAttrMapper2.getConfig(); + userAttrMapperConfig2.put(ProtocolMapperUtils.USER_ATTRIBUTE, ATTRIBUTE_TO_MAP_NAME_2); + userAttrMapperConfig2.put(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME, ATTRIBUTE_TO_MAP_NAME_2); + userAttrMapperConfig2.put(OIDCAttributeMapperHelper.JSON_TYPE, ProviderConfigProperty.STRING_TYPE); + userAttrMapperConfig2.put(OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN, "true"); + userAttrMapperConfig2.put(OIDCAttributeMapperHelper.INCLUDE_IN_ID_TOKEN, "true"); + userAttrMapperConfig2.put(OIDCAttributeMapperHelper.INCLUDE_IN_USERINFO, "true"); + userAttrMapperConfig2.put(ProtocolMapperUtils.MULTIVALUED, "true"); + + client.setProtocolMappers(Arrays.asList(emailMapper, userAttrMapper, userAttrMapper2, nestedAttrMapper, dottedAttrMapper)); return Collections.singletonList(client); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlBrokerConfiguration.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlBrokerConfiguration.java index 0e9556176a..2cfa37afde 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlBrokerConfiguration.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlBrokerConfiguration.java @@ -124,8 +124,8 @@ public class KcSamlBrokerConfiguration implements BrokerConfiguration { userAttrMapper.setProtocolMapper(UserAttributeStatementMapper.PROVIDER_ID); Map userAttrMapperConfig = userAttrMapper.getConfig(); - userAttrMapperConfig.put(ProtocolMapperUtils.USER_ATTRIBUTE, AbstractUserAttributeMapperTest.ATTRIBUTE_TO_MAP_NAME); - userAttrMapperConfig.put(AttributeStatementHelper.SAML_ATTRIBUTE_NAME, AbstractUserAttributeMapperTest.ATTRIBUTE_TO_MAP_NAME); + userAttrMapperConfig.put(ProtocolMapperUtils.USER_ATTRIBUTE, KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME); + userAttrMapperConfig.put(AttributeStatementHelper.SAML_ATTRIBUTE_NAME, KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME); userAttrMapperConfig.put(AttributeStatementHelper.SAML_ATTRIBUTE_NAMEFORMAT, AttributeStatementHelper.BASIC); userAttrMapperConfig.put(AttributeStatementHelper.FRIENDLY_NAME, ""); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/OidcAdvancedClaimToRoleMapperTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/OidcAdvancedClaimToRoleMapperTest.java new file mode 100644 index 0000000000..42e827a5b2 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/OidcAdvancedClaimToRoleMapperTest.java @@ -0,0 +1,319 @@ +package org.keycloak.testsuite.broker; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import org.junit.Before; +import org.junit.Test; +import org.keycloak.admin.client.resource.IdentityProviderResource; +import org.keycloak.admin.client.resource.RealmResource; +import org.keycloak.admin.client.resource.UsersResource; +import org.keycloak.broker.oidc.mappers.AdvancedClaimToRoleMapper; +import org.keycloak.broker.provider.ConfigConstants; +import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.IdentityProviderMapperRepresentation; +import org.keycloak.representations.idm.IdentityProviderRepresentation; +import org.keycloak.representations.idm.MappingsRepresentation; +import org.keycloak.representations.idm.RoleRepresentation; +import org.keycloak.representations.idm.UserRepresentation; +import org.keycloak.testsuite.util.UserBuilder; + +import javax.ws.rs.core.Response; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.junit.Assert.assertThat; +import static org.keycloak.testsuite.admin.ApiUtil.createUserAndResetPasswordWithAdminClient; + +/** + * @author hmlnarik, Benjamin Weimer + */ +public class OidcAdvancedClaimToRoleMapperTest extends AbstractBaseBrokerTest { + + private static final String CLIENT = "realm-management"; + private static final String CLIENT_ROLE = "view-realm"; + private static final String CLIENT_ROLE_MAPPER_REPRESENTATION = CLIENT + "." + CLIENT_ROLE; + + private static final String CLAIMS = "[\n" + + " {\n" + + " \"key\": \"" + KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME + "\",\n" + + " \"value\": \"value 1\"\n" + + " },\n" + + " {\n" + + " \"key\": \"" + KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME_2 + "\",\n" + + " \"value\": \"value 2\"\n" + + " }\n" + + "]"; + + private static final String CLAIMS_REGEX = "[\n" + + " {\n" + + " \"key\": \"" + KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME + "\",\n" + + " \"value\": \"va.*\"\n" + + " },\n" + + " {\n" + + " \"key\": \"" + KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME_2 + "\",\n" + + " \"value\": \"value 2\"\n" + + " }\n" + + "]"; + + + @Override + protected BrokerConfiguration getBrokerConfiguration() { + return new KcOidcBrokerConfiguration(); + } + + @Before + public void addClients() { + List clients = bc.createProviderClients(suiteContext); + if (clients != null) { + RealmResource providerRealm = adminClient.realm(bc.providerRealmName()); + for (ClientRepresentation client : clients) { + log.debug("adding client " + client.getName() + " to realm " + bc.providerRealmName()); + + Response resp = providerRealm.clients().create(client); + resp.close(); + } + } + + clients = bc.createConsumerClients(suiteContext); + if (clients != null) { + RealmResource consumerRealm = adminClient.realm(bc.consumerRealmName()); + for (ClientRepresentation client : clients) { + log.debug("adding client " + client.getName() + " to realm " + bc.consumerRealmName()); + + Response resp = consumerRealm.clients().create(client); + resp.close(); + } + } + } + + @Test + public void valueMatchesRegexTest() { + AdvancedClaimToRoleMapper advancedClaimToRoleMapper = new AdvancedClaimToRoleMapper(); + + assertThat(advancedClaimToRoleMapper.valueMatchesRegex("AB.*", "AB_ADMIN"), is(true)); + assertThat(advancedClaimToRoleMapper.valueMatchesRegex("AB.*", "AA_ADMIN"), is(false)); + assertThat(advancedClaimToRoleMapper.valueMatchesRegex("99.*", 999), is(true)); + assertThat(advancedClaimToRoleMapper.valueMatchesRegex("98.*", 999), is(false)); + assertThat(advancedClaimToRoleMapper.valueMatchesRegex("99\\..*", 99.9), is(true)); + assertThat(advancedClaimToRoleMapper.valueMatchesRegex("AB.*", null), is(false)); + assertThat(advancedClaimToRoleMapper.valueMatchesRegex("AB.*", Arrays.asList("AB_ADMIN", "AA_ADMIN")), is(true)); + } + + @Test + public void allClaimValuesMatch() { + createAdvancedClaimToRoleMapper(CLAIMS, false); + createUserInProviderRealm(ImmutableMap.>builder() + .put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.builder().add("value 1").build()) + .put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME_2, ImmutableList.builder().add("value 2").build()) + .build()); + + logInAsUserInIDPForFirstTime(); + + UserRepresentation user = findUser(bc.consumerRealmName(), bc.getUserLogin(), bc.getUserEmail()); + assertThatRoleHasBeenAssigned(user); + + logoutFromRealm(bc.consumerRealmName()); + } + + @Test + public void claimValuesMismatch() { + createAdvancedClaimToRoleMapper(CLAIMS, false); + createUserInProviderRealm(ImmutableMap.>builder() + .put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.builder().add("value 1").build()) + .put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME_2, ImmutableList.builder().add("value mismatch").build()) + .build()); + + logInAsUserInIDPForFirstTime(); + + UserRepresentation user = findUser(bc.consumerRealmName(), bc.getUserLogin(), bc.getUserEmail()); + assertThatRoleHasNotBeenAssigned(user); + + logoutFromRealm(bc.consumerRealmName()); + } + + @Test + public void claimValuesMatchIfNoClaimsSpecified() { + createAdvancedClaimToRoleMapper("[]", false); + createUserInProviderRealm(ImmutableMap.>builder() + .put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.builder().add("some value").build()) + .put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME_2, ImmutableList.builder().add("some value").build()) + .build()); + + logInAsUserInIDPForFirstTime(); + + UserRepresentation user = findUser(bc.consumerRealmName(), bc.getUserLogin(), bc.getUserEmail()); + assertThatRoleHasBeenAssigned(user); + + logoutFromRealm(bc.consumerRealmName()); + } + + @Test + public void allClaimValuesMatchRegex() { + createAdvancedClaimToRoleMapper(CLAIMS_REGEX, true); + createUserInProviderRealm(ImmutableMap.>builder() + .put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.builder().add("value 1").build()) + .put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME_2, ImmutableList.builder().add("value 2").build()) + .build()); + + logInAsUserInIDPForFirstTime(); + + UserRepresentation user = findUser(bc.consumerRealmName(), bc.getUserLogin(), bc.getUserEmail()); + assertThatRoleHasBeenAssigned(user); + + logoutFromRealm(bc.consumerRealmName()); + } + + + @Test + public void claimValuesMismatchRegex() { + createAdvancedClaimToRoleMapper(CLAIMS_REGEX, true); + createUserInProviderRealm(ImmutableMap.>builder() + .put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.builder().add("mismatch").build()) + .put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME_2, ImmutableList.builder().add("value 2").build()) + .build()); + + logInAsUserInIDPForFirstTime(); + + UserRepresentation user = findUser(bc.consumerRealmName(), bc.getUserLogin(), bc.getUserEmail()); + assertThatRoleHasNotBeenAssigned(user); + + logoutFromRealm(bc.consumerRealmName()); + } + + @Test + public void updateBrokeredUserMismatchDeletesRole() { + createAdvancedClaimToRoleMapper(CLAIMS, false); + createUserInProviderRealm(ImmutableMap.>builder() + .put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.builder().add("value 1").build()) + .put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME_2, ImmutableList.builder().add("value 2").build()) + .build()); + + logInAsUserInIDPForFirstTime(); + + UserRepresentation user = findUser(bc.consumerRealmName(), bc.getUserLogin(), bc.getUserEmail()); + assertThatRoleHasBeenAssigned(user); + + logoutFromRealm(bc.consumerRealmName()); + + // update + user = findUser(bc.providerRealmName(), bc.getUserLogin(), bc.getUserEmail()); + ImmutableMap> mismatchingAttributes = ImmutableMap.>builder() + .put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.builder().add("value 1").build()) + .put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME_2, ImmutableList.builder().add("value mismatch").build()) + .build(); + user.setAttributes(mismatchingAttributes); + adminClient.realm(bc.providerRealmName()).users().get(user.getId()).update(user); + + logInAsUserInIDP(); + user = findUser(bc.consumerRealmName(), bc.getUserLogin(), bc.getUserEmail()); + + assertThatRoleHasNotBeenAssigned(user); + } + + @Test + public void updateBrokeredUserMatchDoesntDeleteRole() { + createAdvancedClaimToRoleMapper(CLAIMS, false); + createUserInProviderRealm(ImmutableMap.>builder() + .put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.builder().add("value 1").build()) + .put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME_2, ImmutableList.builder().add("value 2").build()) + .build()); + + logInAsUserInIDPForFirstTime(); + + UserRepresentation user = findUser(bc.consumerRealmName(), bc.getUserLogin(), bc.getUserEmail()); + assertThatRoleHasBeenAssigned(user); + + logoutFromRealm(bc.consumerRealmName()); + + // update + user = findUser(bc.providerRealmName(), bc.getUserLogin(), bc.getUserEmail()); + ImmutableMap> matchingAttributes = ImmutableMap.>builder() + .put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.builder().add("value 1").build()) + .put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME_2, ImmutableList.builder().add("value 2").build()) + .put("some.other.attribute", ImmutableList.builder().add("some value").build()) + .build(); + user.setAttributes(matchingAttributes); + adminClient.realm(bc.providerRealmName()).users().get(user.getId()).update(user); + + logInAsUserInIDP(); + user = findUser(bc.consumerRealmName(), bc.getUserLogin(), bc.getUserEmail()); + + assertThatRoleHasBeenAssigned(user); + } + + private void createAdvancedClaimToRoleMapper(String claimsRepresentation, boolean areClaimValuesRegex) { + log.debug("adding identity provider to realm " + bc.consumerRealmName()); + + RealmResource realm = adminClient.realm(bc.consumerRealmName()); + final IdentityProviderRepresentation idp = bc.setUpIdentityProvider(suiteContext); + Response resp = realm.identityProviders().create(idp); + resp.close(); + + IdentityProviderMapperRepresentation advancedClaimToRoleMapper = new IdentityProviderMapperRepresentation(); + advancedClaimToRoleMapper.setName("advanced-claim-to-role-mapper"); + advancedClaimToRoleMapper.setIdentityProviderMapper(AdvancedClaimToRoleMapper.PROVIDER_ID); + advancedClaimToRoleMapper.setConfig(ImmutableMap.builder() + .put(AdvancedClaimToRoleMapper.CLAIM_PROPERTY_NAME, claimsRepresentation) + .put(AdvancedClaimToRoleMapper.ARE_CLAIM_VALUES_REGEX_PROPERTY_NAME, areClaimValuesRegex ? "true" : "false") + .put(ConfigConstants.ROLE, CLIENT_ROLE_MAPPER_REPRESENTATION) + .build()); + + IdentityProviderResource idpResource = realm.identityProviders().get(idp.getAlias()); + advancedClaimToRoleMapper.setIdentityProviderAlias(bc.getIDPAlias()); + resp = idpResource.addMapper(advancedClaimToRoleMapper); + resp.close(); + } + + private void createUserInProviderRealm(Map> attributes) { + log.debug("Creating user in realm " + bc.providerRealmName()); + + UserRepresentation user = UserBuilder.create() + .username(bc.getUserLogin()) + .email(bc.getUserEmail()) + .build(); + user.setEmailVerified(true); + user.setAttributes(attributes); + this.userId = createUserAndResetPasswordWithAdminClient(adminClient.realm(bc.providerRealmName()), user, bc.getUserPassword()); + } + + private UserRepresentation findUser(String realm, String userName, String email) { + UsersResource consumerUsers = adminClient.realm(realm).users(); + + List users = consumerUsers.list(); + assertThat("There must be exactly one user", users, hasSize(1)); + UserRepresentation user = users.get(0); + assertThat("Username has to match", user.getUsername(), equalTo(userName)); + assertThat("Email has to match", user.getEmail(), equalTo(email)); + + MappingsRepresentation roles = consumerUsers.get(user.getId()).roles().getAll(); + + List realmRoles = roles.getRealmMappings().stream() + .map(RoleRepresentation::getName) + .collect(Collectors.toList()); + user.setRealmRoles(realmRoles); + + Map> clientRoles = new HashMap<>(); + roles.getClientMappings().forEach((key, value) -> clientRoles.put(key, value.getMappings().stream() + .map(RoleRepresentation::getName) + .collect(Collectors.toList()))); + user.setClientRoles(clientRoles); + + return user; + } + + private void assertThatRoleHasBeenAssigned(UserRepresentation user) { + assertThat(user.getClientRoles().get(CLIENT), contains(CLIENT_ROLE)); + } + + private void assertThatRoleHasNotBeenAssigned(UserRepresentation user) { + assertThat(user.getClientRoles().get(CLIENT), not(contains(CLIENT_ROLE))); + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/OidcUserAttributeMapperTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/OidcUserAttributeMapperTest.java index bdae7f72cc..d07ec0f70b 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/OidcUserAttributeMapperTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/OidcUserAttributeMapperTest.java @@ -19,7 +19,7 @@ public class OidcUserAttributeMapperTest extends AbstractUserAttributeMapperTest attrMapper1.setName("attribute-mapper"); attrMapper1.setIdentityProviderMapper(UserAttributeMapper.PROVIDER_ID); attrMapper1.setConfig(ImmutableMap.builder() - .put(UserAttributeMapper.CLAIM, ATTRIBUTE_TO_MAP_NAME) + .put(UserAttributeMapper.CLAIM, KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME) .put(UserAttributeMapper.USER_ATTRIBUTE, MAPPED_ATTRIBUTE_NAME) .build()); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/SamlUserAttributeMapperTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/SamlUserAttributeMapperTest.java index 20de190d38..e6446dcb3d 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/SamlUserAttributeMapperTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/SamlUserAttributeMapperTest.java @@ -43,7 +43,7 @@ public class SamlUserAttributeMapperTest extends AbstractUserAttributeMapperTest attrMapper1.setName("attribute-mapper"); attrMapper1.setIdentityProviderMapper(UserAttributeMapper.PROVIDER_ID); attrMapper1.setConfig(ImmutableMap.builder() - .put(UserAttributeMapper.ATTRIBUTE_NAME, ATTRIBUTE_TO_MAP_NAME) + .put(UserAttributeMapper.ATTRIBUTE_NAME, KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME) .put(UserAttributeMapper.USER_ATTRIBUTE, MAPPED_ATTRIBUTE_NAME) .build()); diff --git a/themes/src/main/resources/theme/base/admin/resources/js/app.js b/themes/src/main/resources/theme/base/admin/resources/js/app.js index 1bfa0abb10..c88c27f2a4 100755 --- a/themes/src/main/resources/theme/base/admin/resources/js/app.js +++ b/themes/src/main/resources/theme/base/admin/resources/js/app.js @@ -2727,6 +2727,10 @@ module.controller('RoleSelectorModalCtrl', function($scope, realm, config, confi module.controller('ProviderConfigCtrl', function ($modal, $scope, $route, ComponentUtils, Client) { clientSelectControl($scope, $route.current.params.realm, Client); $scope.fileNames = {}; + $scope.newMapEntries = {}; + var cachedMaps = {}; + var cachedParsedMaps = {}; + var focusMapValueId = null; // KEYCLOAK-4463 $scope.initEditor = function(editor){ @@ -2802,6 +2806,70 @@ module.controller('ProviderConfigCtrl', function ($modal, $scope, $route, Compon reader.readAsText($files[0]); $scope.fileNames[optionName] = $files[0].name; } + + $scope.addMapEntry = function(optionName) { + $scope.removeMapEntry(optionName, $scope.newMapEntries[optionName].key) + + var parsedMap = JSON.parse($scope.config[optionName]); + parsedMap.push($scope.newMapEntries[optionName]); + $scope.config[optionName] = JSON.stringify(parsedMap); + + delete $scope.newMapEntries[optionName]; + } + + $scope.removeMapEntry = function(optionName, key) { + var parsedMap = JSON.parse($scope.config[optionName]); + + for(var i = parsedMap.length - 1; i >= 0; i--) { + if(parsedMap[i]['key'] === key) { + parsedMap.splice(i, 1); + } + } + + $scope.config[optionName] = JSON.stringify(parsedMap); + } + + $scope.updateMapEntry = function(optionName, key, value) { + var parsedMap = JSON.parse($scope.config[optionName]); + + for(var i = parsedMap.length - 1; i >= 0; i--) { + if(parsedMap[i]['key'] === key) { + parsedMap[i]['value'] = value; + } + } + $scope.config[optionName] = JSON.stringify(parsedMap); + + focusMapValueId = "mapValue-" + optionName + "-" + key; + } + + $scope.jsonParseMap = function(optionName) { + + if(cachedParsedMaps[optionName] === undefined) { + cachedMaps[optionName] = "[]"; + cachedParsedMaps[optionName] = []; + + if(!$scope.config.hasOwnProperty(optionName)){ + $scope.config[optionName]=cachedMaps[optionName]; + } else { + cachedMaps[optionName] = $scope.config[optionName]; + cachedParsedMaps[optionName] = JSON.parse(cachedMaps[optionName]); + } + } + + var mapChanged = $scope.config[optionName] !== cachedMaps[optionName]; + + if(mapChanged){ + cachedMaps[optionName] = $scope.config[optionName]; + cachedParsedMaps[optionName] = JSON.parse(cachedMaps[optionName]); + } + + if(!mapChanged && focusMapValueId !== null){ + document.getElementById(focusMapValueId).focus(); + focusMapValueId = null; + } + + return cachedParsedMaps[optionName]; + } }); module.directive('kcProviderConfig', function ($modal) { diff --git a/themes/src/main/resources/theme/base/admin/resources/templates/kc-provider-config.html b/themes/src/main/resources/theme/base/admin/resources/templates/kc-provider-config.html index 70e5ad74b2..8e1f9e2f61 100755 --- a/themes/src/main/resources/theme/base/admin/resources/templates/kc-provider-config.html +++ b/themes/src/main/resources/theme/base/admin/resources/templates/kc-provider-config.html @@ -55,6 +55,37 @@