8000 Add Prefix option to OIDC User Attribute Mapper by pszabo891 · Pull Request #42002 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@
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 $
Expand All @@ -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 ALLOW_NULLABLE = "allow.nullable.property";
public static final String EMAIL = "email";
public static final String FIRST_NAME = "firstName";
Expand All @@ -59,19 +62,31 @@ public class UserAttributeMapper extends AbstractClaimMapper {
static {
ProviderConfigProperty property;
ProviderConfigProperty property1;
ProviderConfigProperty property2;
ProviderConfigProperty allowNullableProperty;

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);

allowNullableProperty = new ProviderConfigProperty();
allowNullableProperty.setName(ALLOW_NULLABLE);
allowNullableProperty.setLabel("Allow Nullable Property");
Expand All @@ -83,6 +98,8 @@ public class UserAttributeMapper extends AbstractClaimMapper {

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);
Expand Down Expand Up @@ -115,12 +132,27 @@ public String getDisplayType() {

@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);
Expand Down Expand Up @@ -158,14 +190,41 @@ private List<String> toList(Object value) {
.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);
}

boolean isNullableProperty = Boolean.parseBoolean(mapperModel.getConfig().getOrDefault(ALLOW_NULLABLE, Boolean.FALSE.toString()));

if (EMAIL.equalsIgnoreCase(attribute)) {
Expand Down Expand Up @@ -198,7 +257,6 @@ public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserMo

@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.";
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public void addIdentityProviderToConsumerRealm(IdentityProviderMapperSyncMode sy
}
}

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())
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
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, boolean nullable) {
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)
.put(UserAttributeMapper.ALLOW_NULLABLE, Boolean.toString(nullable))
.build());

IdentityProviderMapperRepresentation emailAttrMapper = new IdentityProviderMapperRepresentation();
emailAttrMapper.setName("attribute-mapper-email");
emailAttrMapper.setIdentityProviderMapper(UserAttributeMapper.PROVIDER_ID);
emailAttrMapper.setConfig(ImmutableMap.<String,String>builder()
.put(IdentityProviderMapperModel.SYNC_MODE, syncMode.toString())
.put(UserAttributeMapper.ALLOW_NULLABLE, Boolean.toString(nullable))
.put(UserAttributeMapper.CLAIM, "email")
.put(UserAttributeMapper.USER_ATTRIBUTE, "email")
.build());

return Lists.newArrayList(attrMapper1, emailAttrMapper);
}

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
0