org.sakaiproject.unboundid.SimpleLdapAttributeMapper Maven / Gradle / Ivy
The newest version!
/**********************************************************************************
* $URL$
* $Id$
***********************************************************************************
*
* Copyright (c) 2003, 2004, 2005, 2006, 2007, 2008 The Sakai Foundation
*
* Licensed under the Educational Community 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.opensource.org/licenses/ECL-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.sakaiproject.unboundid;
import java.text.MessageFormat;
import java.text.ParsePosition;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.sakaiproject.entity.api.ResourceProperties;
import org.sakaiproject.user.api.UserEdit;
import com.unboundid.ldap.sdk.Attribute;
import com.unboundid.ldap.sdk.migrate.ldapjdk.LDAPAttribute;
import com.unboundid.ldap.sdk.migrate.ldapjdk.LDAPAttributeSet;
import com.unboundid.ldap.sdk.migrate.ldapjdk.LDAPEntry;
/**
* Implements LDAP attribute mappings and filter generations using
* an attribute map keyed by constants in
* {@link AttributeMappingConstants}. The strategy for calculating
* Sakai user type can be injected as a {@link UserTypeMapper}.
* This strategy defaults to {@link EmptyStringUserTypeMapper}, which
* will match <= 2.3.0 OOTB behavior.
*
* @author Dan McCallum, Unicon Inc
*
*/
@Slf4j
public class SimpleLdapAttributeMapper implements LdapAttributeMapper {
/**
* User entry attribute mappings. Keys are logical attr names,
* values are physical attr names.
*/
private Map attributeMappings;
/**
* Formatters used for manipulating attribute values sent to and returned from LDAP.
*/
private Map valueMappings;
/**
* Keys are physical attr names, values are collections of
* logical attr names. Essentially an inverse copy of
* {@link #attributeMappings}.
*/
private Map> reverseAttributeMappings;
/** strategy for calculating the Sakai user type given a
* LDAPEntry
*/
private UserTypeMapper userTypeMapper;
/** copy of {@link #attributeMappings}.values()
*/
private String[] physicalAttrNames;
/**
* Completes configuration of this instance.
*
* Initializes internal mappings to a copy of
* {@link AttributeMappingConstants#DEFAULT_ATTR_MAPPINGS} if
* the current map is empty. Initializes user
* type mapping strategy to a
* {@link EmptyStringUserTypeMapper} if no strategy
* has been specified.
*
*
* This defaulting enables UDP config
* forward-compatibility.
*
*/
public void init() {
log.debug("init()");
if ( attributeMappings == null || attributeMappings.isEmpty() ) {
log.debug("init(): creating default attribute mappings");
setAttributeMappings(AttributeMappingConstants.DEFAULT_ATTR_MAPPINGS);
}
if ( userTypeMapper == null ) {
userTypeMapper = new EmptyStringUserTypeMapper();
log.debug("init(): created default user type mapper [mapper = {}]", userTypeMapper);
}
if ( valueMappings == null ) {
valueMappings = Collections.emptyMap();
log.debug("init(): created default value mapper [mapper = {}]", valueMappings);
} else {
// Check we have good value mappings and throw any out that aren't (warning user).
Iterator> iterator = valueMappings.entrySet().iterator();
while (iterator.hasNext()) {
Entry entry = iterator.next();
if (entry.getValue().getFormats().length != 1) {
iterator.remove();
log.warn(String.format("Removed value mapping as it didn't have one format: %s -> %s",
entry.getKey(), entry.getValue().toPattern()));
}
}
}
}
/**
* Builds a filter of the form <email-attr>=<emailAddr
>
*/
public String getFindUserByEmailFilter(String emailAddr) {
String emailAttr =
attributeMappings.get(AttributeMappingConstants.EMAIL_ATTR_MAPPING_KEY);
MessageFormat valueFormat = valueMappings.get(AttributeMappingConstants.EMAIL_ATTR_MAPPING_KEY);
if (valueFormat == null) {
return emailAttr + "=" + escapeSearchFilterTerm(emailAddr);
} else {
valueFormat = (MessageFormat) valueFormat.clone();
return emailAttr + "=" + escapeSearchFilterTerm(valueFormat.format(new Object[]{emailAddr}));
}
}
/**
* Builds a filter of the form <login-attr>=<eid
>
*/
public String getFindUserByEidFilter(String eid) {
String eidAttr =
attributeMappings.get(AttributeMappingConstants.LOGIN_ATTR_MAPPING_KEY);
MessageFormat valueFormat = valueMappings.get(AttributeMappingConstants.LOGIN_ATTR_MAPPING_KEY);
if (valueFormat == null) {
return eidAttr + "=" + escapeSearchFilterTerm(eid);
} else {
valueFormat = (MessageFormat) valueFormat.clone();
return eidAttr + "=" + escapeSearchFilterTerm(valueFormat.format(new Object[]{eid}));
}
}
public String getFindUserByAidFilter(String aid) {
String eidAttr =
attributeMappings.get(AttributeMappingConstants.AUTHENTICATION_ATTR_MAPPING_KEY);
return eidAttr + "=" + escapeSearchFilterTerm(aid);
}
/**
* Performs {@link LDAPEntry}-to-{@Link LdapUserData} attribute
* mappings. Assigns the given {@link LDAPEntry}'s DN to the
* {@link LdapUserData} as a property keyed by
* {@link AttributeMappingConstants#USER_DN_PROPERTY}. Then iterates
* over {@link LDAPEntry#getAttributeSet()}, handing each attribute
* to {@link #mapLdapAttributeOntoUserData(LDAPAttribute, LdapUserData, Collection)}.
* Then enforces the preferred first name field, if it exists.
* Finally, assigns a "type" to the {@link LdapUserData} as defined
* by {@link #mapLdapEntryToSakaiUserType(LDAPEntry)}.
*
* @see UserTypeMapper
* @param ldapEntry the user's directory entry
* @param userData target {@link LdapUserData}
*/
public void mapLdapEntryOntoUserData(LDAPEntry ldapEntry, LdapUserData userData) {
log.debug("mapLdapEntryOntoUserData(): mapping entry [dn = {}]", ldapEntry.getDN());
setUserDataDn(ldapEntry, userData);
LDAPAttributeSet ldapAttributeSet = ldapEntry.getAttributeSet();
Enumeration ldapAttributes = ldapAttributeSet.getAttributes();
while (ldapAttributes.hasMoreElements()) {
LDAPAttribute ldapAttribute = ldapAttributes.nextElement();
// we do the reverse lookup here since it will always need to
// be performed and we want to ensure it only happens once
// per attribute, regardless of the complexity of the actual
// mapping onto the user object
Collection logicalAttrNames =
getReverseAttributeMappings(ldapAttribute.getName());
mapLdapAttributeOntoUserData(ldapAttribute, userData, logicalAttrNames);
}
//enforce use of firstNamePreferred if its set
userData.setFirstName(usePreferredFirstName(userData));
// calculating a user's "type" potentially involves calculations
// against the entire LDAPEntry
userData.setType(mapLdapEntryToSakaiUserType(ldapEntry));
}
public String getUserBindDn(LdapUserData userData) {
return getUserDataDn(userData);
}
protected String getUserDataDn(LdapUserData userData) {
return userData.getProperties().getProperty(AttributeMappingConstants.USER_DN_PROPERTY);
}
protected void setUserDataDn(LDAPEntry entry, LdapUserData targetUserData) {
targetUserData.setProperty(
AttributeMappingConstants.USER_DN_PROPERTY,
entry.getDN());
}
/**
* Map the given {@link LDAPAttribute} onto the given
* {@link LdapUserData}. Client can specify the logical attribute
* name(s) which have been configured for the given {@link LDAPAttribute}.
* This implementation has specific handling for the following
* logical attribute names:
*
*
* - {@link AttributeMappingConstants#LOGIN_ATTR_MAPPING_KEY} - {@link LdapUserData#setEid(String)}
* - {@link AttributeMappingConstants#FIRST_NAME_ATTR_MAPPING_KEY} - {@link LdapUserData#setFirstName(String)}
* - {@link AttributeMappingConstants#LAST_NAME_ATTR_MAPPING_KEY} - {@link LdapUserData#setLastName(String)}
* - {@link AttributeMappingConstants#EMAIL_ATTR_MAPPING_KEY} - {@link LdapUserData#setEmail(String)}
*
*
* Any other logical attribute names passed in logicalAttrNames
* will be mapped onto userData
as a property using
* the logical attribute name as a key.
*
* @param attribute the {@link LDAPAttribute} to map
* @param userData the target {@link LdapUserData} instance
* @param logicalAttrNames logical name(s) of the attribute
. May
* be null or empty, indicating no configured logical name(s).
*/
protected void mapLdapAttributeOntoUserData(LDAPAttribute attribute,
LdapUserData userData, Collection logicalAttrNames) {
if ( logicalAttrNames == null || logicalAttrNames.isEmpty() ) {
log.debug("No logical name for attribute. [physical name = {}]", attribute.getName());
return;
}
for ( String logicalAttrName : logicalAttrNames ) {
mapLdapAttributeOntoUserData(attribute, userData, logicalAttrName);
}
}
/**
* A delegate of {@link #mapLdapAttributeOntoUserData(LDAPAttribute, LdapUserData, Collection)}
* that allows for discrete handling of each logical attribute name associated with
* the given {@link LDAPAttribute}
*
* @param attribute
* @param userData
* @param logicalAttrName
*/
protected void mapLdapAttributeOntoUserData(LDAPAttribute attribute,
LdapUserData userData, String logicalAttrName) {
Attribute unboundidAttribute = attribute.toAttribute();
String attrValue = unboundidAttribute.getValue();
MessageFormat format = valueMappings.get(logicalAttrName);
if (format != null && attrValue != null) {
format = (MessageFormat)format.clone();
log.debug("mapLdapAttributeOntoUserData(): value mapper [attrValue = {}; format={}]", attrValue, format.toString());
attrValue = (String)(format.parse(attrValue, new ParsePosition(0))[0]);
}
log.debug("mapLdapAttributeOntoUserData() preparing to map: [logical attr name = {}][physical attr name = {}][value = {}]",
logicalAttrName, attribute.getName(), attrValue);
if ( logicalAttrName.equals(AttributeMappingConstants.LOGIN_ATTR_MAPPING_KEY) ) {
log.debug("mapLdapAttributeOntoUserData() mapping attribute to User.eid: [logical attr name = {}][physical attr name = {}][value = {}]",
logicalAttrName, attribute.getName(), attrValue);
userData.setEid(attrValue);
} else if ( logicalAttrName.equals(AttributeMappingConstants.FIRST_NAME_ATTR_MAPPING_KEY) ) {
log.debug("mapLdapAttributeOntoUserData() mapping attribute to User.firstName: [logical attr name = {}][physical attr name = [}][value = {}]",
logicalAttrName, attribute.getName(), attrValue);
userData.setFirstName(attrValue);
} else if ( logicalAttrName.equals(AttributeMappingConstants.PREFERRED_FIRST_NAME_ATTR_MAPPING_KEY) ) {
log.debug("mapLdapAttributeOntoUserData() mapping attribute to User.firstNamePreferred: logical attr name = {}][physical attr name = {}][value = {}]",
logicalAttrName, attribute.getName(), attrValue);
userData.setPreferredFirstName(attrValue);
} else if ( logicalAttrName.equals(AttributeMappingConstants.LAST_NAME_ATTR_MAPPING_KEY) ) {
log.debug("mapLdapAttributeOntoUserData() mapping attribute to User.lastName: [logical attr name = {}][physical attr name = {}][value = {}]",
logicalAttrName, attribute.getName(), attrValue);
userData.setLastName(attrValue);
} else if ( logicalAttrName.equals(AttributeMappingConstants.EMAIL_ATTR_MAPPING_KEY) ) {
log.debug("mapLdapAttributeOntoUserData() mapping attribute to User.email: [logical attr name = {}][physical attr name = {}][value = {}]",
logicalAttrName, attribute.getName(), attrValue);
userData.setEmail(attrValue);
} else if ( logicalAttrName.equals(AttributeMappingConstants.DISPLAY_ID_ATTR_MAPPING_KEY) ) {
log.debug("mapLdapAttributeOntoUserData() mapping attribute to User display Id: logical attr name = {}][physical attr name = {}][value = {}]",
logicalAttrName, attribute.getName(), attrValue);
userData.setProperty(UnboundidDirectoryProvider.DISPLAY_ID_PROPERTY, attrValue);
} else if ( logicalAttrName.equals(AttributeMappingConstants.DISPLAY_NAME_ATTR_MAPPING_KEY) ) {
log.debug("mapLdapAttributeOntoUserData() mapping attribute to User display name: [logical attr name = {}][physical attr name = {}][value = {}]",
logicalAttrName, attribute.getName(), attrValue);
userData.setProperty(UnboundidDirectoryProvider.DISPLAY_NAME_PROPERTY, attrValue);
} else {
log.debug("mapLdapAttributeOntoUserData() mapping attribute to a User property: [logical attr name (and property name) = {}][physical attr name = {}][value = {}]",
logicalAttrName, attribute.getName(), attrValue);
userData.setProperty(logicalAttrName, attrValue);
}
}
/**
* Passes the given LDAPEntry
and a reference to this
* SimpleLdapAttributeMapper
to
* {@link UserTypeMapper#mapLdapEntryToSakaiUserType(LDAPEntry, LdapAttributeMapper)}.
* By default, this will just return an empty String.
*
* @param ldapEntry the LDAPEntry
to map
* @return a String representing a Sakai user type. null
s and
* empty Strings are possible.
*/
protected String mapLdapEntryToSakaiUserType(LDAPEntry ldapEntry) {
return userTypeMapper.mapLdapEntryToSakaiUserType(ldapEntry, this);
}
/**
* Straightforward {@link LdapUserData} to
* {@link org.sakaiproject.user.api.UserEdit} field-to-field mapping, including
* properties.
*/
public void mapUserDataOntoUserEdit(LdapUserData userData, UserEdit userEdit) {
log.debug("mapUserDataOntoUserEdit(): [userData = {}]", userData);
userEdit.setEid(userData.getEid());
userEdit.setFirstName(userData.getFirstName());
userEdit.setLastName(userData.getLastName());
userEdit.setEmail(userData.getEmail());
userEdit.setType(userData.getType());
Properties srcProps = userData.getProperties();
ResourceProperties tgtProps = userEdit.getProperties();
for ( Entry srcProp : srcProps.entrySet() ) {
tgtProps.addProperty((String)srcProp.getKey(),
(String)srcProp.getValue());
}
}
public String escapeSearchFilterTerm(final String unescapedTerm) {
if (unescapedTerm == null) return null;
//From RFC 2254
String term = unescapedTerm.replaceAll("\\\\","\\\\5c");
term = term.replaceAll("\\*","\\\\2a");
term = term.replaceAll("\\(","\\\\28");
term = term.replaceAll("\\)","\\\\29");
term = term.replaceAll("\\"+Character.toString('\u0000'), "\\\\00");
return term;
}
/**
* Map the given logical attribute name to a physical attribute name.
*
* @param key the logical attribute name
* @return the corresponding physical attribute name, or null
* if no mapping exists.
*/
public String getAttributeMapping(String key) {
return attributeMappings.get(key);
}
/**
* Access the configured logical names associated with the given
* physical attribute name. May return null
.
*
* @param physicalAttrName a physical LDAP attribute name to reverse
* map to zero or more logical attribute names
* @return a collection of logical attribute names; may be null
* or empty.
*/
public Collection getReverseAttributeMappings(String physicalAttrName) {
return reverseAttributeMappings.get(physicalAttrName);
}
protected Map> getReverseAttributeMap() {
return this.reverseAttributeMappings;
}
/**
* Implemented to return the current values of
* {link {@link #getAttributeMappings().values()} as
* a String array.
*/
public String[] getSearchResultAttributes() {
return physicalAttrNames;
}
/**
* Returns a direct reference to the currently
* cached mappings. Note that if this map is
* modified, the next call to
* {@link #getSearchResultAttributes()} may
* return stale values.
*/
public Map getAttributeMappings()
{
return attributeMappings;
}
/**
* Caches the given Map reference and takes a
* snapshot of the values therein for future
* use by {@link #getSearchResultAttributes()}.
*
* @see #getAttributeMappings()
*/
public void setAttributeMappings(Map attributeMappings)
{
if ( attributeMappings == null || attributeMappings.isEmpty() ) {
this.attributeMappings = AttributeMappingConstants.DEFAULT_ATTR_MAPPINGS;
} else {
this.attributeMappings = attributeMappings;
}
cachePhysicalAttributeNames();
cacheReverseAttributeLookupMap();
log.debug("setAttributeMappings(): [attrib map = {}]", this.attributeMappings);
log.debug("setAttributeMappings(): [reverse attrib map = {}]", this.reverseAttributeMappings);
log.debug("setAttributeMappings(): [cached phys attrb names = {}]", Arrays.toString(this.physicalAttrNames));
}
/**
* Converts the current attribute map's values into
* a String array.
*/
private void cachePhysicalAttributeNames() {
if ( attributeMappings == null ) {
physicalAttrNames = new String[0];
return;
}
physicalAttrNames = new String[attributeMappings.size()];
int k = 0;
for ( String name : attributeMappings.values() ) {
physicalAttrNames[k++] = name;
}
}
/**
* Caches the result of {@link #reverseAttributeMap(Map)}, passing
* the currently cached attribute map. The result completely
* replaces any currently cached reverse attribute map.
*
*/
private void cacheReverseAttributeLookupMap() {
reverseAttributeMappings = reverseAttributeMap(attributeMappings);
}
/**
* Creates a reverse lookup map of a given attribute map's values.
* That is, creates a map of physical to logical LDAP attribute names.
* Since a multiple logical names may point to a single physical name,
* values in this map are actually {@link Collection}'s.
*
* Protected access control mainly to enable testing
*
* @param toReverse
* @return
*/
protected Map> reverseAttributeMap(Map toReverse) {
Map> reversed = new HashMap>();
for ( Map.Entry entry : toReverse.entrySet()) {
Collection logicalAttrNames =
reversed.get(entry.getValue());
String logicalAttrName = entry.getKey();
String physicalAttrName = entry.getValue();
if (logicalAttrNames == null) {
logicalAttrNames = new ArrayList(1);
logicalAttrNames.add(logicalAttrName);
reversed.put(physicalAttrName, logicalAttrNames);
} else {
logicalAttrNames.add(logicalAttrName);
}
}
return reversed;
}
/**
* Access the strategy for calculating the Sakai user type given a
* LDAPEntry
*/
public UserTypeMapper getUserTypeMapper() {
return userTypeMapper;
}
/** Assign the strategy for calculating the Sakai user type given a
* LDAPEntry
*/
public void setUserTypeMapper(UserTypeMapper userTypeMapper) {
this.userTypeMapper = userTypeMapper;
}
/**
* Determines if a user has a preferredFirstName set and if so, returns it for use.
* Otherwise, returns their firstName as normal.
*
* @param userData the LdapUserData
for the user
* @return a String of the user's first name.
*/
protected String usePreferredFirstName(LdapUserData userData) {
if(StringUtils.isNotBlank(userData.getPreferredFirstName())) {
log.debug("usePreferredFirstName() using firstNamePreferred.");
return userData.getPreferredFirstName();
} else {
log.debug("usePreferredFirstName() using firstName.");
return userData.getFirstName();
}
}
/**
* @inheritDoc
*/
public String getFindUserByCrossAttributeSearchFilter(final String unescapedCriteria) {
String eidAttr = attributeMappings.get(AttributeMappingConstants.LOGIN_ATTR_MAPPING_KEY);
String emailAttr = attributeMappings.get(AttributeMappingConstants.EMAIL_ATTR_MAPPING_KEY);
String givenNameAttr = attributeMappings.get(AttributeMappingConstants.FIRST_NAME_ATTR_MAPPING_KEY);
String lastNameAttr = attributeMappings.get(AttributeMappingConstants.LAST_NAME_ATTR_MAPPING_KEY);
//This explicitly constructs the filter with wildcards in it.
//However, we escape the given criteria to prevent any other injection
String criteria = escapeSearchFilterTerm(unescapedCriteria);
//(|(uid=criteria*)(mail=criteria*)(givenName=criteria*)(sn=criteria*))
StringBuilder sb = new StringBuilder();
sb.append("(|");
sb.append("(");
sb.append(eidAttr);
sb.append("=");
sb.append(criteria);
sb.append("*)");
sb.append("(");
sb.append(emailAttr);
sb.append("=");
sb.append(criteria);
sb.append("*)");
sb.append("(");
sb.append(givenNameAttr);
sb.append("=");
sb.append(criteria);
sb.append("*)");
sb.append("(");
sb.append(lastNameAttr);
sb.append("=");
sb.append(criteria);
sb.append("*)");
sb.append(")");
return sb.toString();
}
/**
* @inheritDoc
*/
public String getManyUsersInOneSearch(Set criteria) {
StringBuilder sb = new StringBuilder();
sb.append("(|");
for ( Iterator eidIterator = criteria.iterator(); eidIterator.hasNext(); ) {
sb.append("(");
sb.append(getFindUserByEidFilter(eidIterator.next()));
sb.append(")");
}
sb.append(")");
log.debug("getManyUsersInOneSearch() completed filter: {}", sb.toString());
return sb.toString();
}
/**
* @return A Map of message formats used for extracting values from LDAP data.
*/
public Map getValueMappings() {
return valueMappings;
}
/**
* @param valueMappings A Map of message formats used for extracting values from LDAP data.
*/
public void setValueMappings(Map valueMappings) {
this.valueMappings = valueMappings;
}
}