KEYCLOAK-12757 New Identity Provider Mapper "Advanced Claim to Role Mapper" with

following features

    * Regex support for claim values.
    * Support for multiple claims.
This commit is contained in:
Benjamin Weimer 2019-09-24 09:57:16 +02:00 committed by Stian Thorgersen
parent 210fd92d23
commit dd9ad305ca
14 changed files with 661 additions and 22 deletions

View file

@ -17,8 +17,14 @@
package org.keycloak.models; 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.io.Serializable;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors;
/** /**
* Specifies a mapping from broker login to user data. * Specifies a mapping from broker login to user data.
@ -28,6 +34,9 @@ import java.util.Map;
*/ */
public class IdentityProviderMapperModel implements Serializable { public class IdentityProviderMapperModel implements Serializable {
private static final TypeReference<List<StringPair>> MAP_TYPE_REPRESENTATION = new TypeReference<List<StringPair>>() {
};
protected String id; protected String id;
protected String name; protected String name;
protected String identityProviderAlias; protected String identityProviderAlias;
@ -75,6 +84,17 @@ public class IdentityProviderMapperModel implements Serializable {
this.config = config; this.config = config;
} }
public Map<String, String> getConfigMap(String configKey) {
String configMap = config.get(configKey);
try {
List<StringPair> 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 @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;
@ -91,4 +111,25 @@ public class IdentityProviderMapperModel implements Serializable {
public int hashCode() { public int hashCode() {
return id.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;
}
}
} }

View file

@ -57,6 +57,11 @@ public class ProviderConfigProperty {
*/ */
public static final String TEXT_TYPE="Text"; public static final String TEXT_TYPE="Text";
/**
* Configure multiple (key, value) pairs
*/
public static final String MAP_TYPE ="Map";
protected String name; protected String name;
protected String label; protected String label;
protected String helpText; protected String helpText;

View file

@ -118,7 +118,7 @@ public abstract class AbstractClaimMapper extends AbstractIdentityProviderMapper
} else if (value instanceof List) { } else if (value instanceof List) {
List list = (List)value; List list = (List)value;
for (Object val : list) { for (Object val : list) {
if (valueEquals(desiredValue, val)) return true; if (valueEquals(desiredValue, val)) return true;
} }
} else if (value instanceof JsonNode) { } else if (value instanceof JsonNode) {
try { try {

View file

@ -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 <a href="mailto:bill@burkecentral.com">Bill Burke, Benjamin Weimer</a>
* @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<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
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<ProviderConfigProperty> 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<String, String> claims = mapperModel.getConfigMap(CLAIM_PROPERTY_NAME);
Boolean areClaimValuesRegex = Boolean.valueOf(mapperModel.getConfig().get(ARE_CLAIM_VALUES_REGEX_PROPERTY_NAME));
for (Map.Entry<String, String> 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;
}
}

View file

@ -19,6 +19,7 @@ org.keycloak.broker.provider.HardcodedRoleMapper
org.keycloak.broker.provider.HardcodedAttributeMapper org.keycloak.broker.provider.HardcodedAttributeMapper
org.keycloak.broker.provider.HardcodedUserSessionAttributeMapper org.keycloak.broker.provider.HardcodedUserSessionAttributeMapper
org.keycloak.broker.oidc.mappers.ClaimToRoleMapper org.keycloak.broker.oidc.mappers.ClaimToRoleMapper
org.keycloak.broker.oidc.mappers.AdvancedClaimToRoleMapper
org.keycloak.broker.oidc.mappers.ExternalKeycloakRoleToRoleMapper org.keycloak.broker.oidc.mappers.ExternalKeycloakRoleToRoleMapper
org.keycloak.broker.oidc.mappers.UserAttributeMapper org.keycloak.broker.oidc.mappers.UserAttributeMapper
org.keycloak.broker.oidc.mappers.UsernameTemplateMapper org.keycloak.broker.oidc.mappers.UsernameTemplateMapper

View file

@ -361,12 +361,12 @@ public class IdentityProviderTest extends AbstractAdminTest {
create(createRep("keycloak-oidc", "keycloak-oidc")); create(createRep("keycloak-oidc", "keycloak-oidc"));
provider = realm.identityProviders().get("keycloak-oidc"); provider = realm.identityProviders().get("keycloak-oidc");
mapperTypes = provider.getMapperTypes(); 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")); create(createRep("oidc", "oidc"));
provider = realm.identityProviders().get("oidc"); provider = realm.identityProviders().get("oidc");
mapperTypes = provider.getMapperTypes(); 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")); create(createRep("saml", "saml"));
provider = realm.identityProviders().get("saml"); provider = realm.identityProviders().get("saml");

View file

@ -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_NAME = "mapped-user-attribute";
protected static final String MAPPED_ATTRIBUTE_FRIENDLY_NAME = "mapped-user-attribute-friendly"; 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"; protected static final String ATTRIBUTE_TO_MAP_FRIENDLY_NAME = "user-attribute-friendly";
private static final Set<String> PROTECTED_NAMES = ImmutableSet.<String>builder().add("email").add("lastName").add("firstName").build(); private static final Set<String> PROTECTED_NAMES = ImmutableSet.<String>builder().add("email").add("lastName").add("firstName").build();
@ -40,7 +39,7 @@ public abstract class AbstractUserAttributeMapperTest extends AbstractBaseBroker
.put("dotted.email", "dotted.email") .put("dotted.email", "dotted.email")
.put("nested.email", "nested.email") .put("nested.email", "nested.email")
.put(ATTRIBUTE_TO_MAP_FRIENDLY_NAME, MAPPED_ATTRIBUTE_FRIENDLY_NAME) .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(); .build();
protected abstract Iterable<IdentityProviderMapperRepresentation> createIdentityProviderMappers(); protected abstract Iterable<IdentityProviderMapperRepresentation> createIdentityProviderMappers();
@ -188,10 +187,10 @@ public abstract class AbstractUserAttributeMapperTest extends AbstractBaseBroker
@Test @Test
public void testBasicMappingSingleValue() { public void testBasicMappingSingleValue() {
testValueMapping(ImmutableMap.<String, List<String>>builder() testValueMapping(ImmutableMap.<String, List<String>>builder()
.put(ATTRIBUTE_TO_MAP_NAME, ImmutableList.<String>builder().add("value 1").build()) .put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.<String>builder().add("value 1").build())
.build(), .build(),
ImmutableMap.<String, List<String>>builder() ImmutableMap.<String, List<String>>builder()
.put(ATTRIBUTE_TO_MAP_NAME, ImmutableList.<String>builder().add("second value").build()) .put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.<String>builder().add("second value").build())
.build() .build()
); );
} }
@ -214,10 +213,10 @@ public abstract class AbstractUserAttributeMapperTest extends AbstractBaseBroker
@Test @Test
public void testBasicMappingClearValue() { public void testBasicMappingClearValue() {
testValueMapping(ImmutableMap.<String, List<String>>builder() testValueMapping(ImmutableMap.<String, List<String>>builder()
.put(ATTRIBUTE_TO_MAP_NAME, ImmutableList.<String>builder().add("value 1").build()) .put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.<String>builder().add("value 1").build())
.build(), .build(),
ImmutableMap.<String, List<String>>builder() ImmutableMap.<String, List<String>>builder()
.put(ATTRIBUTE_TO_MAP_NAME, ImmutableList.<String>builder().build()) .put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.<String>builder().build())
.build() .build()
); );
} }
@ -225,7 +224,7 @@ public abstract class AbstractUserAttributeMapperTest extends AbstractBaseBroker
@Test @Test
public void testBasicMappingRemoveValue() { public void testBasicMappingRemoveValue() {
testValueMapping(ImmutableMap.<String, List<String>>builder() testValueMapping(ImmutableMap.<String, List<String>>builder()
.put(ATTRIBUTE_TO_MAP_NAME, ImmutableList.<String>builder().add("value 1").build()) .put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.<String>builder().add("value 1").build())
.build(), .build(),
ImmutableMap.<String, List<String>>builder() ImmutableMap.<String, List<String>>builder()
.build() .build()
@ -235,10 +234,10 @@ public abstract class AbstractUserAttributeMapperTest extends AbstractBaseBroker
@Test @Test
public void testBasicMappingMultipleValues() { public void testBasicMappingMultipleValues() {
testValueMapping(ImmutableMap.<String, List<String>>builder() testValueMapping(ImmutableMap.<String, List<String>>builder()
.put(ATTRIBUTE_TO_MAP_NAME, ImmutableList.<String>builder().add("value 1").add("value 2").build()) .put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.<String>builder().add("value 1").add("value 2").build())
.build(), .build(),
ImmutableMap.<String, List<String>>builder() ImmutableMap.<String, List<String>>builder()
.put(ATTRIBUTE_TO_MAP_NAME, ImmutableList.<String>builder().add("second value").add("second value 2").build()) .put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.<String>builder().add("second value").add("second value 2").build())
.build() .build()
); );
} }
@ -248,7 +247,7 @@ public abstract class AbstractUserAttributeMapperTest extends AbstractBaseBroker
testValueMapping(ImmutableMap.<String, List<String>>builder() testValueMapping(ImmutableMap.<String, List<String>>builder()
.build(), .build(),
ImmutableMap.<String, List<String>>builder() ImmutableMap.<String, List<String>>builder()
.put(ATTRIBUTE_TO_MAP_NAME, ImmutableList.<String>builder().add("second value").add("second value 2").build()) .put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.<String>builder().add("second value").add("second value 2").build())
.build() .build()
); );
} }
@ -256,7 +255,7 @@ public abstract class AbstractUserAttributeMapperTest extends AbstractBaseBroker
@Test @Test
public void testDeleteBasicMappingMultipleValues() { public void testDeleteBasicMappingMultipleValues() {
testValueMapping(ImmutableMap.<String, List<String>>builder() testValueMapping(ImmutableMap.<String, List<String>>builder()
.put(ATTRIBUTE_TO_MAP_NAME, ImmutableList.<String>builder().add("second value").add("second value 2").build()) .put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.<String>builder().add("second value").add("second value 2").build())
.build(), .build(),
ImmutableMap.<String, List<String>>builder() ImmutableMap.<String, List<String>>builder()
.build() .build()

View file

@ -21,13 +21,15 @@ import static org.keycloak.testsuite.broker.BrokerTestConstants.*;
import static org.keycloak.testsuite.broker.BrokerTestTools.*; import static org.keycloak.testsuite.broker.BrokerTestTools.*;
/** /**
*
* @author hmlnarik * @author hmlnarik
*/ */
public class KcOidcBrokerConfiguration implements BrokerConfiguration { public class KcOidcBrokerConfiguration implements BrokerConfiguration {
public static final KcOidcBrokerConfiguration INSTANCE = new KcOidcBrokerConfiguration(); 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 @Override
public RealmRepresentation createProviderRealm() { public RealmRepresentation createProviderRealm() {
RealmRepresentation realm = new RealmRepresentation(); RealmRepresentation realm = new RealmRepresentation();
@ -106,15 +108,29 @@ public class KcOidcBrokerConfiguration implements BrokerConfiguration {
userAttrMapper.setProtocolMapper(UserAttributeMapper.PROVIDER_ID); userAttrMapper.setProtocolMapper(UserAttributeMapper.PROVIDER_ID);
Map<String, String> userAttrMapperConfig = userAttrMapper.getConfig(); Map<String, String> userAttrMapperConfig = userAttrMapper.getConfig();
userAttrMapperConfig.put(ProtocolMapperUtils.USER_ATTRIBUTE, AbstractUserAttributeMapperTest.ATTRIBUTE_TO_MAP_NAME); userAttrMapperConfig.put(ProtocolMapperUtils.USER_ATTRIBUTE, ATTRIBUTE_TO_MAP_NAME);
userAttrMapperConfig.put(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME, AbstractUserAttributeMapperTest.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.JSON_TYPE, ProviderConfigProperty.STRING_TYPE);
userAttrMapperConfig.put(OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN, "true"); userAttrMapperConfig.put(OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN, "true");
userAttrMapperConfig.put(OIDCAttributeMapperHelper.INCLUDE_IN_ID_TOKEN, "true"); userAttrMapperConfig.put(OIDCAttributeMapperHelper.INCLUDE_IN_ID_TOKEN, "true");
userAttrMapperConfig.put(OIDCAttributeMapperHelper.INCLUDE_IN_USERINFO, "true"); userAttrMapperConfig.put(OIDCAttributeMapperHelper.INCLUDE_IN_USERINFO, "true");
userAttrMapperConfig.put(ProtocolMapperUtils.MULTIVALUED, "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<String, String> 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); return Collections.singletonList(client);
} }

View file

@ -124,8 +124,8 @@ public class KcSamlBrokerConfiguration implements BrokerConfiguration {
userAttrMapper.setProtocolMapper(UserAttributeStatementMapper.PROVIDER_ID); userAttrMapper.setProtocolMapper(UserAttributeStatementMapper.PROVIDER_ID);
Map<String, String> userAttrMapperConfig = userAttrMapper.getConfig(); Map<String, String> userAttrMapperConfig = userAttrMapper.getConfig();
userAttrMapperConfig.put(ProtocolMapperUtils.USER_ATTRIBUTE, AbstractUserAttributeMapperTest.ATTRIBUTE_TO_MAP_NAME); userAttrMapperConfig.put(ProtocolMapperUtils.USER_ATTRIBUTE, KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME);
userAttrMapperConfig.put(AttributeStatementHelper.SAML_ATTRIBUTE_NAME, AbstractUserAttributeMapperTest.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.SAML_ATTRIBUTE_NAMEFORMAT, AttributeStatementHelper.BASIC);
userAttrMapperConfig.put(AttributeStatementHelper.FRIENDLY_NAME, ""); userAttrMapperConfig.put(AttributeStatementHelper.FRIENDLY_NAME, "");

View file

@ -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, <a href="mailto:external.benjamin.weimer@bosch-si.com">Benjamin Weimer</a>
*/
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<ClientRepresentation> 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.<String, List<String>>builder()
.put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.<String>builder().add("value 1").build())
.put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME_2, ImmutableList.<String>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.<String, List<String>>builder()
.put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.<String>builder().add("value 1").build())
.put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME_2, ImmutableList.<String>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.<String, List<String>>builder()
.put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.<String>builder().add("some value").build())
.put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME_2, ImmutableList.<String>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.<String, List<String>>builder()
.put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.<String>builder().add("value 1").build())
.put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME_2, ImmutableList.<String>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.<String, List<String>>builder()
.put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.<String>builder().add("mismatch").build())
.put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME_2, ImmutableList.<String>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.<String, List<String>>builder()
.put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.<String>builder().add("value 1").build())
.put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME_2, ImmutableList.<String>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<String, List<String>> mismatchingAttributes = ImmutableMap.<String, List<String>>builder()
.put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.<String>builder().add("value 1").build())
.put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME_2, ImmutableList.<String>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.<String, List<String>>builder()
.put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.<String>builder().add("value 1").build())
.put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME_2, ImmutableList.<String>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<String, List<String>> matchingAttributes = ImmutableMap.<String, List<String>>builder()
.put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.<String>builder().add("value 1").build())
.put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME_2, ImmutableList.<String>builder().add("value 2").build())
.put("some.other.attribute", ImmutableList.<String>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.<String, String>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<String, List<String>> 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<UserRepresentation> 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<String> realmRoles = roles.getRealmMappings().stream()
.map(RoleRepresentation::getName)
.collect(Collectors.toList());
user.setRealmRoles(realmRoles);
Map<String, List<String>> 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)));
}
}

