KEYCLOAK-3648
This commit is contained in:
parent
bed592460d
commit
bfec073457
12 changed files with 262 additions and 122 deletions
|
@ -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()) {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -24,6 +24,7 @@ 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}
|
||||||
|
@ -95,7 +96,7 @@ 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]"));
|
||||||
|
|
||||||
|
|
|
@ -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"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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())
|
||||||
|
|
|
@ -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"));
|
||||||
|
|
Loading…
Reference in a new issue