/**********************************************************************************
* $URL: https://source.sakaiproject.org/svn/providers/trunk/jldap/src/java/edu/amc/sakai/user/JLDAPDirectoryProvider.java $
* $Id: JLDAPDirectoryProvider.java 132537 2013-12-13 00:23:13Z ottenhoff@longsight.com $
***********************************************************************************
*
* Copyright (c) 2003, 2004, 2005, 2006, 2007, 2008, 2009 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 edu.amc.sakai.user;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.sakaiproject.memory.api.Cache;
import org.sakaiproject.memory.api.MemoryService;
import org.sakaiproject.user.api.ExternalUserSearchUDP;
import org.sakaiproject.user.api.User;
import org.sakaiproject.user.api.UserDirectoryProvider;
import org.sakaiproject.user.api.UserEdit;
import org.sakaiproject.user.api.UserFactory;
import org.sakaiproject.user.api.UsersShareEmailUDP;
import org.apache.commons.lang.StringUtils;
import com.novell.ldap.LDAPConnection;
import com.novell.ldap.LDAPEntry;
import com.novell.ldap.LDAPException;
import com.novell.ldap.LDAPJSSESecureSocketFactory;
import com.novell.ldap.LDAPSearchConstraints;
import com.novell.ldap.LDAPSearchResults;
import com.novell.ldap.LDAPSocketFactory;
/**
* <p>
* An implementation of a Sakai UserDirectoryProvider that authenticates/retrieves
* users from a LDAP directory.
* </p>
*
* @author Dan McCallum, Unicon Inc
* @author David Ross, Albany Medical College
* @author Rishi Pande, Virginia Tech
*/
public class JLDAPDirectoryProvider implements UserDirectoryProvider, LdapConnectionManagerConfig, ExternalUserSearchUDP, UsersShareEmailUDP
{
/** Default LDAP connection port */
public static final int DEFAULT_LDAP_PORT = 389;
/** Default secure/unsecure LDAP connection creation behavior */
public static final boolean DEFAULT_IS_SECURE_CONNECTION = false;
/** Default LDAP access timeout in milliseconds */
public static final int DEFAULT_OPERATION_TIMEOUT_MILLIS = 5000;
/** Default referral following behavior */
public static final boolean DEFAULT_IS_FOLLOW_REFERRALS = false;
public static final boolean DEFAULT_IS_SEARCH_ALIASES = false;
/** Default search scope for filters executed by
* {@link #searchDirectory(String, LDAPConnection, LdapEntryMapper, String[], String, int)}
*/
public static final int DEFAULT_SEARCH_SCOPE = LDAPConnection.SCOPE_SUB;
/** Default LDAP use of connection pooling */
public static final boolean DEFAULT_POOLING = false;
/** Default LDAP maximum number of connections in the pool */
public static final int DEFAULT_POOL_MAX_CONNS = 10;
/** Default LDAP maximum number of objects in a result */
public static final int DEFAULT_MAX_RESULT_SIZE = 1000;
/** Default LDAP maximum number of objects to query for */
public static final int DEFAULT_BATCH_SIZE = 200;
public static final boolean DEFAULT_CASE_SENSITIVE_CACHE_KEYS = false;
public static final boolean DEFAULT_ALLOW_AUTHENTICATION = true;
public static final boolean DEFAULT_AUTHENTICATE_WITH_PROVIDER_FIRST = false;
/** Class-specific logger */
private static Log M_log = LogFactory.getLog(JLDAPDirectoryProvider.class);
/** LDAP host address */
private String ldapHost;
/** LDAP connection port. Defaults to {@link #DEFAULT_LDAP_PORT} */
private int ldapPort = DEFAULT_LDAP_PORT;
/** SSL keystore location */
private String keystoreLocation;
/** SSL keystore password */
private String keystorePassword;
/** DN for LDAP manager user */
private String ldapUser;
/** Password for LDAP manager user */
private String ldapPassword;
/** Should connection allocation include a bind attempt */
private boolean autoBind;
/** Base DN for user lookups */
private String basePath;
/** Toggle SSL connections. Defaults to {@link #DEFAULT_IS_SECURE_CONNECTION} */
private boolean secureConnection = DEFAULT_IS_SECURE_CONNECTION;
/** Should connection pooling be used? */
private boolean pooling = DEFAULT_POOLING;
/** Maximum number of physical connections in the pool */
private int poolMaxConns = DEFAULT_POOL_MAX_CONNS;
/** Maximum number of results from one LDAP query */
private int maxResultSize = DEFAULT_MAX_RESULT_SIZE;
/** The size of each batch to load from LDAP when loading multiple users. */
private int batchSize = DEFAULT_BATCH_SIZE;
/** Socket factory for secure connections. Only relevant if
* {@link #secureConnection} is true. Defaults to a new instance
* of {@link LDAPJSSESecureSocketFactory}.
*/
private LDAPSocketFactory secureSocketFactory =
new LDAPJSSESecureSocketFactory();
/** LDAP referral following behavior. Defaults to {@link #DEFAULT_IS_FOLLOW_REFERRALS} */
private boolean followReferrals = DEFAULT_IS_FOLLOW_REFERRALS;
private boolean searchAliases = DEFAULT_IS_SEARCH_ALIASES;
/**
* Default timeout for operations in milliseconds. Defaults
* to {@link #DEFAULT_OPERATION_TIMEOUT_MILLIS}. Datatype
* matches arg type for
* <code>LDAPConstraints.setTimeLimit(int)<code>.
*/
private int operationTimeout = DEFAULT_OPERATION_TIMEOUT_MILLIS;
private int searchScope = DEFAULT_SEARCH_SCOPE;
/**
* User entry attribute mappings. Keys are logical attr names,
* values are physical attr names.
*
* @see LdapAttributeMapper
*/
private Map<String,String> attributeMappings;
private MemoryService memoryService;
/**
* Cache of {@link LdapUserData} objects, keyed by eid.
* {@link cacheTtl} controls TTL.
*
* TODO: This is a naive implementation: cache
* is completely isolated on each app node.
*/
private Cache userCache;
/** Handles LDAPConnection allocation */
private LdapConnectionManager ldapConnectionManager;
/** Handles LDAP attribute mappings and encapsulates filter writing */
private LdapAttributeMapper ldapAttributeMapper;
/** Currently limited to allowing/disallowing searches for particular user EIDs.
* Implements things like user EID blacklists. */
private EidValidator eidValidator;
/**
* Defaults to an anon-inner class which handles {@link LDAPEntry}(ies)
* by passing them to {@link #mapLdapEntryOntoUserData(LDAPEntry)}, the
* result of which is passed to {@link #cacheUserData(LdapUserData)}
* and returned;
*/
protected LdapEntryMapper defaultLdapEntryMapper = new LdapEntryMapper() {
// doesn't update UserEdit in the off chance the search result actually
// yields multiple records
public Object mapLdapEntry(LDAPEntry searchResult, int resultNum) {
LdapUserData cacheRecord = mapLdapEntryOntoUserData(searchResult);
cacheUserData(cacheRecord);
return cacheRecord;
}
};
private boolean caseSensitiveCacheKeys = DEFAULT_CASE_SENSITIVE_CACHE_KEYS;
/**
* Flag for allowing/disallowing authentication on a global basis
*/
private boolean allowAuthentication = DEFAULT_ALLOW_AUTHENTICATION;
/**
* Flag for controlling the return value of
* {@link #authenticateWithProviderFirst(String)} on a global basis.
*/
private boolean authenticateWithProviderFirst = DEFAULT_AUTHENTICATE_WITH_PROVIDER_FIRST;
public JLDAPDirectoryProvider() {
if ( M_log.isDebugEnabled() ) {
M_log.debug("instantating JLDAPDirectoryProvider");
}
}
/**
* Typically invoked by Spring to complete bean initialization.
* Ensures initialization of delegate {@link LdapConnectionManager}
* and {@link LdapAttributeMapper}
*
* @see #initLdapConnectionManager()
* @see #initLdapAttributeMapper()
*/
public void init()
{
if ( M_log.isDebugEnabled() ) {
M_log.debug("init()");
}
userCache = memoryService.newCache(getClass().getName()+".userCache");
// We don't want to allow people to break their config by setting the batch size to be more than the maxResultsSize.
if (batchSize > maxResultSize) {
batchSize = maxResultSize;
M_log.warn("JLDAP batchSize is larger than maxResultSize, batchSize has been reduced from: "+ batchSize + " to: "+ maxResultSize);
}
initLdapConnectionManager();
initLdapAttributeMapper();
}
/**
* Lazily "injects" a {@link LdapConnectionManager} if one
* has not been assigned already.
*
* <p>
* Implementation note: this approach to initing the
* connection mgr preserves forward compatibility of
* existing config, but config should probably be
* refactored to inject the appropriate config directly
* into the connection mgr.
* </p>
*/
protected void initLdapConnectionManager() {
if ( M_log.isDebugEnabled() ) {
M_log.debug("initLdapConnectionManager()");
}
// all very awkward b/c of the mixed-in config implementation
if ( ldapConnectionManager == null ) {
ldapConnectionManager = newDefaultLdapConnectionManager();
}
ldapConnectionManager.setConfig(this);
ldapConnectionManager.init(); // see javadoc
}
/**
* Lazily "injects" a {@link LdapAttributeMapper} if one
* has not been assigned already.
*
* <p>
* Implementation note: this approach to initing the
* attrib mgr preserves forward compatibility of
* existing config, but config should probably be
* refactored to inject the appropriate config directly
* into the attrib mgr.
* </p>
*/
protected void initLdapAttributeMapper() {
if ( M_log.isDebugEnabled() ) {
M_log.debug("initLdapAttributeMapper()");
}
if ( ldapAttributeMapper == null ) {
// emulate what Spring should really be doing
ldapAttributeMapper = newDefaultLdapAttributeMapper();
ldapAttributeMapper.setAttributeMappings(attributeMappings);
ldapAttributeMapper.init();
}
}
/**
* Factory method for default {@link LdapConnectionManager} instances.
* Ensures forward compatibility of existing config which
* does not specify a delegate {@link LdapConnectionManager}.
*
* @return a new {@link SimpleLdapConnectionManager}
*/
protected LdapConnectionManager newDefaultLdapConnectionManager() {
if (this.isPooling()) {
if ( M_log.isDebugEnabled() ) {
M_log.debug(
"newDefaultLdapConnectionManager(): returning a new PoolingLdapConnectionManager");
}
return new PoolingLdapConnectionManager();
} else {
if ( M_log.isDebugEnabled() ) {
M_log.debug(
"newDefaultLdapConnectionManager(): returning a new SimpleLdapConnectionManager");
}
return new SimpleLdapConnectionManager();
}
}
/**
* Factory method for default {@link LdapAttributeMapper} instances.
* Ensures forward compatibility of existing config which
* does not specify a delegate {@link LdapAttributeMapper}.
*
* @return a new {@link LdapAttributeMapper}
*/
protected LdapAttributeMapper newDefaultLdapAttributeMapper() {
if ( M_log.isDebugEnabled() ) {
M_log.debug(
"newDefaultLdapAttributeMapper(): returning a new SimpleLdapAttributeMapper");
}
return new SimpleLdapAttributeMapper();
}
/**
* Typically called by Spring to signal bean destruction.
*
*/
public void destroy()
{
if ( M_log.isDebugEnabled() ) {
M_log.debug("destroy()");
}
clearCache();
}
/**
* Resets the internal {@link LdapUserData} cache
*/
public void clearCache() {
if ( M_log.isDebugEnabled() ) {
M_log.debug("clearCache()");
}
userCache.clear();
}
/**
* Authenticates the specified user login by recursively searching for
* and binding to a DN below the configured base DN. Search results are
* subsequently added to the cache.
*
* <p>Caching search results departs from
* behavior in <= 2.3.0 versions, which removed cache entries following
* authentication. If the intention is to ensure fresh user data at each
* login, the most natural approach is probably to clear the cache before
* executing the authentication process. At this writing, though, the
* default {@link org.sakaiproject.user.api.UserDirectoryService} impl
* will invoke {@link #getUser(UserEdit)} prior to
* {{@link #authenticateUser(String, UserEdit, String)}} if the Sakai's
* local db does not recognize the specified EID. Therefore, clearing the
* cache at in {{@link #authenticateUser(String, UserEdit, String)}}
* at best leads to confusing mid-session attribute changes. In the future
* we may want to consider strategizing this behavior, or adding an eid
* parameter to {@link #destroyAuthentication()} so cache records can
* be invalidated on logout without ugly dependencies on the
* {@link org.sakaiproject.tool.api.SessionManager}
*
* @see #lookupUserBindDn(String, LDAPConnection)
*/
public boolean authenticateUser(final String userLogin, final UserEdit edit, final String password)
{
if ( M_log.isDebugEnabled() ) {
M_log.debug("authenticateUser(): [userLogin = " + userLogin + "]");
}
if ( !(allowAuthentication) ) {
M_log.debug("authenticateUser(): denying authentication attempt [userLogin = " + userLogin + "]. All authentication has been disabled via configuration");
return false;
}
if ( StringUtils.isBlank(password) )
{
if ( M_log.isDebugEnabled() ) {
M_log.debug("authenticateUser(): returning false, blank password");
}
return false;
}
LDAPConnection conn = null;
try
{
// conn is implicitly bound as manager, if necessary
if ( M_log.isDebugEnabled() ) {
M_log.debug("authenticateUser(): allocating connection for login [userLogin = " + userLogin + "]");
}
conn = ldapConnectionManager.getConnection();
// look up the end-user's DN, which could be nested at some
// arbitrary depth below getBasePath().
// TODO: optimization opportunity if user entries are
// directly below getBasePath()
final String endUserDN = lookupUserBindDn(userLogin, conn);
if ( endUserDN == null ) {
if ( M_log.isDebugEnabled() ) {
M_log.debug("authenticateUser(): failed to find bind dn for login [userLogin = " + userLogin + "], returning false");
}
return false;
}
if ( M_log.isDebugEnabled() ) {
M_log.debug("authenticateUser(): returning connection to pool [userLogin = " + userLogin + "]");
}
ldapConnectionManager.returnConnection(conn);
conn = null;
if ( M_log.isDebugEnabled() ) {
M_log.debug("authenticateUser(): attempting to allocate bound connection [userLogin = " +
userLogin + "][bind dn [" + endUserDN + "]");
}
conn = ldapConnectionManager.getBoundConnection(endUserDN, password);
if ( M_log.isDebugEnabled() ) {
M_log.debug("authenticateUser(): successfully allocated bound connection [userLogin = " +
userLogin + "][bind dn [" + endUserDN + "]");
}
return true;
}
catch (LDAPException e)
{
if (e.getResultCode() == LDAPException.INVALID_CREDENTIALS) {
if ( M_log.isWarnEnabled() ) {
M_log.warn("authenticateUser(): invalid credentials [userLogin = "
+ userLogin + "]");
}
return false;
} else {
throw new RuntimeException(
"authenticateUser(): LDAPException during authentication attempt [userLogin = "
+ userLogin + "][result code = " + e.resultCodeToString() +
"][error message = "+ e.getLDAPErrorMessage() + "]", e);
}
} catch ( Exception e ) {
throw new RuntimeException(
"authenticateUser(): Exception during authentication attempt [userLogin = "
+ userLogin + "]", e);
} finally {
if ( conn != null ) {
if ( M_log.isDebugEnabled() ) {
M_log.debug("authenticateUser(): returning connection to connection manager");
}
ldapConnectionManager.returnConnection(conn);
}
}
}
/**
* Locates a user directory entry using an email address
* as a key. Updates the specified {@link org.sakaiproject.user.api.UserEdit}
* with directory attributes if the search is successful.
* The {@link org.sakaiproject.user.api.UserEdit} param is
* technically optional and will be ignored if <code>null</code>.
*
* <p>
* All {@link java.lang.Exception}s are logged and result in
* a <code>false</code> return, as do searches which yield
* no results. (A concession to backward compat.)
* </p>
*
* @param edit the {@link org.sakaiproject.user.api.UserEdit} to update
* @param email the search key
* @return boolean <code>true</code> if the search
* completed without error and found a directory entry
*/
public boolean findUserByEmail(UserEdit edit, String email)
{
try {
boolean useStdFilter = !(ldapAttributeMapper instanceof EidDerivedEmailAddressHandler);
LdapUserData resolvedEntry = null;
if ( !(useStdFilter) ) {
try {
String eid =
StringUtils.trimToNull(((EidDerivedEmailAddressHandler)ldapAttributeMapper).unpackEidFromAddress(email));
if ( eid == null ) { // shouldn't happen (see unpackEidFromEmail() javadoc)
throw new InvalidEmailAddressException("Attempting to unpack an EID from [" + email +
"] resulted in a null or empty string");
}
resolvedEntry = getUserByEid(eid, null);
} catch ( InvalidEmailAddressException e ) {
M_log.warn("findUserByEmail(): Attempted to look up user at an invalid email address [" + email + "]", e);
useStdFilter = true; // fall back to std processing, we cant derive an EID from this addr
}
}
// we do _only_ fall back to std processing if EidDerivedEmailAddressHandler actually
// indicated it could not handle the given email addr. If it could handle the addr
// but found no results, we honor that empty result set
if ( useStdFilter ) { // value may have been changed in EidDerivedEmailAddressHandler block above
String filter =
ldapAttributeMapper.getFindUserByEmailFilter(email);
resolvedEntry = (LdapUserData)searchDirectoryForSingleEntry(filter,
null, null, null, null);
}
if ( resolvedEntry == null ) {
if ( M_log.isDebugEnabled() ) {
M_log.debug("findUserByEmail(): failed to find user by email [email = " + email + "]");
}
return false;
}
if ( M_log.isDebugEnabled() ) {
M_log.debug("findUserByEmail(): found user by email [email = " + email + "]");
}
if ( edit != null ) {
mapUserDataOntoUserEdit(resolvedEntry, edit);
}
return true;
} catch ( Exception e ) {
M_log.error("findUserByEmail(): failed [email = " + email + "]");
M_log.debug("Exception: ", e);
return false;
}
/*
if ( M_log.isDebugEnabled() ) {
M_log.debug("findUserByEmail(): [email = " + email + "]");
}
try {
String filter =
ldapAttributeMapper.getFindUserByEmailFilter(email);
// takes care of caching and everything
LdapUserData mappedEntry =
(LdapUserData)searchDirectoryForSingleEntry(filter,
null, null, null, null);
if ( mappedEntry == null ) {
if ( M_log.isDebugEnabled() ) {
M_log.debug("findUserByEmail(): failed to find user by email [email = " + email + "]");
}
return false;
}
if ( M_log.isDebugEnabled() ) {
M_log.debug("findUserByEmail(): found user by email [email = " + email + "]");
}
if ( edit != null ) {
mapUserDataOntoUserEdit(mappedEntry, edit);
}
return true;
} catch (Exception e) {
M_log.error("findUserByEmail(): failed [email = " + email + "]", e);
return false;
}
*/
}
/**
* Effectively the same as
* <code>getUserByEid(edit, edit.getEid())</code>.
*
* @see #getUserByEid(UserEdit, String)
*/
public boolean getUser(UserEdit edit)
{
try {
return getUserByEid(edit, edit.getEid(), null);
} catch ( LDAPException e ) {
M_log.error("getUser() failed [eid: " + edit.getEid() + "]", e);
return false;
}
}
/**
* Similar to iterating over <code>users</code> passing
* each element to {@link #getUser(UserEdit)}, removing the
* {@link org.sakaiproject.user.api.UserEdit} if that method
* returns <code>false</code>.
*
* <p>Adds search retry capability if any one lookup fails
* with a directory error. Empties <code>users</code> and
* returns if a retry exits exceptionally
* <p>
*/
public void getUsers(Collection<UserEdit> users)
{
if ( M_log.isDebugEnabled() ) {
M_log.debug("getUsers(): [Collection size = " + users.size() + "]");
}
LDAPConnection conn = null;
boolean abortiveSearch = false;
int maxQuerySize = getMaxObjectsToQueryFor();
UserEdit userEdit = null;
HashMap<String, UserEdit> usersToSearchInLDAP = new HashMap<String, UserEdit>();
List<UserEdit> usersToRemove = new ArrayList<UserEdit>();
try {
int cnt = 0;
for ( Iterator<UserEdit> userEdits = users.iterator(); userEdits.hasNext(); ) {
userEdit = (UserEdit) userEdits.next();
String eid = userEdit.getEid();
if ( !(isSearchableEid(eid)) ) {
userEdits.remove();
//proceed ahead with this (perhaps the final) iteration
//usersToSearchInLDAP needs to be processed unless empty
} else {
// Check the cache before sending the request to LDAP
LdapUserData cachedUserData = getCachedUserEntry(eid);
if ( cachedUserData == null ) {
usersToSearchInLDAP.put(eid, userEdit);
cnt++;
} else {
// populate userEdit with cached ldap data:
mapUserDataOntoUserEdit(cachedUserData, userEdit);
}
}
// We need to make sure this query isn't larger than maxQuerySize
if ((!userEdits.hasNext() || cnt == maxQuerySize) && !usersToSearchInLDAP.isEmpty()) {
if (conn == null) {
conn = ldapConnectionManager.getConnection();
}
String filter = ldapAttributeMapper.getManyUsersInOneSearch(usersToSearchInLDAP.keySet());
List<LdapUserData> ldapUsers = searchDirectory(filter, null, null, null, null, maxQuerySize);
for (LdapUserData ldapUserData : ldapUsers) {
String ldapEid = ldapUserData.getEid();
if (StringUtils.isEmpty(ldapEid)) {
continue;
}
if (!(caseSensitiveCacheKeys)) {
ldapEid = ldapEid.toLowerCase();
}
UserEdit ue = usersToSearchInLDAP.get(ldapEid);
mapUserDataOntoUserEdit(ldapUserData, ue);
usersToSearchInLDAP.remove(ldapEid);
}
// see if there are any users that we could not find in the LDAP query
for (Map.Entry<String, UserEdit> entry : usersToSearchInLDAP.entrySet()) {
usersToRemove.add(entry.getValue());
}
// clear the HashMap and reset the counter
usersToSearchInLDAP.clear();
cnt = 0;
}
}
// Finally clean up the original collection and remove and users we could not find
for (UserEdit userRemove : usersToRemove) {
if (M_log.isDebugEnabled()) {
M_log.debug("JLDAP getUsers could not find user: " + userRemove.getEid());
}
users.remove(userRemove);
}
} catch (LDAPException e) {
abortiveSearch = true;
throw new RuntimeException("getUsers(): LDAPException during search [eid = " +
(userEdit == null ? null : userEdit.getEid()) +
"][result code = " + e.resultCodeToString() +
"][error message = " + e.getLDAPErrorMessage() + "]", e);
} catch ( Exception e ) {
abortiveSearch = true;
throw new RuntimeException("getUsers(): RuntimeException during search eid = " +
(userEdit == null ? null : userEdit.getEid()) +
"]", e);
} finally {
if ( conn != null ) {
if ( M_log.isDebugEnabled() ) {
M_log.debug("getUsers(): returning connection to connection manager");
}
ldapConnectionManager.returnConnection(conn);
}
// no sense in returning a partially complete search result
if ( abortiveSearch ) {
if ( M_log.isDebugEnabled() ) {
M_log.debug("getUsers(): abortive search, clearing received users collection");
}
users.clear();
}
}
}
/**
* By default returns the global boolean setting configured
* via {@link #setAuthenticateWithProviderFirst(boolean)}.
*/
public boolean authenticateWithProviderFirst(String id)
{
return authenticateWithProviderFirst;
}
/**
* Effectively the same as <code>getUserByEid(null,eid)</code>.
*
* @see #getUserByEid(UserEdit, String)
*/
public boolean userExists(String eid)
{
if ( M_log.isDebugEnabled() ) {
M_log.debug("userExists(): [eid = " + eid + "]");
}
try {
return getUserByEid(null, eid, null);
} catch ( LDAPException e ) {
M_log.error("userExists() failed: [eid = " + eid + "]", e);
return false;
}
}
/**
* Finds a user record using an <code>eid</code> as an index.
* Updates the given {@link org.sakaiproject.user.api.UserEdit}
* if a directory entry is found.
*
* @see #getUserByEid(String, LDAPConnection)
* @param userToUpdate the {@link org.sakaiproject.user.api.UserEdit}
* to update, may be <code>null<code>
* @param eid the user ID
* @param conn a <code>LDAPConnection</code> to reuse. may be <code>null</code>
* @return <code>true</code> if the directory entry was found, false if the
* search returns without error but without results
* @throws LDAPException if the search returns with a directory access error
*/
protected boolean getUserByEid(UserEdit userToUpdate, String eid, LDAPConnection conn)
throws LDAPException {
LdapUserData foundUserData = getUserByEid(eid, conn);
if ( foundUserData == null ) {
return false;
}
if ( userToUpdate != null ) {
mapUserDataOntoUserEdit(foundUserData, userToUpdate);
}
return true;
}
/**
* Finds a user record using an <code>eid</code> as an index.
*
* @param eid the Sakai EID to search on
* @param conn an optional {@link LDAPConnection}
* @return object representing the found LDAP entry, or null if no results
* @throws LDAPException if the search returns with a directory access error
*/
protected LdapUserData getUserByEid(String eid, LDAPConnection conn)
throws LDAPException {
if ( M_log.isDebugEnabled() ) {
M_log.debug("getUserByEid(): [eid = " + eid + "]");
}
LdapUserData cachedUserData = getCachedUserEntry(eid);
boolean foundCachedUserData = cachedUserData != null;
if ( foundCachedUserData ) {
if ( M_log.isDebugEnabled() ) {
M_log.debug("getUserByEid(): found cached user [eid = " + eid + "]");
}
return cachedUserData;
}
if ( !(isSearchableEid(eid)) ) {
if ( M_log.isInfoEnabled() ) {
M_log.info("User EID not searchable (possibly blacklisted or otherwise syntactically invalid) [" + eid + "]");
}
return null;
}
String filter =
ldapAttributeMapper.getFindUserByEidFilter(eid);
// takes care of caching and everything
return (LdapUserData)searchDirectoryForSingleEntry(filter,
conn, null, null, null);
}
/**
* Consults the cached {@link EidValidator} to determine if the
* given {@link User} EID is searchable. Allows any EID if no
* {@link EidValidator} has been configured.
*
* @param eid a user EID, possibly <code>null</code> or otherwise "empty"
* @return <code>true</code> if no {@link EidValidator} has been
* set, or the result of {@link EidValidator#isSearchableEid(String)}
*/
protected boolean isSearchableEid(String eid) {
if ( eidValidator == null ) {
return true;
}
return eidValidator.isSearchableEid(eid);
}
/**
* Search the directory for a DN corresponding to a user's
* EID. Typically, this is the same as DN of the object
* from which the user's attributes are retrieved, but
* that need not necessarily be the case.
*
* @see #getUserByEid(String, LDAPConnection)
* @see LdapAttributeMapper#getUserBindDn(LdapUserData)
* @param eid the user's Sakai EID
* @param conn an optional {@link LDAPConnection}
* @return the user's bindable DN or null if no matching directory entry
* @throws LDAPException if the directory query exits with an error
*/
protected String lookupUserBindDn(String eid, LDAPConnection conn)
throws LDAPException {
if ( M_log.isDebugEnabled() ) {
M_log.debug("lookupUserEntryDN(): [eid = " + eid +
"][reusing conn = " + (conn != null) + "]");
}
LdapUserData foundUserData = getUserByEid(eid, conn);
if ( foundUserData == null ) {
if ( M_log.isDebugEnabled() ) {
M_log.debug("lookupUserEntryDN(): no directory entried found [eid = " +
eid + "]");
}
return null;
}
return ldapAttributeMapper.getUserBindDn(foundUserData);
}
/**
* Searches the directory for at most one entry matching the
* specified filter.
*
* @param filter a search filter
* @param conn an optional {@link LDAPConnection}
* @param searchResultPhysicalAttributeNames
* @param searchBaseDn
* @return a matching <code>LDAPEntry</code> or <code>null</code> if no match
* @throws LDAPException if the search exits with an error
*/
protected Object searchDirectoryForSingleEntry(String filter,
LDAPConnection conn,
LdapEntryMapper mapper,
String[] searchResultPhysicalAttributeNames,
String searchBaseDn)
throws LDAPException {
if ( M_log.isDebugEnabled() ) {
M_log.debug("searchDirectoryForSingleEntry(): [filter = " + filter +
"][reusing conn = " + (conn != null) + "]");
}
List<LdapUserData> results = searchDirectory(filter, conn,
mapper,
searchResultPhysicalAttributeNames,
searchBaseDn,
1);
if ( results.isEmpty() ) {
return null;
}
return results.iterator().next();
}
/**
* Execute a directory search using the specified filter
* and connection. Maps each resulting {@link LDAPEntry}
* to a {@link LdapUserData}, returning a {@link List}
* of the latter.
*
* @param filter the search filter
* @param conn an optional {@link LDAPConnection}
* @param mapper result interpreter. Defaults to
* {@link #defaultLdapEntryMapper} if <code>null</code>
* @param searchResultPhysicalAttributeNames attributes to retrieve.
* May be <code>null</code>, in which case defaults to
* {@link LdapAttributeMapper#getSearchResultAttributes()}.
* @param searchBaseDn base DN from which to begin search.
* May be <code>null</code>, in which case defaults to assigned
* <code>basePath</code>
* @param maxResults maximum number of retrieved LDAP objects. Ignored
* if <= 0
* @return An empty {@link List} if no results. Will not return <code>null</code>
* @throws LDAPException if thrown by the search
* @throws RuntimeExction wrapping any non-{@link LDAPException} {@link Exception}
*/
protected List<LdapUserData> searchDirectory(String filter,
LDAPConnection conn,
LdapEntryMapper mapper,
String[] searchResultPhysicalAttributeNames,
String searchBaseDn,
int maxResults)
throws LDAPException {
boolean receivedConn = conn != null;
if ( M_log.isDebugEnabled() ) {
M_log.debug("searchDirectory(): [filter = " + filter +
"][reusing conn = " + receivedConn + "]");
}
try {
if ( !(receivedConn) ) {
conn = ldapConnectionManager.getConnection();
}
if (conn == null) {
throw new IllegalStateException("Unable to obtain a valid LDAP connection");
}
searchResultPhysicalAttributeNames =
scrubSearchResultPhysicalAttributeNames(
searchResultPhysicalAttributeNames);
searchBaseDn =
scrubSearchBaseDn(searchBaseDn);
if ( mapper == null ) {
mapper = defaultLdapEntryMapper;
}
// TODO search constraints should be configurable in their entirety
LDAPSearchConstraints constraints = new LDAPSearchConstraints();
if (isSearchAliases()) {
constraints.setDereference(LDAPSearchConstraints.DEREF_ALWAYS);
} else {
constraints.setDereference(LDAPSearchConstraints.DEREF_NEVER);
}
constraints.setTimeLimit(operationTimeout);
constraints.setReferralFollowing(followReferrals); // TODO: Do we want to make an explicit set optional?
// Batch size is zero because we don't process the results until they are all in.
constraints.setBatchSize(0);
constraints.setMaxResults(maxResults);
if ( M_log.isDebugEnabled() ) {
M_log.debug("searchDirectory(): [baseDN = " +
searchBaseDn + "][filter = " + filter +
"][return attribs = " +
Arrays.toString(searchResultPhysicalAttributeNames) +
"][max results = " + maxResults + "]" +
"][search scope = " + searchScope + "]");
}
LDAPSearchResults searchResults =
conn.search(searchBaseDn,
searchScope,
filter,
searchResultPhysicalAttributeNames,
false,
constraints);
List<LdapUserData> mappedResults = new ArrayList<LdapUserData>();
int resultCnt = 0;
while ( searchResults.hasMore() ) {
LDAPEntry entry = searchResults.next();
Object mappedResult = mapper.mapLdapEntry(entry, ++resultCnt);
if ( mappedResult == null ) {
continue;
}
mappedResults.add((LdapUserData) mappedResult);
}
return mappedResults;
} catch (LDAPException e) {
throw e;
} catch ( Exception e ) {
throw new RuntimeException("searchDirectory(): RuntimeException while executing search [baseDN = " +
searchBaseDn + "][filter = " + filter +
"][return attribs = " +
Arrays.toString(searchResultPhysicalAttributeNames) +
"][max results = " + maxResults + "]", e);
} finally {
if ( !(receivedConn) && conn != null ) {
if ( M_log.isDebugEnabled() ) {
M_log.debug("searchDirectory(): returning connection to connection manager");
}
ldapConnectionManager.returnConnection(conn);
}
}
}
/**
* Responsible for pre-processing base DNs passed to
* {@link #searchDirectory(String, LDAPConnection, String[], String, int)}.
* As implemented, simply checks for a <code>null</code> reference,
* in which case it returns the currently cached "basePath". Otherwise
* returns the received <code>String</code> as is.
*
* @see #setBasePath(String)
* @param searchBaseDn a proposed base DN. May be <code>null</code>
* @return a default base DN or the received DN, if non <code>null</code>. Return
* value may be <code>null</code> if no default base DN has been configured
*/
protected String scrubSearchBaseDn(String searchBaseDn) {
searchBaseDn = searchBaseDn == null ? basePath : searchBaseDn;
return searchBaseDn;
}
/**
* Responsible for pre-processing search result attribute names
* passed to
* {@link #searchDirectory(String, LDAPConnection, String[], String, int)}.
* If the given <code>String[]></code> is <code>null</code>,
* will use {@link LdapAttributeMapper#getSearchResultAttributes()}.
* If that method returns <code>null</code> will return an empty
* <code>String[]></code>. Otherwise returns the received <code>String[]></code>
* as-is.
*
* @param searchResultPhysicalAttributeNames
* @return
*/
protected String[] scrubSearchResultPhysicalAttributeNames(
String[] searchResultPhysicalAttributeNames) {
if ( searchResultPhysicalAttributeNames == null ) {
searchResultPhysicalAttributeNames =
ldapAttributeMapper.getSearchResultAttributes();
}
if ( searchResultPhysicalAttributeNames == null ) {
searchResultPhysicalAttributeNames = new String[0];
}
return searchResultPhysicalAttributeNames;
}
/**
* Maps attributes from the specified <code>LDAPEntry</code> onto
* a newly instantiated {@link LdapUserData}. Implemented to
* delegate to the currently assigned {@link LdapAttributeMapper}.
*
* @see LdapAttributeMapper#mapLdapEntryOntoUserData(LDAPEntry, LdapUserData)
* @param ldapEntry a non-null directory entry to map
* @return a new {@link LdapUserData}, populated with directory
* attributes
*/
protected LdapUserData mapLdapEntryOntoUserData(LDAPEntry ldapEntry) {
if ( M_log.isDebugEnabled() ) {
M_log.debug("mapLdapEntryOntoUserData() [dn = " + ldapEntry.getDN() + "]");
}
LdapUserData userData = newLdapUserData();
ldapAttributeMapper.mapLdapEntryOntoUserData(ldapEntry, userData);
return userData;
}
/**
* Instantiates a {@link LdapUserData}. This method exists primarily for
* overriding in test cases.
*
* @return a new {@link LdapUserData}
*/
protected LdapUserData newLdapUserData() {
return new LdapUserData();
}
/**
* Maps attribites from the specified {@link LdapUserData} onto
* a {@link org.sakaiproject.user.api.UserEdit}. Implemented to
* delegate to the currently assigned {@link LdapAttributeMapper}.
*
* @see LdapAttributeMapper#mapUserDataOntoUserEdit(LdapUserData, UserEdit)
* @param userData a non-null user cache entry
* @param userEdit a non-null user domain object
*/
protected void mapUserDataOntoUserEdit(LdapUserData userData, UserEdit userEdit) {
if ( M_log.isDebugEnabled() ) {
// std. UserEdit impl has no meaningful toString() impl
M_log.debug("mapUserDataOntoUserEdit() [cache record = " + userData + "]");
}
// delegate to the LdapAttributeMapper since it knows the most
// about how the LdapUserData instance was originally populated
ldapAttributeMapper.mapUserDataOntoUserEdit(userData, userEdit);
// This is not an entirely satisfactory solution, but it's important
// for all attribute mapping to respect this configuration (SAK-12705),
// so we centralized the logic rather than rely on swappable attribute
// mapping plugins. We decided to override the EID casing when mapping
// to UserEdits rather than when mapping to LdapUserDatas since we
// felt it was better to keep the caching of LDAP data and the mapping
// of that data to Sakai-consumable values as separate concerns.
//
// One wonders if a better solution might be to enforce case-sentivity
// rules where they matter, though, which is currenty in the UDS.
if ( !(caseSensitiveCacheKeys) ) {
userEdit.setEid(toCaseInsensitiveCacheKey(userData.getEid()));
}
}
/**
* Retrieve a user record from the cache.
*
* @param eid the cache key
* @return a user cache record, or null if a cache miss
*/
protected LdapUserData getCachedUserEntry(String eid) {
if ( M_log.isDebugEnabled() ) {
M_log.debug("getCachedUserEntry(): [eid = " + eid + "]");
}
if ( !(caseSensitiveCacheKeys) ) {
eid = toCaseInsensitiveCacheKey(eid);
}
LdapUserData cachedUserEntry = (LdapUserData) userCache.get(eid);
if (cachedUserEntry != null) {
if ( M_log.isDebugEnabled() ) {
M_log.debug("getCachedUserEntry(): [found entry = " + cachedUserEntry.toString() + "]");
}
return cachedUserEntry;
}
return null;
}
/**
* Add a {@link LdapUserData} object to the cache. Responsible
* for the setting the freshness timestamp.
*
* @param user the {@link LdapUserData} to add to the cache
*/
protected void cacheUserData(LdapUserData user){
String eid = user.getEid();
if ( eid == null ) {
throw new IllegalArgumentException("Attempted to cache a user record without an eid [UserData = " + user + "]");
}
user.setTimeStamp(System.currentTimeMillis());
if ( M_log.isDebugEnabled() ) {
M_log.debug("cacheUserData(): [user record = " + user + "]");
}
if ( !(caseSensitiveCacheKeys) ) {
eid = toCaseInsensitiveCacheKey(eid);
}
userCache.put(eid, user);
}
protected String toCaseInsensitiveCacheKey(String eid) {
if ( eid == null ) {
return null;
}
return eid.toLowerCase();
}
/**
* {@inheritDoc}
*/
public String getLdapHost()
{
return ldapHost;
}
/**
* {@inheritDoc}
*/
public void setLdapHost(String ldapHost)
{
this.ldapHost = ldapHost;
}
/**
* {@inheritDoc}
*/
public int getLdapPort()
{
return ldapPort;
}
/**
* {@inheritDoc}
*/
public void setLdapPort(int ldapPort)
{
this.ldapPort = ldapPort;
}
/**
* {@inheritDoc}
*/
public String getLdapUser() {
return ldapUser;
}
/**
* {@inheritDoc}
*/
public void setLdapUser(String ldapUser) {
this.ldapUser = ldapUser;
}
/**
* {@inheritDoc}
*/
public String getLdapPassword() {
return ldapPassword;
}
/**
* {@inheritDoc}
*/
public void setLdapPassword(String ldapPassword) {
this.ldapPassword = ldapPassword;
}
/**
* {@inheritDoc}
*/
public boolean isSecureConnection()
{
return secureConnection;
}
/**
* {@inheritDoc}
*/
public void setSecureConnection(boolean secureConnection)
{
this.secureConnection = secureConnection;
}
/**
* {@inheritDoc}
*/
public String getKeystoreLocation()
{
return keystoreLocation;
}
/**
* {@inheritDoc}
*/
public void setKeystoreLocation(String keystoreLocation)
{
this.keystoreLocation = keystoreLocation;
}
/**
* {@inheritDoc}
*/
public String getKeystorePassword()
{
return keystorePassword;
}
/**
* {@inheritDoc}
*/
public void setKeystorePassword(String keystorePassword)
{
this.keystorePassword = keystorePassword;
}
/**
* {@inheritDoc}
*/
public LDAPSocketFactory getSecureSocketFactory()
{
return secureSocketFactory;
}
/**
* {@inheritDoc}
*/
public void setSecureSocketFactory(LDAPSocketFactory secureSocketFactory)
{
this.secureSocketFactory = secureSocketFactory;
}
public String getBasePath()
{
return basePath;
}
public void setBasePath(String basePath)
{
this.basePath = basePath;
}
/**
* {@inheritDoc}
*/
public int getOperationTimeout()
{
return operationTimeout;
}
/**
* {@inheritDoc}
*/
public void setOperationTimeout(int operationTimeout)
{
this.operationTimeout = operationTimeout;
}
/**
* @return LDAP attribute map, keys are logical names,
* values are physical names. may be null
*/
public Map<String, String> getAttributeMappings()
{
return attributeMappings;
}
/**
* @param attributeMappings LDAP attribute map, keys are logical names,
* values are physical names. may be null
*/
public void setAttributeMappings(Map<String, String> attributeMappings)
{
this.attributeMappings = attributeMappings;
}
/**
* {@inheritDoc}
*/
public boolean isFollowReferrals() {
return followReferrals;
}
/**
* {@inheritDoc}
*/
public void setFollowReferrals(boolean followReferrals) {
this.followReferrals = followReferrals;
}
/**
* {@inheritDoc}
*/
public boolean isAutoBind() {
return autoBind;
}
/**
* {@inheritDoc}
*/
public void setAutoBind(boolean autoBind) {
this.autoBind = autoBind;
}
/**
* {@inheritDoc}
*/
public boolean isPooling() {
return pooling;
}
/**
* {@inheritDoc}
*/
public void setPooling(boolean pooling) {
this.pooling = pooling;
}
/**
* {@inheritDoc}
*/
public int getPoolMaxConns() {
return poolMaxConns;
}
/**
* {@inheritDoc}
*/
public void setPoolMaxConns(int poolMaxConns) {
this.poolMaxConns = poolMaxConns;
}
/**
* {@inheritDoc}
*/
public int getMaxObjectsToQueryFor() {
return getBatchSize();
}
/**
* {@inheritDoc}
*/
public void setMaxObjectsToQueryFor (int maxObjectsToQueryFor) {
M_log.info("maxObjectToQueryFor is deprecated please use " + "batchSize@org.sakaiproject.user.api.UserDirectoryProvider instead");
setBatchSize(maxObjectsToQueryFor);
}
/**
* {@inheritDoc}
*/
public int getBatchSize() {
return batchSize;
}
/**
* {@inheritDoc}
*/
public void setBatchSize(int batchSize) {
this.batchSize = batchSize;
}
/**
* {@inheritDoc}
*/
public int getMaxResultSize() {
return maxResultSize;
}
/**
* {@inheritDoc}
*/
public void setMaxResultSize(int maxResultSize) {
this.maxResultSize = maxResultSize;
}
/**
* Access the currently assigned {@link LdapConnectionManager} delegate.
* This delegate handles LDAPConnection allocation.
*
* @return the current {@link LdapConnectionManager}. May be
* null if {@link #init()} has not been called yet.
*/
public LdapConnectionManager getLdapConnectionManager() {
return ldapConnectionManager;
}
/**
* Assign the {@link LdapConnectionManager} delegate. This
* delegate handles LDAPConnection allocation.
*
* @param ldapConnectionManager a {@link LdapConnectionManager}.
* may be null
*/
public void setLdapConnectionManager(LdapConnectionManager ldapConnectionManager) {
this.ldapConnectionManager = ldapConnectionManager;
}
/**
* Access the currently assigned {@link LdapAttributeMapper} delegate.
* This delegate handles LDAP attribute mappings and encapsulates filter
* writing.
*
* @return the current {@link LdapAttributeMapper}. May be
* null if {@link #init()} has not been called yet.
*/
public LdapAttributeMapper getLdapAttributeMapper() {
return ldapAttributeMapper;
}
/**
* Assign the {@link LdapAttributeMapper} delegate. This delegate
* handles LDAP attribute mappings and encapsulates filter
* writing.
*
* @param ldapAttributeMapper a {@link LdapAttributeMapper}.
* may be null
*/
public void setLdapAttributeMapper(LdapAttributeMapper ldapAttributeMapper) {
this.ldapAttributeMapper = ldapAttributeMapper;
}
/**
* Set the cache key case-sensitivity behavior. Defaults to
* {@link #DEFAULT_CASE_SENSITIVE_CACHE_KEYS}. At this writing,
* the cache is keyed exclusively by <code>User.eid</code> values.
*
* @see #cacheUserData(LdapUserData)
* @see #getCachedUserEntry(String)
* @see #defaultLdapEntryMapper
* @param caseSensitive
*/
public void setCaseSensitiveCacheKeys(boolean caseSensitive) {
this.caseSensitiveCacheKeys = caseSensitive;
}
/**
* Access the cache key case-sensitivity behavior. Defaults to
* {@link #DEFAULT_CASE_SENSITIVE_CACHE_KEYS}. At this writing,
* the cache is keyed exclusively by <code>User.eid</code> values.
*
* @see #cacheUserData(LdapUserData)
* @see #getCachedUserEntry(String)
* @see #defaultLdapEntryMapper
* @return boolean
*/
public boolean isCaseSensitiveCacheKeys() {
return caseSensitiveCacheKeys;
}
/**
* Access the service used to verify EIDs prior to executing
* searches on those values.
*
* @see #isSearchableEid(String)
* @return an {@link EidValidator} or <code>null</code> if no
* such dependency has been configured
*/
public EidValidator getEidValidator() {
return eidValidator;
}
/**
* Assign the service used to verify EIDs prior to executing
* searches on those values. This field defaults to <code>null</code>
* indicating that all EIDs are searchable.
*
* @param eidValidator an {@link EidValidator} or <code>null</code>
* to indicate that all EIDs are searchable.
*/
public void setEidValidator(EidValidator eidValidator) {
this.eidValidator = eidValidator;
}
/**
* Access the current global authentication "on/off"
* switch.
*
* @see #setAllowAuthentication(boolean)
*
* @return boolean
*/
public boolean isAllowAuthentication() {
return allowAuthentication;
}
/**
* Access the current global authentication "on/off" switch.
* <code>false</code> completely disables
* {@link #authenticateUser(String, UserEdit, String)} (regardless of
* the value returned from
* {@link #authenticateWithProviderFirst(String)}). <code>true</code>
* enables the {@link #authenticateUser(String, UserEdit, String)}
* algorithm. To simply authenticate all users without
* checking credentials, e.g. in a test environment, consider overriding
* {@link #authenticateUser(String, UserEdit, String)} altogether.
*
* <p>Defaults to {@link #DEFAULT_ALLOW_AUTHENTICATION}</p>
*
* @param allowAuthentication
*/
public void setAllowAuthentication(boolean allowAuthentication) {
this.allowAuthentication = allowAuthentication;
}
/**
* An alias of {@link #setAllowAuthentication(boolean)} for backward
* compatibility with existing customized deployments of this provider
* which had already implemented this feature.
*
* @param authenticateAllowed
*/
public void setAuthenticateAllowed(boolean authenticateAllowed) {
setAllowAuthentication(authenticateAllowed);
}
/**
* Access the configured global return value for
* {@link #authenticateWithProviderFirst(String)}. See
* {@link #setAuthenticateWithProviderFirst(boolean)} for
* additional semantics.
*
* @return boolean
*/
public boolean isAuthenticateWithProviderFirst() {
return authenticateWithProviderFirst;
}
/**
* Configure the global return value of
* {@link #authenticateWithProviderFirst(String)}. Be aware that
* future development may expose a first-class extension point
* for custom implementations of {@link #authenticateWithProviderFirst(String)},
* in which case the value configured here will be treated as a default
* rather than an override.
*
* @param authenticateWithProviderFirst
*/
public void setAuthenticateWithProviderFirst(
boolean authenticateWithProviderFirst) {
this.authenticateWithProviderFirst = authenticateWithProviderFirst;
}
/**
* Access the configured search scope for all filters executed by
* {@link #searchDirectory(String, LDAPConnection, LdapEntryMapper, String[], String, int)}.
* int value corresponds to a constant in {@link LDAPConnection}:
* SCOPE_BASE = 0, SCOPE_ONE = 1, SCOPE_SUB = 2. Defaults to
* {@link #DEFAULT_SEARCH_SCOPE}.
*
*/
public int getSearchScope() {
return searchScope;
}
/**
* Set the configured search scope for all filters executed by
* {@link #searchDirectory(String, LDAPConnection, LdapEntryMapper, String[], String, int)}.
* Validated
*
* @param searchScope
* @throws IllegalArgumentException if given scope value is invalid
*/
public void setSearchScope(int searchScope) throws IllegalArgumentException {
switch ( searchScope ) {
case LDAPConnection.SCOPE_BASE :
case LDAPConnection.SCOPE_ONE :
case LDAPConnection.SCOPE_SUB :
this.searchScope = searchScope;
return;
default :
throw new IllegalArgumentException("Invalid search scope [" + searchScope +"]");
}
}
public MemoryService getMemoryService() {
return memoryService;
}
public void setMemoryService(MemoryService memoryService) {
this.memoryService = memoryService;
}
/**
* Search all the externally provided users that match this criteria in eid,
* email, first or last name.
*
* @param criteria
* The search criteria.
* @param first
* The first record position to return.
* @param last
* The last record position to return.
* @return A list (User) of all the aliases matching the criteria, within the
* record range given (sorted by sort name).
*/
//public List<User> searchUsers(String criteria, int first, int last) {
// M_log.error("Not yet implemented");
// return null;
//}
/**
* Search for externally provided users that match this criteria in eid, email, first or last name.
*
* <p>Returns a List of UserEdit objects. This list will be <b>empty</b> if no results are returned or <b>null</b>
* if your external provider does not implement this interface.<br />
*
* The list will also be null if the LDAP server returns an error, for example an '(11) Administrative Limit Exceeded'
* or '(4) Sizelimit Exceeded', due to a search term being too broad and returning too many results.</p>
*
* <p>See LdapAttributeMapper.getFindUserByCrossAttributeSearchFilter for the filter used.</p>
*
* @param criteria
* The search criteria.
* @param first
* The first record position to return. LDAP does not support paging so this value is unused.
* @param last
* The last record position to return. LDAP does not support paging so this value is unused.
* @param factory
* Use this factory's newUser() method to create the UserEdit objects you populate and return in the List.
* @return
* A list (UserEdit) of all the users matching the criteria.
*/
public List<UserEdit> searchExternalUsers(String criteria, int first, int last, UserFactory factory) {
String filter = ldapAttributeMapper.getFindUserByCrossAttributeSearchFilter(criteria);
List<UserEdit> users = new ArrayList<UserEdit>();
try {
//no limit to the number of search results, use the LDAP server's settings.
List<LdapUserData> ldapUsers = searchDirectory(filter, null, null, null, null, maxResultSize);
for(LdapUserData ldapUserData: ldapUsers) {
//create a user object and map the data onto it
//SAK-20625 ensure we have an id-eid mapping at this time
UserEdit user = factory.newUser(ldapUserData.getEid());
mapUserDataOntoUserEdit(ldapUserData, user);
users.add(user);
}
} catch (LDAPException e) {
M_log.warn("An error occurred searching for users: " + e.getClass().getName() + ": (" + e.getResultCode() + ") " + e.getMessage());
return null;
}
return users;
}
/**
* Find all user objects which have this email address.
*
* @param email
* The email address string.
* @param factory
* To create all the UserEdit objects you populate and return in the return collection.
* @return Collection (UserEdit) of user objects that have this email address, or an empty Collection if there are none.
*/
@SuppressWarnings("rawtypes")
public Collection findUsersByEmail(String email, UserFactory factory) {
String filter = ldapAttributeMapper.getFindUserByEmailFilter(email);
List<User> users = new ArrayList<User>();
try {
List<LdapUserData> ldapUsers = searchDirectory(filter, null, null, null, null, maxResultSize);
for(LdapUserData ldapUserData: ldapUsers) {
//SAK-20625 ensure we have an id-eid mapping at this time
UserEdit user = factory.newUser(ldapUserData.getEid());
mapUserDataOntoUserEdit(ldapUserData, user);
users.add(user);
}
} catch (LDAPException e) {
M_log.warn("An error occurred finding users by email: " + e.getClass().getName() + ": (" + e.getResultCode() + ") " + e.getMessage());
return null;
}
return users;
}
public boolean isSearchAliases()
{
return searchAliases;
}
public void setSearchAliases(boolean searchAliases)
{
this.searchAliases = searchAliases;
}
}