View file

@ -19,7 +19,7 @@ public class OidcUserAttributeMapperTest extends AbstractUserAttributeMapperTest
attrMapper1.setName("attribute-mapper"); attrMapper1.setName("attribute-mapper");
attrMapper1.setIdentityProviderMapper(UserAttributeMapper.PROVIDER_ID); attrMapper1.setIdentityProviderMapper(UserAttributeMapper.PROVIDER_ID);
attrMapper1.setConfig(ImmutableMap.<String,String>builder() attrMapper1.setConfig(ImmutableMap.<String,String>builder()
.put(UserAttributeMapper.CLAIM, ATTRIBUTE_TO_MAP_NAME) .put(UserAttributeMapper.CLAIM, KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME)
.put(UserAttributeMapper.USER_ATTRIBUTE, MAPPED_ATTRIBUTE_NAME) .put(UserAttributeMapper.USER_ATTRIBUTE, MAPPED_ATTRIBUTE_NAME)
.build()); .build());

View file

@ -43,7 +43,7 @@ public class SamlUserAttributeMapperTest extends AbstractUserAttributeMapperTest
attrMapper1.setName("attribute-mapper"); attrMapper1.setName("attribute-mapper");
attrMapper1.setIdentityProviderMapper(UserAttributeMapper.PROVIDER_ID); attrMapper1.setIdentityProviderMapper(UserAttributeMapper.PROVIDER_ID);
attrMapper1.setConfig(ImmutableMap.<String,String>builder() attrMapper1.setConfig(ImmutableMap.<String,String>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) .put(UserAttributeMapper.USER_ATTRIBUTE, MAPPED_ATTRIBUTE_NAME)
.build()); .build());

