KEYCLOAK-3648

This commit is contained in:
Manuel Palacio 2016-12-01 19:34:33 +01:00
parent bed592460d
commit bfec073457
12 changed files with 262 additions and 122 deletions

View file

@ -161,6 +161,10 @@ public class BrokeredIdentityContext {
getContextData().put(Constants.USER_ATTRIBUTES_PREFIX + attributeName, list); getContextData().put(Constants.USER_ATTRIBUTES_PREFIX + attributeName, list);
} }
public void setUserAttribute(String attributeName, List<String> attributeValues) {
getContextData().put(Constants.USER_ATTRIBUTES_PREFIX + attributeName, attributeValues);
}
public String getUserAttribute(String attributeName) { public String getUserAttribute(String attributeName) {
List<String> userAttribute = (List<String>) getContextData().get(Constants.USER_ATTRIBUTES_PREFIX + attributeName); List<String> userAttribute = (List<String>) getContextData().get(Constants.USER_ATTRIBUTES_PREFIX + attributeName);
if (userAttribute == null || userAttribute.isEmpty()) { if (userAttribute == null || userAttribute.isEmpty()) {

View file

@ -76,7 +76,7 @@ public abstract class AbstractClaimMapper extends AbstractIdentityProviderMapper
{ {
// Search the OIDC UserInfo claim set (if any) // Search the OIDC UserInfo claim set (if any)
JsonNode profileJsonNode = (JsonNode) context.getContextData().get(OIDCIdentityProvider.USER_INFO); JsonNode profileJsonNode = (JsonNode) context.getContextData().get(OIDCIdentityProvider.USER_INFO);
String value = AbstractJsonUserAttributeMapper.getJsonValue(profileJsonNode, claim); Object value = AbstractJsonUserAttributeMapper.getJsonValue(profileJsonNode, claim);
if (value != null) return value; if (value != null) return value;
} }
return null; return null;

View file

@ -35,7 +35,7 @@ import java.util.List;
* Abstract class for Social Provider mappers which allow mapping of JSON user profile field into Keycloak user * Abstract class for Social Provider mappers which allow mapping of JSON user profile field into Keycloak user
* attribute. Concrete mapper classes with own ID and provider mapping must be implemented for each social provider who * attribute. Concrete mapper classes with own ID and provider mapping must be implemented for each social provider who
* uses {@link JsonNode} user profile. * uses {@link JsonNode} user profile.
* *
* @author Vlastimil Elias (velias at redhat dot com) * @author Vlastimil Elias (velias at redhat dot com)
*/ */
public abstract class AbstractJsonUserAttributeMapper extends AbstractIdentityProviderMapper { public abstract class AbstractJsonUserAttributeMapper extends AbstractIdentityProviderMapper {
@ -81,8 +81,8 @@ public abstract class AbstractJsonUserAttributeMapper extends AbstractIdentityPr
} }
/** /**
* Store used profile JsonNode into user context for later use by this mapper. Profile data are dumped into special logger if enabled also to allow investigation of the structure. * Store used profile JsonNode into user context for later use by this mapper. Profile data are dumped into special logger if enabled also to allow investigation of the structure.
* *
* @param user context to store profile data into * @param user context to store profile data into
* @param profile to store into context * @param profile to store into context
* @param provider identification of social provider to be used in log dump * @param provider identification of social provider to be used in log dump
@ -125,9 +125,13 @@ public abstract class AbstractJsonUserAttributeMapper extends AbstractIdentityPr
} }
attribute = attribute.trim(); attribute = attribute.trim();
String value = getJsonValue(mapperModel, context); Object value = getJsonValue(mapperModel, context);
if (value != null) { if (value != null) {
context.setUserAttribute(attribute, value); if (value instanceof List) {
context.setUserAttribute(attribute, (List<String>) value);
} else {
context.setUserAttribute(attribute, value.toString());
}
} }
} }
@ -136,7 +140,7 @@ public abstract class AbstractJsonUserAttributeMapper extends AbstractIdentityPr
// we do not update user profile from social provider // we do not update user profile from social provider
} }
protected static String getJsonValue(IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { protected static Object getJsonValue(IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
String jsonField = mapperModel.getConfig().get(CONF_JSON_FIELD); String jsonField = mapperModel.getConfig().get(CONF_JSON_FIELD);
if (jsonField == null || jsonField.trim().isEmpty()) { if (jsonField == null || jsonField.trim().isEmpty()) {
@ -152,7 +156,7 @@ public abstract class AbstractJsonUserAttributeMapper extends AbstractIdentityPr
JsonNode profileJsonNode = (JsonNode) context.getContextData().get(CONTEXT_JSON_NODE); JsonNode profileJsonNode = (JsonNode) context.getContextData().get(CONTEXT_JSON_NODE);
String value = getJsonValue(profileJsonNode, jsonField); Object value = getJsonValue(profileJsonNode, jsonField);
if (value == null) { if (value == null) {
logger.debugf("User profile JSON value '%s' is not available.", jsonField); logger.debugf("User profile JSON value '%s' is not available.", jsonField);
@ -161,7 +165,7 @@ public abstract class AbstractJsonUserAttributeMapper extends AbstractIdentityPr
return value; return value;
} }
public static String getJsonValue(JsonNode baseNode, String fieldPath) { public static Object getJsonValue(JsonNode baseNode, String fieldPath) {
logger.debug("Going to process JsonNode path " + fieldPath + " on data " + baseNode); logger.debug("Going to process JsonNode path " + fieldPath + " on data " + baseNode);
if (baseNode != null) { if (baseNode != null) {
@ -206,6 +210,20 @@ public abstract class AbstractJsonUserAttributeMapper extends AbstractIdentityPr
} }
if (idx < 0) { if (idx < 0) {
if (currentNode.isArray()) {
List<String> values = new ArrayList<>();
for (JsonNode childNode : currentNode) {
if (childNode.isTextual()) {
values.add(childNode.textValue());
} else {
logger.warn("JsonNode in array is not text value " + childNode);
}
}
if (values.isEmpty()) {
return null;
}
return arrayIndex == idx? values : null;
}
if (!currentNode.isValueNode()) { if (!currentNode.isValueNode()) {
logger.debug("JsonNode is not value node for name " + currentFieldName); logger.debug("JsonNode is not value node for name " + currentFieldName);
return null; return null;

View file

@ -20,14 +20,20 @@ package org.keycloak.broker.oidc.mappers;
import org.keycloak.broker.oidc.KeycloakOIDCIdentityProviderFactory; import org.keycloak.broker.oidc.KeycloakOIDCIdentityProviderFactory;
import org.keycloak.broker.oidc.OIDCIdentityProviderFactory; import org.keycloak.broker.oidc.OIDCIdentityProviderFactory;
import org.keycloak.broker.provider.BrokeredIdentityContext; import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.common.util.CollectionUtil;
import org.keycloak.models.IdentityProviderMapperModel; import org.keycloak.models.IdentityProviderMapperModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.saml.common.util.StringUtil;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.stream.Collectors;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -37,9 +43,12 @@ public class UserAttributeMapper extends AbstractClaimMapper {
public static final String[] COMPATIBLE_PROVIDERS = {KeycloakOIDCIdentityProviderFactory.PROVIDER_ID, OIDCIdentityProviderFactory.PROVIDER_ID}; public static final String[] COMPATIBLE_PROVIDERS = {KeycloakOIDCIdentityProviderFactory.PROVIDER_ID, OIDCIdentityProviderFactory.PROVIDER_ID};
private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>(); private static final List<ProviderConfigProperty> configProperties = new ArrayList<>();
public static final String USER_ATTRIBUTE = "user.attribute"; public static final String USER_ATTRIBUTE = "user.attribute";
public static final String EMAIL = "email";
public static final String FIRST_NAME = "firstName";
public static final String LAST_NAME = "lastName";
static { static {
ProviderConfigProperty property; ProviderConfigProperty property;
@ -88,37 +97,63 @@ public class UserAttributeMapper extends AbstractClaimMapper {
@Override @Override
public void preprocessFederatedIdentity(KeycloakSession session, RealmModel realm, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { public void preprocessFederatedIdentity(KeycloakSession session, RealmModel realm, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
String attribute = mapperModel.getConfig().get(USER_ATTRIBUTE); String attribute = mapperModel.getConfig().get(USER_ATTRIBUTE);
Object value = getClaimValue(mapperModel, context); if(StringUtil.isNullOrEmpty(attribute)){
if (value != null) { return;
if (attribute.equalsIgnoreCase("email")) {
context.setEmail(value.toString());
} else if (attribute.equalsIgnoreCase("firstName")) {
context.setFirstName(value.toString());
} else if (attribute.equalsIgnoreCase("lastName")) {
context.setLastName(value.toString());
} else {
context.setUserAttribute(attribute, value.toString());
}
} }
Object value = getClaimValue(mapperModel, context);
List<String> values = toList(value);
if (EMAIL.equalsIgnoreCase(attribute)) {
setIfNotEmpty(context::setEmail, values);
} else if (FIRST_NAME.equalsIgnoreCase(attribute)) {
setIfNotEmpty(context::setFirstName, values);
} else if (LAST_NAME.equalsIgnoreCase(attribute)) {
setIfNotEmpty(context::setLastName, values);
} else {
List<String> valuesToString = values.stream()
.filter(Objects::nonNull)
.map(Object::toString)
.collect(Collectors.toList());
context.setUserAttribute(attribute, valuesToString);
}
}
private void setIfNotEmpty(Consumer<String> consumer, List<String> values) {
if (values != null && !values.isEmpty()) {
consumer.accept(values.get(0));
}
}
private List<String> toList(Object value) {
List<Object> values = (value instanceof List)
? (List) value
: Collections.singletonList(value);
return values.stream()
.filter(Objects::nonNull)
.map(Object::toString)
.collect(Collectors.toList());
} }
@Override @Override
public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
String attribute = mapperModel.getConfig().get(USER_ATTRIBUTE); String attribute = mapperModel.getConfig().get(USER_ATTRIBUTE);
if(StringUtil.isNullOrEmpty(attribute)){
return;
}
Object value = getClaimValue(mapperModel, context); Object value = getClaimValue(mapperModel, context);
String stringValue = null; List<String> values = toList(value);
if (value != null) stringValue = value.toString(); if (EMAIL.equalsIgnoreCase(attribute)) {
if (attribute.equalsIgnoreCase("email")) { setIfNotEmpty(user::setEmail, values);
user.setEmail(stringValue); } else if (FIRST_NAME.equalsIgnoreCase(attribute)) {
} else if (attribute.equalsIgnoreCase("firstName")) { setIfNotEmpty(user::setFirstName, values);
user.setFirstName(stringValue); } else if (LAST_NAME.equalsIgnoreCase(attribute)) {
} else if (attribute.equalsIgnoreCase("lastName")) { setIfNotEmpty(user::setLastName, values);
user.setLastName(stringValue);
} else { } else {
String current = user.getFirstAttribute(attribute); List<String> current = user.getAttribute(attribute);
if (stringValue != null && !stringValue.equals(current)) { if (!CollectionUtil.collectionEquals(values, current)) {
user.setSingleAttribute(attribute, stringValue); user.setAttribute(attribute, values);
} else if (value == null) { } else if (values.isEmpty()) {
user.removeAttribute(attribute); user.removeAttribute(attribute);
} }
} }

View file

@ -21,17 +21,18 @@ import org.keycloak.broker.provider.AbstractIdentityProviderMapper;
import org.keycloak.broker.provider.BrokeredIdentityContext; import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.broker.saml.SAMLEndpoint; import org.keycloak.broker.saml.SAMLEndpoint;
import org.keycloak.broker.saml.SAMLIdentityProviderFactory; import org.keycloak.broker.saml.SAMLIdentityProviderFactory;
import org.keycloak.common.util.CollectionUtil;
import org.keycloak.dom.saml.v2.assertion.AssertionType; import org.keycloak.dom.saml.v2.assertion.AssertionType;
import org.keycloak.dom.saml.v2.assertion.AttributeStatementType; import org.keycloak.dom.saml.v2.assertion.AttributeStatementType;
import org.keycloak.dom.saml.v2.assertion.AttributeType; import org.keycloak.dom.saml.v2.assertion.AttributeType;
import org.keycloak.models.IdentityProviderMapperModel; import org.keycloak.models.*;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.saml.common.util.StringUtil;
import java.util.ArrayList; import java.util.*;
import java.util.List; import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.stream.Collectors;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -41,11 +42,14 @@ public class UserAttributeMapper extends AbstractIdentityProviderMapper {
public static final String[] COMPATIBLE_PROVIDERS = {SAMLIdentityProviderFactory.PROVIDER_ID}; public static final String[] COMPATIBLE_PROVIDERS = {SAMLIdentityProviderFactory.PROVIDER_ID};
private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>(); private static final List<ProviderConfigProperty> configProperties = new ArrayList<>();
public static final String ATTRIBUTE_NAME = "attribute.name"; public static final String ATTRIBUTE_NAME = "attribute.name";
public static final String ATTRIBUTE_FRIENDLY_NAME = "attribute.friendly.name"; public static final String ATTRIBUTE_FRIENDLY_NAME = "attribute.friendly.name";
public static final String USER_ATTRIBUTE = "user.attribute"; public static final String USER_ATTRIBUTE = "user.attribute";
private static final String EMAIL = "email";
private static final String FIRST_NAME = "firstName";
private static final String LAST_NAME = "lastName";
static { static {
ProviderConfigProperty property; ProviderConfigProperty property;
@ -99,61 +103,84 @@ public class UserAttributeMapper extends AbstractIdentityProviderMapper {
@Override @Override
public void preprocessFederatedIdentity(KeycloakSession session, RealmModel realm, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { public void preprocessFederatedIdentity(KeycloakSession session, RealmModel realm, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
String attribute = mapperModel.getConfig().get(USER_ATTRIBUTE); String attribute = mapperModel.getConfig().get(USER_ATTRIBUTE);
String value = getAttribute(mapperModel, context); if (StringUtil.isNullOrEmpty(attribute)) {
if (value != null) { return;
if (attribute.equalsIgnoreCase("email")) { }
context.setEmail(value); String attributeName = getAttributeNameFromMapperModel(mapperModel);
} else if (attribute.equalsIgnoreCase("firstName")) {
context.setFirstName(value); List<String> attributeValuesInContext = findAttributeValuesInContext(attributeName, context);
} else if (attribute.equalsIgnoreCase("lastName")) { if (!attributeValuesInContext.isEmpty()) {
context.setLastName(value); if (attribute.equalsIgnoreCase(EMAIL)) {
setIfNotEmpty(context::setEmail, attributeValuesInContext);
} else if (attribute.equalsIgnoreCase(FIRST_NAME)) {
setIfNotEmpty(context::setFirstName, attributeValuesInContext);
} else if (attribute.equalsIgnoreCase(LAST_NAME)) {
setIfNotEmpty(context::setLastName, attributeValuesInContext);
} else { } else {
context.setUserAttribute(attribute, value); context.setUserAttribute(attribute, attributeValuesInContext);
} }
} }
} }
protected String getAttribute(IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { private String getAttributeNameFromMapperModel(IdentityProviderMapperModel mapperModel) {
String name = mapperModel.getConfig().get(ATTRIBUTE_NAME); String attributeName = mapperModel.getConfig().get(ATTRIBUTE_NAME);
if (name != null && name.trim().equals("")) name = null; if (attributeName == null) {
String friendly = mapperModel.getConfig().get(ATTRIBUTE_FRIENDLY_NAME); attributeName = mapperModel.getConfig().get(ATTRIBUTE_FRIENDLY_NAME);
if (friendly != null && friendly.trim().equals("")) friendly = null;
AssertionType assertion = (AssertionType)context.getContextData().get(SAMLEndpoint.SAML_ASSERTION);
for (AttributeStatementType statement : assertion.getAttributeStatements()) {
for (AttributeStatementType.ASTChoiceType choice : statement.getAttributes()) {
AttributeType attr = choice.getAttribute();
if (name != null && !name.equals(attr.getName())) continue;
if (friendly != null && !friendly.equals(attr.getFriendlyName())) continue;
List<Object> attributeValue = attr.getAttributeValue();
if (attributeValue == null || attributeValue.isEmpty()) return null;
return attributeValue.get(0).toString();
}
} }
return null; return attributeName;
}
private void setIfNotEmpty(Consumer<String> consumer, List<String> values) {
if (values != null && !values.isEmpty()) {
consumer.accept(values.get(0));
}
}
private Predicate<AttributeStatementType.ASTChoiceType> elementWith(String attributeName) {
return attributeType -> {
AttributeType attribute = attributeType.getAttribute();
return Objects.equals(attribute.getName(), attributeName)
|| Objects.equals(attribute.getFriendlyName(), attributeName);
};
}
private List<String> findAttributeValuesInContext(String attributeName, BrokeredIdentityContext context) {
AssertionType assertion = (AssertionType) context.getContextData().get(SAMLEndpoint.SAML_ASSERTION);
return assertion.getAttributeStatements().stream()
.flatMap(statement -> statement.getAttributes().stream())
.filter(elementWith(attributeName))
.flatMap(attributeType -> attributeType.getAttribute().getAttributeValue().stream())
.filter(Objects::nonNull)
.map(Object::toString)
.collect(Collectors.toList());
} }
@Override @Override
public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
String attribute = mapperModel.getConfig().get(USER_ATTRIBUTE); String attribute = mapperModel.getConfig().get(USER_ATTRIBUTE);
String value = getAttribute(mapperModel, context); if (StringUtil.isNullOrEmpty(attribute)) {
if (attribute.equalsIgnoreCase("email")) { return;
user.setEmail(value); }
} else if (attribute.equalsIgnoreCase("firstName")) { String attributeName = getAttributeNameFromMapperModel(mapperModel);
user.setFirstName(value); List<String> attributeValuesInContext = findAttributeValuesInContext(attributeName, context);
} else if (attribute.equalsIgnoreCase("lastName")) { if (attribute.equalsIgnoreCase(EMAIL)) {
user.setLastName(value); setIfNotEmpty(user::setEmail, attributeValuesInContext);
} else if (attribute.equalsIgnoreCase(FIRST_NAME)) {
setIfNotEmpty(user::setFirstName, attributeValuesInContext);
} else if (attribute.equalsIgnoreCase(LAST_NAME)) {
setIfNotEmpty(user::setLastName, attributeValuesInContext);
} else { } else {
String current = user.getFirstAttribute(attribute); List<String> currentAttributeValues = user.getAttributes().get(attribute);
if (value != null && !value.equals(current)) { if (attributeValuesInContext != null
user.setSingleAttribute(attribute, value.toString()); && currentAttributeValues != null
} else if (value == null) { && !CollectionUtil.collectionEquals(attributeValuesInContext, currentAttributeValues)) {
user.setAttribute(attribute, attributeValuesInContext);
} else if (attributeValuesInContext == null) {
user.removeAttribute(attribute); user.removeAttribute(attribute);
} }
} }
} }
@Override @Override

View file

@ -25,10 +25,9 @@ import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.representations.IDToken; import org.keycloak.representations.IDToken;
import org.keycloak.services.ServicesLogger; import org.keycloak.services.ServicesLogger;
import java.util.ArrayList; import java.util.*;
import java.util.HashMap; import java.util.function.Function;
import java.util.List; import java.util.stream.Collectors;
import java.util.Map;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -70,29 +69,78 @@ public class OIDCAttributeMapperHelper {
ServicesLogger.LOGGER.multipleValuesForMapper(attributeValue.toString(), mappingModel.getName()); ServicesLogger.LOGGER.multipleValuesForMapper(attributeValue.toString(), mappingModel.getName());
} }
attributeValue = valueAsList.get(0); attributeValue = valueAsList;
} }
} }
String type = mappingModel.getConfig().get(JSON_TYPE); String type = mappingModel.getConfig().get(JSON_TYPE);
Object converted = convertToType(type, attributeValue);
return converted != null ? converted : attributeValue;
}
private static <X, T> List<T> transform(List<X> attributeValue, Function<X, T> mapper) {
return attributeValue.stream()
.filter(Objects::nonNull)
.map(mapper)
.collect(Collectors.toList());
}
private static Object convertToType(String type, Object attributeValue) {
if (type == null) return attributeValue; if (type == null) return attributeValue;
if (type.equals("boolean")) { switch (type) {
if (attributeValue instanceof Boolean) return attributeValue; case "boolean":
if (attributeValue instanceof String) return Boolean.valueOf((String)attributeValue); Boolean booleanObject = getBoolean(attributeValue);
throw new RuntimeException("cannot map type for token claim"); if (booleanObject != null) return booleanObject;
} else if (type.equals("String")) { if (attributeValue instanceof List) {
if (attributeValue instanceof String) return attributeValue; return transform((List<Boolean>) attributeValue, OIDCAttributeMapperHelper::getBoolean);
return attributeValue.toString(); }
} else if (type.equals("long")) { throw new RuntimeException("cannot map type for token claim");
if (attributeValue instanceof Long) return attributeValue; case "String":
if (attributeValue instanceof String) return Long.valueOf((String)attributeValue); if (attributeValue instanceof String) return attributeValue;
throw new RuntimeException("cannot map type for token claim"); if (attributeValue instanceof List) {
} else if (type.equals("int")) { return transform((List<String>) attributeValue, OIDCAttributeMapperHelper::getString);
if (attributeValue instanceof Integer) return attributeValue; }
if (attributeValue instanceof String) return Integer.valueOf((String)attributeValue); return attributeValue.toString();
throw new RuntimeException("cannot map type for token claim"); case "long":
Long longObject = getLong(attributeValue);
if (longObject != null) return longObject;
if (attributeValue instanceof List) {
return transform((List<Long>) attributeValue, OIDCAttributeMapperHelper::getLong);
}
throw new RuntimeException("cannot map type for token claim");
case "int":
Integer intObject = getInteger(attributeValue);
if (intObject != null) return intObject;
if (attributeValue instanceof List) {
return transform((List<Integer>) attributeValue, OIDCAttributeMapperHelper::getInteger);
}
throw new RuntimeException("cannot map type for token claim");
default:
return null;
} }
return attributeValue; }
private static String getString(Object attributeValue) {
return attributeValue.toString();
}
private static Long getLong(Object attributeValue) {
if (attributeValue instanceof Long) return (Long) attributeValue;
if (attributeValue instanceof String) return Long.valueOf((String) attributeValue);
return null;
}
private static Integer getInteger(Object attributeValue) {
if (attributeValue instanceof Integer) return (Integer) attributeValue;
if (attributeValue instanceof String) return Integer.valueOf((String) attributeValue);
return null;
}
private static Boolean getBoolean(Object attributeValue) {
if (attributeValue instanceof Boolean) return (Boolean) attributeValue;
if (attributeValue instanceof String) return Boolean.valueOf((String) attributeValue);
return null;
} }
public static void mapClaim(IDToken token, ProtocolMapperModel mappingModel, Object attributeValue) { public static void mapClaim(IDToken token, ProtocolMapperModel mappingModel, Object attributeValue) {

View file

@ -52,6 +52,15 @@ public class AttributeStatementHelper {
attributeStatement.addAttribute(new AttributeStatementType.ASTChoiceType(attribute)); attributeStatement.addAttribute(new AttributeStatementType.ASTChoiceType(attribute));
} }
public static void addAttributes(AttributeStatementType attributeStatement, ProtocolMapperModel mappingModel,
List<String> attributeValues) {
AttributeType attribute = createAttributeType(mappingModel);
attributeValues.forEach(attribute::addAttributeValue);
attributeStatement.addAttribute(new AttributeStatementType.ASTChoiceType(attribute));
}
public static AttributeType createAttributeType(ProtocolMapperModel mappingModel) { public static AttributeType createAttributeType(ProtocolMapperModel mappingModel) {
String attributeName = mappingModel.getConfig().get(SAML_ATTRIBUTE_NAME); String attributeName = mappingModel.getConfig().get(SAML_ATTRIBUTE_NAME);
AttributeType attribute = new AttributeType(attributeName); AttributeType attribute = new AttributeType(attributeName);

View file

@ -80,10 +80,9 @@ public class UserAttributeStatementMapper extends AbstractSAMLProtocolMapper imp
public void transformAttributeStatement(AttributeStatementType attributeStatement, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, ClientSessionModel clientSession) { public void transformAttributeStatement(AttributeStatementType attributeStatement, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, ClientSessionModel clientSession) {
UserModel user = userSession.getUser(); UserModel user = userSession.getUser();
String attributeName = mappingModel.getConfig().get(ProtocolMapperUtils.USER_ATTRIBUTE); String attributeName = mappingModel.getConfig().get(ProtocolMapperUtils.USER_ATTRIBUTE);
String attributeValue = KeycloakModelUtils.resolveFirstAttribute(user, attributeName); List<String> attributeValues = KeycloakModelUtils.resolveAttribute(user, attributeName);
if (attributeValue == null) return; if (attributeValues.isEmpty()) return;
AttributeStatementHelper.addAttribute(attributeStatement, mappingModel, attributeValue); AttributeStatementHelper.addAttributes(attributeStatement, mappingModel, attributeValues);
} }
public static ProtocolMapperModel createAttributeMapper(String name, String userAttribute, public static ProtocolMapperModel createAttributeMapper(String name, String userAttribute,

View file

@ -24,10 +24,11 @@ import org.junit.Test;
import org.keycloak.broker.oidc.mappers.AbstractJsonUserAttributeMapper; import org.keycloak.broker.oidc.mappers.AbstractJsonUserAttributeMapper;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays;
/** /**
* Unit test for {@link org.keycloak.broker.oidc.mappers.AbstractJsonUserAttributeMapper} * Unit test for {@link org.keycloak.broker.oidc.mappers.AbstractJsonUserAttributeMapper}
* *
* @author Vlastimil Elias (velias at redhat dot com) * @author Vlastimil Elias (velias at redhat dot com)
*/ */
public class AbstractJsonUserAttributeMapperTest { public class AbstractJsonUserAttributeMapperTest {
@ -58,7 +59,7 @@ public class AbstractJsonUserAttributeMapperTest {
//unknown field returns null //unknown field returns null
Assert.assertEquals(null, AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "value_unknown")); Assert.assertEquals(null, AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "value_unknown"));
// we check value is trimmed also! // we check value is trimmed also!
Assert.assertEquals("v1", AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "value1")); Assert.assertEquals("v1", AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "value1"));
Assert.assertEquals(null, AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "value_empty")); Assert.assertEquals(null, AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "value_empty"));
@ -95,14 +96,14 @@ public class AbstractJsonUserAttributeMapperTest {
public void getJsonValue_simpleArray() throws JsonProcessingException, IOException { public void getJsonValue_simpleArray() throws JsonProcessingException, IOException {
// array field itself returns null if no index is provided // array field itself returns null if no index is provided
Assert.assertEquals(null, AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "value_array")); Assert.assertEquals(Arrays.asList("a1", "a2"), AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "value_array"));
// outside index returns null // outside index returns null
Assert.assertEquals(null, AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "value_array[2]")); Assert.assertEquals(null, AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "value_array[2]"));
//corect index //corect index
Assert.assertEquals("a1", AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "value_array[0]")); Assert.assertEquals("a1", AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "value_array[0]"));
Assert.assertEquals("a2", AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "value_array[1]")); Assert.assertEquals("a2", AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "value_array[1]"));
//incorrect array constructs //incorrect array constructs
Assert.assertNull(AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "value_array[]")); Assert.assertNull(AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "value_array[]"));
Assert.assertNull(AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "value_array]")); Assert.assertNull(AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "value_array]"));
@ -125,7 +126,7 @@ public class AbstractJsonUserAttributeMapperTest {
Assert.assertEquals(null, AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "nesta.a.av1")); Assert.assertEquals(null, AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "nesta.a.av1"));
Assert.assertEquals(null, AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "nesta.a].av1")); Assert.assertEquals(null, AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "nesta.a].av1"));
Assert.assertEquals(null, AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "nesta.a[.av1")); Assert.assertEquals(null, AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "nesta.a[.av1"));
} }
} }

