/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.broker.saml.mappers;
import org.keycloak.broker.provider.AbstractIdentityProviderMapper;
import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.broker.saml.SAMLEndpoint;
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.AttributeStatementType;
import org.keycloak.dom.saml.v2.assertion.AttributeType;
import org.keycloak.models.*;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.saml.common.util.StringUtil;
import java.util.*;
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>
* @version $Revision: 1 $
*/
public class UserAttributeMapper extends AbstractIdentityProviderMapper {
public static final String[] COMPATIBLE_PROVIDERS = {SAMLIdentityProviderFactory.PROVIDER_ID};
private static final List<ProviderConfigProperty> configProperties = new ArrayList<>();
public static final String ATTRIBUTE_NAME = "attribute.name";
public static final String ATTRIBUTE_FRIENDLY_NAME = "attribute.friendly.name";
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 {
ProviderConfigProperty property;
property = new ProviderConfigProperty();
property.setName(ATTRIBUTE_NAME);
property.setLabel("Attribute Name");
property.setHelpText("Name of attribute to search for in assertion. You can leave this blank and specify a friendly name instead.");
property.setType(ProviderConfigProperty.STRING_TYPE);
configProperties.add(property);
property = new ProviderConfigProperty();
property.setName(ATTRIBUTE_FRIENDLY_NAME);
property.setLabel("Friendly Name");
property.setHelpText("Friendly name of attribute to search for in assertion. You can leave this blank and specify a name instead.");
property.setType(ProviderConfigProperty.STRING_TYPE);
configProperties.add(property);
property = new ProviderConfigProperty();
property.setName(USER_ATTRIBUTE);
property.setLabel("User Attribute Name");
property.setHelpText("User attribute name to store saml attribute. Use email, lastName, and firstName to map to those predefined user properties.");
property.setType(ProviderConfigProperty.STRING_TYPE);
configProperties.add(property);
}
public static final String PROVIDER_ID = "saml-user-attribute-idp-mapper";
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return configProperties;
}
@Override
public String getId() {
return PROVIDER_ID;
}
@Override
public String[] getCompatibleProviders() {
return COMPATIBLE_PROVIDERS;
}
@Override
public String getDisplayCategory() {
return "Attribute Importer";
}
@Override
public String getDisplayType() {
return "Attribute Importer";
}
@Override
public void preprocessFederatedIdentity(KeycloakSession session, RealmModel realm, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
String attribute = mapperModel.getConfig().get(USER_ATTRIBUTE);
if (StringUtil.isNullOrEmpty(attribute)) {
return;
}
String attributeName = getAttributeNameFromMapperModel(mapperModel);
List<String> attributeValuesInContext = findAttributeValuesInContext(attributeName, context);
if (!attributeValuesInContext.isEmpty()) {
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 {
context.setUserAttribute(attribute, attributeValuesInContext);
}
}
}
private String getAttributeNameFromMapperModel(IdentityProviderMapperModel mapperModel) {
String attributeName = mapperModel.getConfig().get(ATTRIBUTE_NAME);
if (attributeName == null) {
attributeName = mapperModel.getConfig().get(ATTRIBUTE_FRIENDLY_NAME);
}
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
public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
String attribute = mapperModel.getConfig().get(USER_ATTRIBUTE);
if (StringUtil.isNullOrEmpty(attribute)) {
return;
}
String attributeName = getAttributeNameFromMapperModel(mapperModel);
List<String> attributeValuesInContext = findAttributeValuesInContext(attributeName, context);
if (attribute.equalsIgnoreCase(EMAIL)) {
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 {
List<String> currentAttributeValues = user.getAttributes().get(attribute);
if (attributeValuesInContext == null) {
// attribute no longer sent by brokered idp, remove it
user.removeAttribute(attribute);
} else if (currentAttributeValues == null) {
// new attribute sent by brokered idp, add it
user.setAttribute(attribute, attributeValuesInContext);
} else if (!CollectionUtil.collectionEquals(attributeValuesInContext, currentAttributeValues)) {
// attribute sent by brokered idp has different values as before, update it
user.setAttribute(attribute, attributeValuesInContext);
}
// attribute allready set
}
}
@Override
public String getHelpText() {
return "Import declared saml attribute if it exists in assertion into the specified user property or attribute.";
}
}