keycloak-d4science-spi-parent/ldap-storage-mapper/src/main/java/org/gcube/keycloak/storage/ldap/mappers/UserAttributeTemplatedLDAPS...

263 lines
11 KiB
Java

/*
* 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.gcube.keycloak.storage.ldap.mappers;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.jboss.logging.Logger;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.LDAPConstants;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.ModelException;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.reflection.Property;
import org.keycloak.storage.ldap.LDAPStorageProvider;
import org.keycloak.storage.ldap.LDAPUtils;
import org.keycloak.storage.ldap.idm.model.LDAPObject;
import org.keycloak.storage.ldap.idm.query.internal.LDAPQuery;
import org.keycloak.storage.ldap.mappers.AbstractLDAPStorageMapper;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class UserAttributeTemplatedLDAPStorageMapper extends AbstractLDAPStorageMapper {
private static final Logger logger = Logger.getLogger(UserAttributeTemplatedLDAPStorageMapper.class);
private static final Map<String, Property<Object>> userModelProperties = LDAPUtils.getUserModelProperties();
public static final String TEMPLATE_ATTRIBUTE = "template.string";
public static final String USER_MODEL_ATTRIBUTE = "user.model.attribute";
public static final String LDAP_ATTRIBUTE = "ldap.attribute";
public static final String READ_ONLY = "read.only";
public static final String ALWAYS_READ_VALUE_FROM_LDAP = "always.read.value.from.ldap";
public static final String IS_MANDATORY_IN_LDAP = "is.mandatory.in.ldap";
public UserAttributeTemplatedLDAPStorageMapper(ComponentModel mapperModel, LDAPStorageProvider ldapProvider) {
super(mapperModel, ldapProvider);
}
@Override
public void onImportUserFromLDAP(LDAPObject ldapUser, UserModel user, RealmModel realm, boolean isCreate) {
String userModelAttrName = getUserModelAttribute();
String ldapAttrName = getLdapAttributeName();
Property<Object> userModelProperty = userModelProperties.get(userModelAttrName.toLowerCase());
if (userModelProperty != null) {
// we have java property on UserModel
String ldapAttrValue = ldapUser.getAttributeAsString(ldapAttrName);
checkDuplicateEmail(userModelAttrName, ldapAttrValue, realm, ldapProvider.getSession(), user);
setPropertyOnUserModel(userModelProperty, user, ldapAttrValue);
} else {
// we don't have java property. Let's set attribute
Set<String> ldapAttrValue = ldapUser.getAttributeAsSet(ldapAttrName);
if (ldapAttrValue != null) {
user.setAttribute(userModelAttrName, new ArrayList<>(ldapAttrValue));
} else {
user.removeAttribute(userModelAttrName);
}
}
}
public static final String VALUE = "VALUE";
public static final String ATTRIBUTE_VALUE = "${" + VALUE + "}";
public static Pattern substitution = Pattern.compile("\\$\\{([^}]+)\\}");
protected String computeAttributeValue(String template, String value) {
Matcher matcher = substitution.matcher(template);
StringBuffer sb = new StringBuffer();
while (matcher.find()) {
String token = matcher.group(1);
if (token.equals(VALUE)) {
matcher.appendReplacement(sb, value);
} else {
matcher.appendReplacement(sb, token);
}
}
matcher.appendTail(sb);
return sb.toString();
}
@Override
public void onRegisterUserToLDAP(LDAPObject ldapUser, UserModel localUser, RealmModel realm) {
String template = getTemplate();
String userModelAttrName = getUserModelAttribute();
String ldapAttrName = getLdapAttributeName();
boolean isMandatoryInLdap = parseBooleanParameter(mapperModel, IS_MANDATORY_IN_LDAP);
Property<Object> userModelProperty = userModelProperties.get(userModelAttrName.toLowerCase());
if (userModelProperty != null) {
// we have java property on UserModel. Assuming we support just properties of simple types
Object attrValue = userModelProperty.getValue(localUser);
if (attrValue == null) {
if (isMandatoryInLdap) {
ldapUser.setSingleAttribute(ldapAttrName, LDAPConstants.EMPTY_ATTRIBUTE_VALUE);
} else {
ldapUser.setAttribute(ldapAttrName, new LinkedHashSet<String>());
}
} else {
ldapUser.setSingleAttribute(ldapAttrName, computeAttributeValue(template, attrValue.toString()));
}
} else {
// we don't have java property. Let's set attribute
List<String> attrValues = localUser.getAttributeStream(userModelAttrName).collect(Collectors.toList());
if (attrValues.size() == 0) {
if (isMandatoryInLdap) {
ldapUser.setSingleAttribute(ldapAttrName, LDAPConstants.EMPTY_ATTRIBUTE_VALUE);
} else {
ldapUser.setAttribute(ldapAttrName, new LinkedHashSet<String>());
}
} else {
UserAttributeTemplatedLDAPStorageMapper.logger
.trace("Computing value from template for all the elements in the list");
List<String> newList = attrValues.stream().map(e -> computeAttributeValue(template, e))
.collect(Collectors.toList());
ldapUser.setAttribute(ldapAttrName, new LinkedHashSet<>(newList));
}
}
if (isReadOnly()) {
ldapUser.addReadOnlyAttributeName(ldapAttrName);
}
}
// throw ModelDuplicateException if there is different user in model with same email
protected void checkDuplicateEmail(String userModelAttrName, String email, RealmModel realm,
KeycloakSession session, UserModel user) {
if (email == null || realm.isDuplicateEmailsAllowed())
return;
if (UserModel.EMAIL.equalsIgnoreCase(userModelAttrName)) {
// lowercase before search
email = KeycloakModelUtils.toLowerCaseSafe(email);
UserModel that = session.users().getUserByEmail(realm, email);
if (that != null && !that.getId().equals(user.getId())) {
session.getTransactionManager().setRollbackOnly();
String exceptionMessage = String.format(
"Can't import user '%s' from LDAP because email '%s' already exists in Keycloak. Existing user with this email is '%s'",
user.getUsername(), email, that.getUsername());
throw new ModelDuplicateException(exceptionMessage, UserModel.EMAIL);
}
}
}
protected void checkDuplicateUsername(String userModelAttrName, String username, RealmModel realm,
KeycloakSession session, UserModel user) {
// only if working in USERNAME attribute
if (UserModel.USERNAME.equalsIgnoreCase(userModelAttrName)) {
if (username == null || username.isEmpty()) {
throw new ModelException("Cannot set an empty username");
}
boolean usernameChanged = !username.equals(user.getUsername());
if (realm.isEditUsernameAllowed() && usernameChanged) {
UserModel that = session.users().getUserByUsername(realm, username);
if (that != null && !that.getId().equals(user.getId())) {
throw new ModelDuplicateException(
String.format(
"Cannot change the username to '%s' because the username already exists in keycloak",
username),
UserModel.USERNAME);
}
} else if (usernameChanged) {
throw new ModelException(
"Cannot change username if the realm is not configured to allow edit the usernames");
}
}
}
@Override
public UserModel proxy(final LDAPObject ldapUser, UserModel delegate, RealmModel realm) {
// Don't update attribute in LDAP later. It's supposed to be written just at registration time
String ldapAttrName = mapperModel.get(LDAP_ATTRIBUTE);
ldapUser.addReadOnlyAttributeName(ldapAttrName);
return delegate;
}
@Override
public void beforeLDAPQuery(LDAPQuery query) {
String ldapAttrName = getLdapAttributeName();
// Add mapped attribute to returning ldap attributes
query.addReturningLdapAttribute(ldapAttrName);
if (isReadOnly()) {
query.addReturningReadOnlyLdapAttribute(ldapAttrName);
}
}
private String getTemplate() {
return mapperModel.getConfig().getFirst(TEMPLATE_ATTRIBUTE);
}
private String getUserModelAttribute() {
return mapperModel.getConfig().getFirst(USER_MODEL_ATTRIBUTE);
}
String getLdapAttributeName() {
return mapperModel.getConfig().getFirst(LDAP_ATTRIBUTE);
}
private boolean isReadOnly() {
return parseBooleanParameter(mapperModel, READ_ONLY);
}
protected void setPropertyOnUserModel(Property<Object> userModelProperty, UserModel user, String ldapAttrValue) {
if (ldapAttrValue == null) {
userModelProperty.setValue(user, null);
} else {
Class<Object> clazz = userModelProperty.getJavaClass();
if (String.class.equals(clazz)) {
userModelProperty.setValue(user, ldapAttrValue);
} else if (Boolean.class.equals(clazz) || boolean.class.equals(clazz)) {
Boolean boolVal = Boolean.valueOf(ldapAttrValue);
userModelProperty.setValue(user, boolVal);
} else {
logger.warnf("Don't know how to set the property '%s' on user '%s' . Value of LDAP attribute is '%s' ",
userModelProperty.getName(), user.getUsername(), ldapAttrValue.toString());
}
}
}
}