View file

@ -32,10 +32,10 @@ import org.keycloak.representations.idm.ProtocolMapperRepresentation;
import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.idm.UserRepresentation;
import java.util.HashMap; import java.util.*;
import java.util.LinkedList;
import java.util.List; import static org.hamcrest.CoreMatchers.hasItems;
import java.util.Map; import static org.hamcrest.MatcherAssert.assertThat;
/** /**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a> * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
@ -119,7 +119,7 @@ public class GroupMappersTest extends AbstractGroupTest {
Assert.assertNotNull(groups); Assert.assertNotNull(groups);
Assert.assertTrue(groups.size() == 1); Assert.assertTrue(groups.size() == 1);
Assert.assertEquals("topGroup", groups.get(0)); Assert.assertEquals("topGroup", groups.get(0));
Assert.assertEquals("true", token.getOtherClaims().get("topAttribute")); Assert.assertEquals(Collections.singletonList("true"), token.getOtherClaims().get("topAttribute"));
} }
{ {
UserRepresentation user = realm.users().search("level2GroupUser", -1, -1).get(0); UserRepresentation user = realm.users().search("level2GroupUser", -1, -1).get(0);
@ -132,8 +132,8 @@ public class GroupMappersTest extends AbstractGroupTest {
Assert.assertNotNull(groups); Assert.assertNotNull(groups);
Assert.assertTrue(groups.size() == 1); Assert.assertTrue(groups.size() == 1);
Assert.assertEquals("level2group", groups.get(0)); Assert.assertEquals("level2group", groups.get(0));
Assert.assertEquals("true", token.getOtherClaims().get("topAttribute")); Assert.assertEquals(Collections.singletonList("true"), token.getOtherClaims().get("topAttribute"));
Assert.assertEquals("true", token.getOtherClaims().get("level2Attribute")); Assert.assertEquals(Collections.singletonList("true"), token.getOtherClaims().get("level2Attribute"));
} }
} }
} }

View file

@ -238,7 +238,6 @@ public abstract class AbstractUserAttributeMapperTest extends AbstractBaseBroker
} }
@Test @Test
@Ignore("Unignore to test KEYCLOAK-3648")
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(ATTRIBUTE_TO_MAP_NAME, ImmutableList.<String>builder().add("value 1").add("value 2").build())

View file

@ -141,12 +141,12 @@ public class OIDCProtocolMappersTest extends AbstractKeycloakTest {
assertNull(idToken.getAddress().getCountry()); // Null because we changed userAttribute name to "country_some", but user contains "country" assertNull(idToken.getAddress().getCountry()); // Null because we changed userAttribute name to "country_some", but user contains "country"
assertEquals(idToken.getAddress().getFormattedAddress(), "6 Foo Street"); assertEquals(idToken.getAddress().getFormattedAddress(), "6 Foo Street");
assertNotNull(idToken.getOtherClaims().get("home_phone")); assertNotNull(idToken.getOtherClaims().get("home_phone"));
assertEquals("617-777-6666", idToken.getOtherClaims().get("home_phone")); assertThat((List<String>) idToken.getOtherClaims().get("home_phone"), hasItems("617-777-6666"));
assertEquals("coded", idToken.getOtherClaims().get("hard")); assertEquals("coded", idToken.getOtherClaims().get("hard"));
Map nested = (Map) idToken.getOtherClaims().get("nested"); Map nested = (Map) idToken.getOtherClaims().get("nested");
assertEquals("coded-nested", nested.get("hard")); assertEquals("coded-nested", nested.get("hard"));
nested = (Map) idToken.getOtherClaims().get("home"); nested = (Map) idToken.getOtherClaims().get("home");
assertEquals("617-777-6666", nested.get("phone")); assertThat((List<String>) nested.get("phone"), hasItems("617-777-6666"));
List<String> departments = (List<String>) idToken.getOtherClaims().get("department"); List<String> departments = (List<String>) idToken.getOtherClaims().get("department");
assertEquals(2, departments.size()); assertEquals(2, departments.size());
assertTrue(departments.contains("finance") && departments.contains("development")); assertTrue(departments.contains("finance") && departments.contains("development"));
@ -161,12 +161,12 @@ public class OIDCProtocolMappersTest extends AbstractKeycloakTest {
assertNull(idToken.getAddress().getCountry()); // Null because we changed userAttribute name to "country_some", but user contains "country" assertNull(idToken.getAddress().getCountry()); // Null because we changed userAttribute name to "country_some", but user contains "country"
assertEquals(idToken.getAddress().getFormattedAddress(), "6 Foo Street"); assertEquals(idToken.getAddress().getFormattedAddress(), "6 Foo Street");
assertNotNull(accessToken.getOtherClaims().get("home_phone")); assertNotNull(accessToken.getOtherClaims().get("home_phone"));
assertEquals("617-777-6666", accessToken.getOtherClaims().get("home_phone")); assertThat((List<String>) accessToken.getOtherClaims().get("home_phone"), hasItems("617-777-6666"));
assertEquals("coded", accessToken.getOtherClaims().get("hard")); assertEquals("coded", accessToken.getOtherClaims().get("hard"));
nested = (Map) accessToken.getOtherClaims().get("nested"); nested = (Map) accessToken.getOtherClaims().get("nested");
assertEquals("coded-nested", nested.get("hard")); assertEquals("coded-nested", nested.get("hard"));
nested = (Map) accessToken.getOtherClaims().get("home"); nested = (Map) accessToken.getOtherClaims().get("home");
assertEquals("617-777-6666", nested.get("phone")); assertThat((List<String>) nested.get("phone"), hasItems("617-777-6666"));
departments = (List<String>) idToken.getOtherClaims().get("department"); departments = (List<String>) idToken.getOtherClaims().get("department");
assertEquals(2, departments.size()); assertEquals(2, departments.size());
assertTrue(departments.contains("finance") && departments.contains("development")); assertTrue(departments.contains("finance") && departments.contains("development"));