/* * 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 Marek Posolda */ public class UserAttributeTemplatedLDAPStorageMapper extends AbstractLDAPStorageMapper { private static final Logger logger = Logger.getLogger(UserAttributeTemplatedLDAPStorageMapper.class); private static final Map> 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 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 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 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()); } } else { ldapUser.setSingleAttribute(ldapAttrName, computeAttributeValue(template, attrValue.toString())); } } else { // we don't have java property. Let's set attribute List 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()); } } else { UserAttributeTemplatedLDAPStorageMapper.logger .trace("Computing value from template for all the elements in the list"); List 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 userModelProperty, UserModel user, String ldapAttrValue) { if (ldapAttrValue == null) { userModelProperty.setValue(user, null); } else { Class 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()); } } } }