org.sakaiproject.unboundid.UnboundidDirectoryProvider Maven / Gradle / Ivy
/**********************************************************************************
* $URL$
* $Id$
***********************************************************************************
*
* 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 org.sakaiproject.unboundid;
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 java.security.GeneralSecurityException;
import javax.net.ssl.SSLSocketFactory;
import lombok.extern.slf4j.Slf4j;
import lombok.Getter;
import lombok.Setter;
import org.apache.commons.lang3.StringUtils;
import org.sakaiproject.authz.api.SecurityService;
import org.sakaiproject.memory.api.MemoryService;
import org.sakaiproject.user.api.AuthenticationIdUDP;
import org.sakaiproject.user.api.DisplayAdvisorUDP;
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 com.unboundid.ldap.sdk.BindRequest;
import com.unboundid.ldap.sdk.BindResult;
import com.unboundid.ldap.sdk.DereferencePolicy;
import com.unboundid.ldap.sdk.GetEntryLDAPConnectionPoolHealthCheck;
import com.unboundid.ldap.sdk.LDAPConnectionOptions;
import com.unboundid.ldap.sdk.LDAPConnectionPool;
import com.unboundid.ldap.sdk.LDAPSearchException;
import com.unboundid.ldap.sdk.ResultCode;
import com.unboundid.ldap.sdk.SearchResult;
import com.unboundid.ldap.sdk.SearchResultEntry;
import com.unboundid.ldap.sdk.SearchScope;
import com.unboundid.ldap.sdk.ServerSet;
import com.unboundid.ldap.sdk.SimpleBindRequest;
import com.unboundid.ldap.sdk.SingleServerSet;
import com.unboundid.ldap.sdk.migrate.ldapjdk.LDAPConnection;
import com.unboundid.ldap.sdk.migrate.ldapjdk.LDAPEntry;
import com.unboundid.ldap.sdk.migrate.ldapjdk.LDAPException;
import com.unboundid.util.ssl.SSLUtil;
import org.sakaiproject.memory.api.Cache;
/**
*
* An implementation of a Sakai UserDirectoryProvider that authenticates/retrieves
* users from a LDAP directory. Forked from JLDAP in early 2016.
*
*
*/
@Slf4j
public class UnboundidDirectoryProvider implements UserDirectoryProvider, LdapConnectionManagerConfig, ExternalUserSearchUDP, UsersShareEmailUDP, DisplayAdvisorUDP, AuthenticationIdUDP
{
/** Security Service */
@Setter private SecurityService securityService;
/** Memory Service */
@Setter private MemoryService memoryService;
/** 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 = 9000;
/** 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 SearchScope DEFAULT_SEARCH_SCOPE = SearchScope.SUB;
/** Default LDAP maximum number of connections in the pool */
public static final int DEFAULT_POOL_MAX_CONNS = 10;
public static final boolean DEFAULT_RETRY_FAILED_OPERATIONS_DUE_TO_INVALID_CONNECTIONS = false;
public static final long DEFAULT_HEALTH_CHECK_INTERVAL_MILLIS = 180000L;
/** 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;
/** Property of the user object to store the display ID under */
public static final String DISPLAY_ID_PROPERTY = UnboundidDirectoryProvider.class+"-displayId";
/** Property of the user object to store the display Name under */
public static final String DISPLAY_NAME_PROPERTY = UnboundidDirectoryProvider.class+"-displayName";
public static final boolean DEFAULT_ALLOW_AUTHENTICATION = true;
public static final boolean DEFAULT_ALLOW_AUTHENTICATION_EXTERNAL = true;
public static final boolean DEFAULT_ALLOW_AUTHENTICATION_ADMIN = false;
public static final boolean DEFAULT_ALLOW_SEARCH_EXTERNAL = true;
public static final boolean DEFAULT_ALLOW_GET_EXTERNAL = true;
public static final boolean DEFAULT_AUTHENTICATE_WITH_PROVIDER_FIRST = false;
/** LDAP host address */
private String[] ldapHost;
/** LDAP connection port. Defaults to {@link #DEFAULT_LDAP_PORT} */
private int[] ldapPort = DEFAULT_LDAP_PORT;
/** 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;
/** Maximum number of physical connections in the pool */
private int poolMaxConns = DEFAULT_POOL_MAX_CONNS;
private boolean retryFailedOperationsDueToInvalidConnections = DEFAULT_RETRY_FAILED_OPERATIONS_DUE_TO_INVALID_CONNECTIONS;
private long healthCheckIntervalMillis = DEFAULT_HEALTH_CHECK_INTERVAL_MILLIS;
private Map healthCheckMappings = null;
/** 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;
/** 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
* LDAPConstraints.setTimeLimit(int).
*/
private int operationTimeout = DEFAULT_OPERATION_TIMEOUT_MILLIS;
private SearchScope searchScope = DEFAULT_SEARCH_SCOPE;
/** Should the provider support searching by Authentication ID */
private boolean enableAid = false;
/**
* User entry attribute mappings. Keys are logical attr names,
* values are physical attr names.
*
* @see LdapAttributeMapper
*/
private Map attributeMappings;
/** Handles LDAPConnection allocation */
private LDAPConnectionPool connectionPool;
/** 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 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);
return cacheRecord;
}
};
/**
* Flag for allowing/disallowing authentication on a global basis
*/
private boolean allowAuthentication = DEFAULT_ALLOW_AUTHENTICATION;
/**
* Flag for allowing/disallowing authentication for external users (who do not already exist).
* If false, only users who have existing accounts may authenticate via LDAP.
*/
@Getter @Setter private boolean allowAuthenticationExternal = DEFAULT_ALLOW_AUTHENTICATION_EXTERNAL;
/**
* Flag for allowing/disallowing authentication for admin-equivalent users.
* If false, users who have admin-equivalent accounts may not authenticate via LDAP.
*/
@Getter @Setter private boolean allowAuthenticationAdmin = DEFAULT_ALLOW_AUTHENTICATION_ADMIN;
/**
* Flag for allowing/disallowing searching external users
*/
@Getter @Setter private boolean allowSearchExternal = DEFAULT_ALLOW_SEARCH_EXTERNAL;
/**
* Flag for allowing/disallowing getting an external user
*/
@Getter @Setter private boolean allowGetExternal = DEFAULT_ALLOW_GET_EXTERNAL;
/**
* Flag for controlling the return value of
* {@link #authenticateWithProviderFirst(String)} on a global basis.
*/
private boolean authenticateWithProviderFirst = DEFAULT_AUTHENTICATE_WITH_PROVIDER_FIRST;
/** Negative cache */
private Cache negativeCache;
public UnboundidDirectoryProvider() {
log.debug("instantating UnboundidDirectoryProvider");
}
/**
* Typically invoked by Spring to complete bean initialization.
* Ensures initialization of delegate {@link LdapConnectionManager}
* and {@link LdapAttributeMapper}
*
* @see #initLdapConnectionManager()
* @see #initLdapAttributeMapper()
*/
public void init()
{
log.debug("init()");
// 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;
log.warn("Unboundid batchSize is larger than maxResultSize, batchSize has been reduced from: "+ batchSize + " to: "+ maxResultSize);
}
// setup the negative user cache
negativeCache = memoryService.getCache(getClass().getName() + ".negativeCache");
createConnectionPool();
initLdapAttributeMapper();
}
/**
* Create the LDAP connection pool
*/
protected synchronized boolean createConnectionPool() {
if (connectionPool != null) {
return true;
}
// Create a new LDAP connection pool with 10 connections
ServerSet serverSet = null;
// Set some sane defaults to better handle timeouts. Unboundid will wait 30 seconds by default on a hung connection.
LDAPConnectionOptions connectOptions = new LDAPConnectionOptions();
connectOptions.setAbandonOnTimeout(false); // If no response from server, dont send an abandon request to the server
connectOptions.setConnectTimeoutMillis(operationTimeout);
connectOptions.setResponseTimeoutMillis(operationTimeout); // Sakai should not be making any giant queries to LDAP
connectOptions.setUseSynchronousMode(true); // "operate more efficiently and without requiring a separate reader thread per connection"
if (isSecureConnection()) {
try {
// If testing locally only, could use `new TrustAllTrustManager()` as contructor parameter to SSLUtil
SSLUtil sslUtil = new SSLUtil();
SSLSocketFactory sslSocketFactory = sslUtil.createSSLSocketFactory();
serverSet = new SingleServerSet(ldapHost[0], ldapPort[0], sslSocketFactory, connectOptions);
} catch (GeneralSecurityException ex) {
log.error("Error while initializing LDAP SSLSocketFactory");
throw new RuntimeException(ex);
}
} else {
serverSet = new SingleServerSet(ldapHost[0], ldapPort[0], connectOptions);
}
BindRequest bindRequest = new SimpleBindRequest(ldapUser, ldapPassword);
try {
log.info("Creating LDAP connection pool of size {}", poolMaxConns);
connectionPool = new LDAPConnectionPool(serverSet, bindRequest, poolMaxConns);
connectionPool.setRetryFailedOperationsDueToInvalidConnections(retryFailedOperationsDueToInvalidConnections);
connectionPool.setHealthCheckIntervalMillis(healthCheckIntervalMillis);
if (healthCheckMappings != null) {
GetEntryLDAPConnectionPoolHealthCheck healthCheck = new GetEntryLDAPConnectionPoolHealthCheck(
ldapUser,
Long.parseLong(healthCheckMappings.get("maxResponseTime")),
Boolean.parseBoolean(healthCheckMappings.get("invokeOnCreate")),
Boolean.parseBoolean(healthCheckMappings.get("invokeAfterAuthentication")),
Boolean.parseBoolean(healthCheckMappings.get("invokeOnCheckout")),
Boolean.parseBoolean(healthCheckMappings.get("invokeOnRelease")),
Boolean.parseBoolean(healthCheckMappings.get("invokeForBackgroundChecks")),
Boolean.parseBoolean(healthCheckMappings.get("invokeOnException")));
connectionPool.setHealthCheck(healthCheck);
}
} catch (com.unboundid.ldap.sdk.LDAPException e) {
log.error("Could not init LDAP pool", e);
return false;
}
return true;
}
/**
* Lazily "injects" a {@link LdapAttributeMapper} if one
* has not been assigned already.
*
*
* 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.
*
*/
protected void initLdapAttributeMapper() {
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 LdapAttributeMapper} instances.
* Ensures forward compatibility of existing config which
* does not specify a delegate {@link LdapAttributeMapper}.
*
* @return a new {@link LdapAttributeMapper}
*/
protected LdapAttributeMapper newDefaultLdapAttributeMapper() {
log.debug("newDefaultLdapAttributeMapper(): returning a new SimpleLdapAttributeMapper");
return new SimpleLdapAttributeMapper();
}
/**
* Typically called by Spring to signal bean destruction.
*
*/
public void destroy() {
log.debug("destroy()");
clearCache();
}
/**
* Resets the internal {@link LdapUserData} cache
*/
public void clearCache() {
log.debug("clearCache()");
negativeCache.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.
*
* 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)
{
com.unboundid.ldap.sdk.LDAPConnection lc = null;
log.debug("authenticateUser(): [userLogin = {}]", userLogin);
if ( !(allowAuthentication) ) {
log.debug("authenticateUser(): denying authentication attempt [userLogin = " + userLogin + "]. All authentication has been disabled via configuration");
return false;
}
if ( StringUtils.isBlank(password) ) {
log.debug("authenticateUser(): returning false, blank password");
return false;
}
if ( !allowAuthenticationExternal && (edit.getId() == null)) {
log.debug("authenticateUser(): returning false, not authenticating for external users");
return false;
}
if ( !allowAuthenticationAdmin && securityService.isSuperUser(edit.getId())) {
log.debug("authenticateUser(): returning false, not authenticating for superuser (admin) {}", edit.getEid());
return false;
}
if (connectionPool == null && !createConnectionPool()) {
log.error("No LDAP connection pool available: unable to authenticate");
return false;
}
try
{
long start = System.currentTimeMillis();
// 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);
if ( endUserDN == null ) {
log.debug("authenticateUser(): failed to find bind dn for login [userLogin = {}], returning false", userLogin);
return false;
}
log.debug("authenticateUser(): attempting to allocate bound connection [userLogin = {}][bind dn [{}]", userLogin, endUserDN);
lc = connectionPool.getConnection();
BindResult bindResult = lc.bind(endUserDN, password);
if(bindResult.getResultCode().equals(ResultCode.SUCCESS)) {
log.info("Authenticated {} ({}) from LDAP in {} ms", userLogin, endUserDN, System.currentTimeMillis() - start);
return true;
}
log.debug("authenticateUser(): unsuccessfull bind attempt [userLogin = {}][bind dn [{}]", userLogin, endUserDN);
return false;
}
catch (com.unboundid.ldap.sdk.LDAPException e)
{
if (e.getResultCode().intValue() == LDAPException.INVALID_CREDENTIALS) {
log.info("authenticateUser(): invalid credentials [userLogin = {}]", userLogin);
return false;
} else {
throw new RuntimeException(
"authenticateUser(): LDAPException during authentication attempt [userLogin = "
+ userLogin + "][result code = " + e.getResultCode().toString() +
"][error message = "+ e.getExceptionMessage() + "]", e);
}
} catch ( Exception e ) {
throw new RuntimeException(
"authenticateUser(): Exception during authentication attempt [userLogin = "
+ userLogin + "]", e);
} finally {
// We don't want this user-bound LDAP connection returned to the pool
connectionPool.releaseDefunctConnection(lc);
}
}
/**
* 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 null
.
*
*
* All {@link java.lang.Exception}s are logged and result in
* a false
return, as do searches which yield
* no results. (A concession to backward compat.)
*
*
* @param edit the {@link org.sakaiproject.user.api.UserEdit} to update
* @param email the search key
* @return boolean true
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);
} catch ( InvalidEmailAddressException e ) {
log.error("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);
}
if ( resolvedEntry == null ) {
log.debug("findUserByEmail(): failed to find user by email [email = {}]", email);
return false;
}
log.debug("findUserByEmail(): found user by email [email = {}]", email);
if ( edit != null ) {
mapUserDataOntoUserEdit(resolvedEntry, edit);
}
return true;
} catch ( Exception e ) {
log.error("findUserByEmail(): failed [email = " + email + "]");
log.debug("Exception: ", e);
return false;
}
}
/**
* Effectively the same as
* getUserByEid(edit, edit.getEid())
.
*
* @see #getUserByEid(UserEdit, String)
*/
public boolean getUser(UserEdit edit)
{
if (!allowGetExternal) {
log.debug("getUser() external get not enabled");
return false;
}
try {
boolean userFound = getUserByEid(edit, edit.getEid());
// No LDAPException means we have a good connection. Cache a negative result.
if (!userFound) {
Object o = negativeCache.get(edit.getEid());
Integer seenCount = 0;
if (o != null) {
seenCount = (Integer) o;
}
negativeCache.put(edit.getEid(), (seenCount + 1));
}
return userFound;
} catch ( LDAPException e ) {
log.error("getUser() failed [eid: " + edit.getEid() + "]", e);
return false;
}
}
public boolean getUserbyAid(String aid, UserEdit user)
{
// Only do search if we're enabled.
if (!(enableAid)) {
return false;
}
LdapUserData foundUserData = getUserByAid(aid);
if ( foundUserData == null ) {
return false;
}
if ( user != null ) {
mapUserDataOntoUserEdit(foundUserData, user);
}
return true;
}
public LdapUserData getUserByAid(String aid) {
String filter = ldapAttributeMapper.getFindUserByAidFilter(aid);
LdapUserData mappedEntry = null;
try {
mappedEntry = (LdapUserData) searchDirectoryForSingleEntry(filter,
null, null, null);
} catch (LDAPException e) {
log.error("Failed to find user for AID: " + aid, e);
}
return mappedEntry;
}
/**
* Similar to iterating over users
passing
* each element to {@link #getUser(UserEdit)}, removing the
* {@link org.sakaiproject.user.api.UserEdit} if that method
* returns false
.
*
* Adds search retry capability if any one lookup fails
* with a directory error. Empties users
and
* returns if a retry exits exceptionally
*
*/
public void getUsers(Collection users)
{
log.debug("getUsers(): [Collection size = {}]", users.size());
boolean abortiveSearch = false;
int maxQuerySize = getMaxObjectsToQueryFor();
UserEdit userEdit = null;
HashMap usersToSearchInLDAP = new HashMap();
List usersToRemove = new ArrayList();
try {
int cnt = 0;
for ( Iterator 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 {
usersToSearchInLDAP.put(eid, userEdit);
cnt++;
}
// We need to make sure this query isn't larger than maxQuerySize
if ((!userEdits.hasNext() || cnt == maxQuerySize) && !usersToSearchInLDAP.isEmpty()) {
String filter = ldapAttributeMapper.getManyUsersInOneSearch(usersToSearchInLDAP.keySet());
List ldapUsers = searchDirectory(filter, null, null, null, maxQuerySize);
for (LdapUserData ldapUserData : ldapUsers) {
String ldapEid = ldapUserData.getEid();
if (StringUtils.isEmpty(ldapEid)) {
continue;
}
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 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) {
log.debug("Unboundid getUsers could not find user: {}", userRemove.getEid());
users.remove(userRemove);
// Add eid to negative cache. We are confident the LDAP conn is alive and well here.
Integer seenCount = 0;
Object o = negativeCache.get(userRemove.getEid());
if (o != null) {
seenCount = (Integer) o;
}
negativeCache.put(userRemove.getEid(), (seenCount + 1));
}
} catch (LDAPException e) {
abortiveSearch = true;
throw new RuntimeException("getUsers(): LDAPException during search [eid = " +
(userEdit == null ? null : userEdit.getEid()) +
"][result code = " + e.errorCodeToString() +
"][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 {
// no sense in returning a partially complete search result
if ( abortiveSearch ) {
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 getUserByEid(null,eid)
.
*
* @see #getUserByEid(UserEdit, String)
*/
public boolean userExists(String eid)
{
log.debug("userExists(): [eid = {}]", eid);
try {
return getUserByEid(null, eid);
} catch ( LDAPException e ) {
log.error("userExists() failed: [eid = " + eid + "]", e);
return false;
}
}
/**
* Finds a user record using an eid
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 null
* @param eid the user ID
* @param conn a LDAPConnection
to reuse. may be null
* @return true
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)
throws LDAPException {
LdapUserData foundUserData = getUserByEid(eid);
if ( foundUserData == null ) {
return false;
}
if ( userToUpdate != null ) {
mapUserDataOntoUserEdit(foundUserData, userToUpdate);
}
return true;
}
/**
* Finds a user record using an eid
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)
throws LDAPException {
if ( !(isSearchableEid(eid)) ) {
if (eid == null)
{
log.debug("User EID not searchable (eid is null)");
return null;
}
log.debug("User EID not searchable (possibly blacklisted or otherwise syntactically invalid) [{}]", eid);
return null;
}
log.debug("getUserByEid(): [eid = {}]", eid);
String filter = ldapAttributeMapper.getFindUserByEidFilter(eid);
// takes care of caching and everything
return (LdapUserData)searchDirectoryForSingleEntry(filter,
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 null
or otherwise "empty"
* @return true
if no {@link EidValidator} has been
* set, or the result of {@link EidValidator#isSearchableEid(String)}
*/
protected boolean isSearchableEid(String eid) {
if (negativeCache == null) {
negativeCache = memoryService.getCache(getClass().getName() + ".negativeCache");
log.debug("negativeCache initialized in isSearchableEid");
}
Object o = negativeCache.get(eid);
if (o != null) {
Integer seenCount = (Integer) o;
log.debug("negativeCache count for {}={}", eid, seenCount);
if (seenCount > 3) {
return false;
}
}
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)
throws LDAPException {
log.debug("lookupUserEntryDN(): [eid = {}]", eid);
LdapUserData foundUserData;
if (enableAid) {
foundUserData = getUserByAid(eid);
} else {
foundUserData = getUserByEid(eid);
}
if ( foundUserData == null ) {
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 LDAPEntry
or null
if no match
* @throws LDAPException if the search exits with an error
*/
protected Object searchDirectoryForSingleEntry(String filter,
LdapEntryMapper mapper,
String[] searchResultPhysicalAttributeNames,
String searchBaseDn)
throws LDAPException {
log.debug("searchDirectoryForSingleEntry(): [filter = {}]", filter);
List results = searchDirectory(filter,
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 null
* @param searchResultPhysicalAttributeNames attributes to retrieve.
* May be null
, in which case defaults to
* {@link LdapAttributeMapper#getSearchResultAttributes()}.
* @param searchBaseDn base DN from which to begin search.
* May be null
, in which case defaults to assigned
* basePath
* @param maxResults maximum number of retrieved LDAP objects. Ignored
* if <= 0
* @return An empty {@link List} if no results. Will not return null
* @throws LDAPException if thrown by the search
* @throws RuntimeExction wrapping any non-{@link LDAPException} {@link Exception}
*/
protected List searchDirectory(final String filter,
final LdapEntryMapper passedMapper,
final String[] searchResultPhysicalAttributeNames,
final String unescapedSearchBaseDn,
final int maxResults)
throws LDAPException {
log.debug("searchDirectory(): [filter = {}]", filter);
if (connectionPool == null && !createConnectionPool()) {
throw new LDAPException("No LDAP connection pool available: unable to search");
}
try {
final String[] scrubbedPhysicalAttributeNames = scrubSearchResultPhysicalAttributeNames(searchResultPhysicalAttributeNames);
final String searchBaseDn = scrubSearchBaseDn(unescapedSearchBaseDn);
LdapEntryMapper mapper = defaultLdapEntryMapper;
if ( passedMapper != null ) {
mapper = passedMapper;
}
DereferencePolicy dr = DereferencePolicy.NEVER;
if (isSearchAliases()) {
dr = DereferencePolicy.ALWAYS;
}
log.debug("searchDirectory(): [baseDN = {}][filter = {}][return attribs = {}][max results = {}][search scope = {}]",
searchBaseDn, filter, Arrays.toString(scrubbedPhysicalAttributeNames), maxResults, searchScope);
long start = System.currentTimeMillis();
SearchResult searchResult = null;
try {
searchResult = connectionPool.search(searchBaseDn,
searchScope,
dr,
maxResults,
operationTimeout,
false,
filter,
scrubbedPhysicalAttributeNames
);
} catch (LDAPSearchException e) {
if (e.getResultCode().equals(ResultCode.SIZE_LIMIT_EXCEEDED)) {
// We still want results even
// though we hit the max. Just take what we
// were able to get.
searchResult = e.getSearchResult();
log.warn("Hit ResultCode.SIZE_LIMIT_EXCEEDED: {}", e.getDiagnosticMessage());
} else {
throw e;
}
}
List searchResults = searchResult.getSearchEntries();
List mappedResults = new ArrayList();
int resultCnt = 0;
for (SearchResultEntry sre : searchResults) {
LDAPEntry entry = new LDAPEntry(sre);
Object mappedResult = mapper.mapLdapEntry(entry, ++resultCnt);
if ( mappedResult == null ) {
continue;
}
mappedResults.add((LdapUserData) mappedResult);
}
log.debug("Query took: {}ms", (System.currentTimeMillis() - start));
return mappedResults;
} catch ( Exception e ) {
throw new RuntimeException("searchDirectory(): RuntimeException while executing search [baseDN = " +
unescapedSearchBaseDn + "][filter = " + filter +
"][return attribs = " +
Arrays.toString(searchResultPhysicalAttributeNames) +
"][max results = " + maxResults + "]", e);
}
}
/**
* Responsible for pre-processing base DNs passed to
* {@link #searchDirectory(String, LDAPConnection, String[], String, int)}.
* As implemented, simply checks for a null
reference,
* in which case it returns the currently cached "basePath". Otherwise
* returns the received String
as is.
*
* @see #setBasePath(String)
* @param searchBaseDn a proposed base DN. May be null
* @return a default base DN or the received DN, if non null
. Return
* value may be null
if no default base DN has been configured
*/
protected String scrubSearchBaseDn(final String searchBaseDn) {
return searchBaseDn == null ? basePath : searchBaseDn;
}
/**
* Responsible for pre-processing search result attribute names
* passed to
* {@link #searchDirectory(String, LDAPConnection, String[], String, int)}.
* If the given String[]>
is null
,
* will use {@link LdapAttributeMapper#getSearchResultAttributes()}.
* If that method returns null
will return an empty
* String[]>
. Otherwise returns the received String[]>
* as-is.
*
* @param searchResultPhysicalAttributeNames
* @return
*/
protected String[] scrubSearchResultPhysicalAttributeNames(final String[] searchResultPhysicalAttributeNames) {
String[] scrubbedNames = searchResultPhysicalAttributeNames;
if ( scrubbedNames == null ) {
scrubbedNames = ldapAttributeMapper.getSearchResultAttributes();
}
if ( scrubbedNames == null ) {
scrubbedNames = new String[0];
}
return scrubbedNames;
}
/**
* Maps attributes from the specified LDAPEntry
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) {
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) {
// std. UserEdit impl has no meaningful toString() impl
log.debug("mapUserDataOntoUserEdit() [userData = {}]", userData);
// delegate to the LdapAttributeMapper since it knows the most
// about how the LdapUserData instance was originally populated
ldapAttributeMapper.mapUserDataOntoUserEdit(userData, userEdit);
userEdit.setEid(StringUtils.lowerCase(userData.getEid()));
}
/**
* {@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;
}
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 getAttributeMappings()
{
return attributeMappings;
}
/**
* @param attributeMappings LDAP attribute map, keys are logical names,
* values are physical names. may be null
*/
public void setAttributeMappings(Map 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 int getPoolMaxConns() {
return poolMaxConns;
}
/**
* {@inheritDoc}
*/
public void setPoolMaxConns(int poolMaxConns) {
this.poolMaxConns = poolMaxConns;
}
/**
* {@inheritDoc}
*/
public boolean getRetryFailedOperationsDueToInvalidConnections() {
return retryFailedOperationsDueToInvalidConnections;
}
/**
* {@inheritDoc}
*/
public void setRetryFailedOperationsDueToInvalidConnections(boolean retryFailedOperationsDueToInvalidConnections) {
this.retryFailedOperationsDueToInvalidConnections = retryFailedOperationsDueToInvalidConnections;
}
public long getHealthCheckIntervalMillis() {
return healthCheckIntervalMillis;
}
public void setHealthCheckIntervalMillis(long healthCheckIntervalMillis) {
this.healthCheckIntervalMillis = healthCheckIntervalMillis;
}
public Map getHealthCheckMappings()
{
return healthCheckMappings;
}
public void setHealthCheckMappings(Map healthCheckMappings)
{
this.healthCheckMappings = healthCheckMappings;
}
/**
* {@inheritDoc}
*/
public int getMaxObjectsToQueryFor() {
return getBatchSize();
}
/**
* {@inheritDoc}
*/
public void setMaxObjectsToQueryFor (int maxObjectsToQueryFor) {
log.info("maxObjectToQueryFor is deprecated please use " + "[email protected] instead");
setBatchSize(maxObjectsToQueryFor);
}
/**
* {@inheritDoc}
*/
public int getBatchSize() {
return batchSize;
}
/**
* {@inheritDoc}
*/
public void setBatchSize(int batchSize) {
this.batchSize = batchSize;
}
/**
* {@inheritDoc}
*/
public void setEnableAid(boolean enableAid) {
this.enableAid = enableAid;
}
/**
* {@inheritDoc}
*/
public int getMaxResultSize() {
return maxResultSize;
}
/**
* {@inheritDoc}
*/
public void setMaxResultSize(int maxResultSize) {
this.maxResultSize = maxResultSize;
}
/**
* 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;
}
/**
* Access the service used to verify EIDs prior to executing
* searches on those values.
*
* @see #isSearchableEid(String)
* @return an {@link EidValidator} or null
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 null
* indicating that all EIDs are searchable.
*
* @param eidValidator an {@link EidValidator} or null
* 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.
* false
completely disables
* {@link #authenticateUser(String, UserEdit, String)} (regardless of
* the value returned from
* {@link #authenticateWithProviderFirst(String)}). true
* 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.
*
* Defaults to {@link #DEFAULT_ALLOW_AUTHENTICATION}
*
* @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;
}
public String getDisplayId(User user) {
String displayId = user.getProperties().getProperty(DISPLAY_ID_PROPERTY);
if (displayId != null && displayId.length() > 0) {
return displayId;
}
return null;
}
public String getDisplayName(User user) {
String displayName = user.getProperties().getProperty(DISPLAY_NAME_PROPERTY);
if (displayName != null && displayName.length() > 0) {
return displayName;
}
return null;
}
/**
* 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 SearchScope 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 :
this.searchScope = SearchScope.BASE;
return;
case LDAPConnection.SCOPE_ONE :
this.searchScope = SearchScope.ONE;
return;
case LDAPConnection.SCOPE_SUB :
this.searchScope = SearchScope.SUB;
return;
default :
throw new IllegalArgumentException("Invalid search scope [" + searchScope +"]");
}
}
/**
* 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 searchUsers(String criteria, int first, int last) {
// log.error("Not yet implemented");
// return null;
//}
/**
* Search for externally provided users that match this criteria in eid, email, first or last name.
*
* Returns a List of UserEdit objects. This list will be empty if no results are returned or null
* if your external provider does not implement this interface.
*
* 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.
*
* See LdapAttributeMapper.getFindUserByCrossAttributeSearchFilter for the filter used.
*
* @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 searchExternalUsers(String criteria, int first, int last, UserFactory factory) {
if (!allowSearchExternal) {
log.debug("External search is disabled");
return null;
}
String filter = ldapAttributeMapper.getFindUserByCrossAttributeSearchFilter(criteria);
List users = new ArrayList();
try {
//no limit to the number of search results, use the LDAP server's settings.
List ldapUsers = searchDirectory(filter, 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) {
log.warn("An error occurred searching for users: " + e.getClass().getName() + ": (" + e.getLDAPResultCode() + ") " + 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) {
List users = new ArrayList();
if (!allowSearchExternal) {
log.debug("External search is disabled");
return users;
}
String filter = ldapAttributeMapper.getFindUserByEmailFilter(email);
try {
List ldapUsers = searchDirectory(filter, 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) {
log.warn("An error occurred finding users by email: " + e.getClass().getName() + ": (" + e.getLDAPResultCode() + ") " + e.getMessage());
return null;
}
return users;
}
public boolean isSearchAliases()
{
return searchAliases;
}
public void setSearchAliases(boolean searchAliases)
{
this.searchAliases = searchAliases;
}
}