View file

@ -2727,6 +2727,10 @@ module.controller('RoleSelectorModalCtrl', function($scope, realm, config, confi
module.controller('ProviderConfigCtrl', function ($modal, $scope, $route, ComponentUtils, Client) { module.controller('ProviderConfigCtrl', function ($modal, $scope, $route, ComponentUtils, Client) {
clientSelectControl($scope, $route.current.params.realm, Client); clientSelectControl($scope, $route.current.params.realm, Client);
$scope.fileNames = {}; $scope.fileNames = {};
$scope.newMapEntries = {};
var cachedMaps = {};
var cachedParsedMaps = {};
var focusMapValueId = null;
// KEYCLOAK-4463 // KEYCLOAK-4463
$scope.initEditor = function(editor){ $scope.initEditor = function(editor){
@ -2802,6 +2806,70 @@ module.controller('ProviderConfigCtrl', function ($modal, $scope, $route, Compon
reader.readAsText($files[0]); reader.readAsText($files[0]);
$scope.fileNames[optionName] = $files[0].name; $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) { module.directive('kcProviderConfig', function ($modal) {

View file

@ -55,6 +55,37 @@
<textarea class="form-control" data-ng-model="config[ option.name ]"/> <textarea class="form-control" data-ng-model="config[ option.name ]"/>
</div> </div>
<div class="col-md-6" data-ng-if="option.type == 'Map'">
<table class="table table-striped table-bordered">
<thead>
<tr>
<th>{{:: 'key' | translate}}</th>
<th>{{:: 'value' | translate}}</th>
<th>{{:: 'actions' | translate}}</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="mapEntry in jsonParseMap(option.name)">
<td>{{mapEntry['key']}}</td>
<td><input ng-model="mapEntry['value']"
ng-change="updateMapEntry(option.name, mapEntry['key'], mapEntry['value'])"
class="form-control" type="text" name="{{mapEntry['key']}}" id="mapValue-{{option.name}}-{{mapEntry['key']}}"/></td>
<td class="kc-action-cell" id="removeMapEntry-{{option.name}}" data-ng-click="removeMapEntry(option.name, mapEntry['key'])">{{:: 'delete' | translate}}
</td>
</tr>
<tr>
<td><input ng-model="newMapEntries[option.name].key" class="form-control" type="text" id="newMapEntryKey-{{option.name}}"/></td>
<td><input ng-model="newMapEntries[option.name].value" class="form-control" type="text" id="newMapEntryValue-{{option.name}}"/></td>
<td class="kc-action-cell" id="addMapEntry-{{option.name}}" data-ng-click="addMapEntry(option.name)"
data-ng-disabled="!newMapEntry.key.length || !newMapEntry.value.length">{{:: 'add' | translate}}
</td>
</tr>
</tbody>
</table>
</div>
<kc-tooltip>{{:: option.helpText | translate}}</kc-tooltip> <kc-tooltip>{{:: option.helpText | translate}}</kc-tooltip>
</div> </div>
</div> </div>