mirror of
https://github.com/keycloak/keycloak.git
synced 2026-02-03 20:39:33 -05:00
Added Prefix option to User Attribute mapper
Signed-off-by: Peter Szabo <petersz.szabo@gmail.com>
This commit is contained in:
parent
6519142f43
commit
ff9ec6f0fe
3 changed files with 165 additions and 7 deletions
|
|
@ -39,6 +39,8 @@ import org.keycloak.models.UserModel;
|
|||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
import org.keycloak.saml.common.util.StringUtil;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
|
|
@ -50,6 +52,7 @@ public class UserAttributeMapper extends AbstractClaimMapper {
|
|||
private static final List<ProviderConfigProperty> configProperties = new ArrayList<>();
|
||||
|
||||
public static final String USER_ATTRIBUTE = "user.attribute";
|
||||
public static final String ATTRIBUTE_PREFFIX = "attribute.prefix";
|
||||
public static final String EMAIL = "email";
|
||||
public static final String FIRST_NAME = "firstName";
|
||||
public static final String LAST_NAME = "lastName";
|
||||
|
|
@ -58,22 +61,35 @@ public class UserAttributeMapper extends AbstractClaimMapper {
|
|||
static {
|
||||
ProviderConfigProperty property;
|
||||
ProviderConfigProperty property1;
|
||||
ProviderConfigProperty property2;
|
||||
|
||||
property1 = new ProviderConfigProperty();
|
||||
property1.setName(CLAIM);
|
||||
property1.setLabel("Claim");
|
||||
property1.setHelpText("Name of claim to search for in token. You can reference nested claims using a '.', i.e. 'address.locality'. To use dot (.) literally, escape it with backslash (\\.)");
|
||||
property1.setType(ProviderConfigProperty.STRING_TYPE);
|
||||
configProperties.add(property1);
|
||||
|
||||
property = new ProviderConfigProperty();
|
||||
property.setName(USER_ATTRIBUTE);
|
||||
property.setLabel("User Attribute Name");
|
||||
property.setHelpText("User attribute name to store claim. Use email, lastName, and firstName to map to those predefined user properties.");
|
||||
property.setType(ProviderConfigProperty.USER_PROFILE_ATTRIBUTE_LIST_TYPE);
|
||||
configProperties.add(property);
|
||||
|
||||
property2 = new ProviderConfigProperty();
|
||||
property2.setName(ATTRIBUTE_PREFFIX);
|
||||
property2.setLabel("Attribute Value Prefix");
|
||||
property2.setHelpText(
|
||||
"Optional prefix to be concatenated in front of every imported attribute value.");
|
||||
property2.setType(ProviderConfigProperty.STRING_TYPE);
|
||||
configProperties.add(property2);
|
||||
}
|
||||
|
||||
public static final String PROVIDER_ID = "oidc-user-attribute-idp-mapper";
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(UserAttributeMapper.class);
|
||||
|
||||
@Override
|
||||
public boolean supportsSyncMode(IdentityProviderSyncMode syncMode) {
|
||||
return IDENTITY_PROVIDER_SYNC_MODES.contains(syncMode);
|
||||
|
|
@ -106,12 +122,27 @@ public class UserAttributeMapper extends AbstractClaimMapper {
|
|||
|
||||
@Override
|
||||
public void preprocessFederatedIdentity(KeycloakSession session, RealmModel realm, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
LOG.debug("executing preprocessFederatedIdentity()");
|
||||
|
||||
String attribute = mapperModel.getConfig().get(USER_ATTRIBUTE);
|
||||
if(StringUtil.isNullOrEmpty(attribute)){
|
||||
String prefix = Objects.toString(mapperModel.getConfig().get(ATTRIBUTE_PREFFIX), "");
|
||||
|
||||
if (StringUtil.isNullOrEmpty(attribute)) {
|
||||
LOG.debug("Empty attribute, not processing.");
|
||||
return;
|
||||
}
|
||||
|
||||
Object value = getClaimValue(mapperModel, context);
|
||||
List<String> values = toList(value);
|
||||
List<String> values;
|
||||
|
||||
if (StringUtil.isNullOrEmpty(prefix)) {
|
||||
LOG.debug("No attribute prefix configured.");
|
||||
values = toList(value);
|
||||
}
|
||||
else {
|
||||
LOG.debug("Retrieved prefix: " + prefix);
|
||||
values = toPrefixedList(value, prefix);
|
||||
}
|
||||
|
||||
if (EMAIL.equalsIgnoreCase(attribute)) {
|
||||
setIfNotEmpty(context::setEmail, values);
|
||||
|
|
@ -145,14 +176,41 @@ public class UserAttributeMapper extends AbstractClaimMapper {
|
|||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private List<String> toPrefixedList(Object value, String prefix) {
|
||||
List<Object> values = (value instanceof List)
|
||||
? (List) value
|
||||
: Collections.singletonList(value);
|
||||
|
||||
return values.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.map(item -> prefix.concat(item.toString()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
LOG.debug("executing updateBrokeredUser()");
|
||||
|
||||
String attribute = mapperModel.getConfig().get(USER_ATTRIBUTE);
|
||||
if(StringUtil.isNullOrEmpty(attribute)){
|
||||
String prefix = Objects.toString(mapperModel.getConfig().get(ATTRIBUTE_PREFFIX), "");
|
||||
|
||||
if (StringUtil.isNullOrEmpty(attribute)) {
|
||||
LOG.debug("Empty attribute, not processing.");
|
||||
return;
|
||||
}
|
||||
|
||||
Object value = getClaimValue(mapperModel, context);
|
||||
List<String> values = toList(value);
|
||||
List<String> values;
|
||||
|
||||
if (StringUtil.isNullOrEmpty(prefix)) {
|
||||
LOG.debug("No attribute prefix configured.");
|
||||
values = toList(value);
|
||||
}
|
||||
else {
|
||||
LOG.debug("Retrieved prefix: " + prefix);
|
||||
values = toPrefixedList(value, prefix);
|
||||
}
|
||||
|
||||
if (EMAIL.equalsIgnoreCase(attribute)) {
|
||||
setIfNotEmpty(user::setEmail, values);
|
||||
} else if (FIRST_NAME.equalsIgnoreCase(attribute)) {
|
||||
|
|
@ -171,7 +229,6 @@ public class UserAttributeMapper extends AbstractClaimMapper {
|
|||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "Import declared claim if it exists in ID, access token or the claim set returned by the user profile endpoint into the specified user property or attribute.";
|
||||
return "Import declared claim if it exists in ID, access token or the claim set returned by the user profile endpoint into the specified user property or attribute and optionally prefix each attribute value with the provided prefix.";
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ public abstract class AbstractUserAttributeMapperTest extends AbstractIdentityPr
|
|||
}
|
||||
}
|
||||
|
||||
private void assertUserAttributes(Map<String, List<String>> attrs, UserRepresentation userRep) {
|
||||
protected void assertUserAttributes(Map<String, List<String>> attrs, UserRepresentation userRep) {
|
||||
Set<String> mappedAttrNames = attrs.entrySet().stream()
|
||||
.filter(me -> me.getValue() != null && ! me.getValue().isEmpty())
|
||||
.map(me -> me.getKey())
|
||||
|
|
|
|||
|
|
@ -0,0 +1,101 @@
|
|||
package org.keycloak.testsuite.broker;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.keycloak.broker.oidc.mappers.UserAttributeMapper;
|
||||
import org.keycloak.models.IdentityProviderMapperModel;
|
||||
import org.keycloak.models.IdentityProviderMapperSyncMode;
|
||||
import org.keycloak.representations.idm.IdentityProviderMapperRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
import static org.keycloak.testsuite.broker.KcSamlBrokerConfiguration.ATTRIBUTE_TO_MAP_FRIENDLY_NAME;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.containsInAnyOrder;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.notNullValue;
|
||||
import static org.hamcrest.Matchers.nullValue;
|
||||
|
||||
public class OidcPrefixedUserAttributeMapperTest extends AbstractUserAttributeMapperTest {
|
||||
|
||||
protected static final String MAPPED_ATTRIBUTE_NAME = "mapped-user-attribute";
|
||||
protected static final String MAPPED_ATTRIBUTE_FRIENDLY_NAME = "mapped-user-attribute-friendly";
|
||||
|
||||
private static final String PREFIX = "prefix_";
|
||||
private static final Set<String> PROTECTED_NAMES = ImmutableSet.<String>builder().add("email").add("lastName").add("firstName").build();
|
||||
private static final Map<String, String> ATTRIBUTE_NAME_TRANSLATION = ImmutableMap.<String, String>builder()
|
||||
.put("dotted.email", "dotted.email")
|
||||
.put("nested.email", "nested.email")
|
||||
.put(ATTRIBUTE_TO_MAP_FRIENDLY_NAME, MAPPED_ATTRIBUTE_FRIENDLY_NAME)
|
||||
.put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, MAPPED_ATTRIBUTE_NAME)
|
||||
.build();
|
||||
|
||||
@Override
|
||||
protected BrokerConfiguration getBrokerConfiguration() {
|
||||
return KcOidcBrokerConfiguration.INSTANCE;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Iterable<IdentityProviderMapperRepresentation> createIdentityProviderMappers(IdentityProviderMapperSyncMode syncMode) {
|
||||
IdentityProviderMapperRepresentation attrMapper1 = new IdentityProviderMapperRepresentation();
|
||||
attrMapper1.setName("prefixed-attribute-mapper");
|
||||
attrMapper1.setIdentityProviderMapper(UserAttributeMapper.PROVIDER_ID);
|
||||
attrMapper1.setConfig(ImmutableMap.<String,String>builder()
|
||||
.put(IdentityProviderMapperModel.SYNC_MODE, syncMode.toString())
|
||||
.put(UserAttributeMapper.CLAIM, KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME)
|
||||
.put(UserAttributeMapper.USER_ATTRIBUTE, MAPPED_ATTRIBUTE_NAME)
|
||||
.put(UserAttributeMapper.ATTRIBUTE_PREFFIX, PREFIX)
|
||||
.build());
|
||||
|
||||
return Lists.newArrayList(attrMapper1);
|
||||
}
|
||||
|
||||
private List<String> toPrefixedList(Object value, String prefix) {
|
||||
List<Object> values = (value.getClass().isArray())
|
||||
? Arrays.asList((Object[]) value)
|
||||
: Collections.singletonList(value);
|
||||
|
||||
return values.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.map(item -> prefix.concat(item.toString()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void assertUserAttributes(Map<String, List<String>> attrs, UserRepresentation userRep) {
|
||||
Set<String> mappedAttrNames = attrs.entrySet().stream()
|
||||
.filter(me -> me.getValue() != null && ! me.getValue().isEmpty())
|
||||
.map(me -> me.getKey())
|
||||
.filter(a -> ! PROTECTED_NAMES.contains(a))
|
||||
.map(ATTRIBUTE_NAME_TRANSLATION::get)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
if (mappedAttrNames.isEmpty()) {
|
||||
assertThat("No attributes are expected to be present", userRep.getAttributes(), nullValue());
|
||||
} else if (attrs.containsKey("email")) {
|
||||
assertThat(userRep.getEmail(), equalTo(attrs.get("email").get(0)));
|
||||
} else {
|
||||
assertThat(userRep.getAttributes(), notNullValue());
|
||||
assertThat(userRep.getAttributes().keySet(), equalTo(mappedAttrNames));
|
||||
for (Map.Entry<String, List<String>> me : attrs.entrySet()) {
|
||||
String mappedAttrName = ATTRIBUTE_NAME_TRANSLATION.get(me.getKey());
|
||||
if (mappedAttrNames.contains(mappedAttrName)) {
|
||||
log.info(userRep.getAttributes());
|
||||
assertThat(userRep.getAttributes().get(mappedAttrName), containsInAnyOrder(toPrefixedList(me.getValue().toArray(), PREFIX).toArray()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Reference in a new issue