2020-07-16 13:42:55 +02:00
|
|
|
/*
|
|
|
|
* 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
|
2021-12-24 17:44:56 +01:00
|
|
|
List<String> attrValues = localUser.getAttributeStream(userModelAttrName).collect(Collectors.toList());
|
2020-07-16 13:42:55 +02:00
|
|
|
|
|
|
|
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);
|
|
|
|
|
2021-12-24 17:44:56 +01:00
|
|
|
UserModel that = session.userLocalStorage().getUserByEmail(realm, email);
|
2020-07-16 13:42:55 +02:00
|
|
|
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) {
|
2021-12-24 17:44:56 +01:00
|
|
|
UserModel that = session.users().getUserByUsername(realm, username);
|
2020-07-16 13:42:55 +02:00
|
|
|
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());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|