Browse Source

Move non security-specific LDAP classes to org.acegisecurity.ldap package

Luke Taylor 19 years ago
parent
commit
7f24e209a6

+ 285 - 0
core/src/main/java/org/acegisecurity/ldap/DefaultInitialDirContextFactory.java

@@ -0,0 +1,285 @@
+package org.acegisecurity.ldap;
+
+import org.acegisecurity.AcegiMessageSource;
+import org.acegisecurity.BadCredentialsException;
+import org.springframework.context.MessageSourceAware;
+import org.springframework.context.MessageSource;
+import org.springframework.context.support.MessageSourceAccessor;
+import org.springframework.util.Assert;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import javax.naming.directory.DirContext;
+import javax.naming.directory.InitialDirContext;
+import javax.naming.Context;
+import javax.naming.CommunicationException;
+import javax.naming.NamingException;
+import java.util.Map;
+import java.util.StringTokenizer;
+import java.util.Hashtable;
+
+/**
+ * Encapsulates the information for connecting to an LDAP server and provides an
+ * access point for obtaining <tt>DirContext</tt> references.
+ * <p>
+ * The directory location is configured using by setting the constructor argument
+ * <tt>providerUrl</tt>. This should be in the form
+ * <tt>ldap://monkeymachine.co.uk:389/dc=acegisecurity,dc=org</tt>. The Sun JNDI
+ * provider also supports lists of space-separated URLs, each of which will be tried
+ * in turn until a connection is obtained.
+ * </p>
+ * <p>
+ * To obtain an initial context, the client calls the <tt>newInitialDirContext</tt>
+ * method. There are two signatures - one with no arguments and one which allows
+ * binding with a specific username and password.
+ * </p>
+ * <p>
+ * The no-args version will bind anonymously unless a manager login has been configured
+ * using the properties <tt>managerDn</tt> and <tt>managerPassword</tt>, in which case
+ * it will bind as the manager user.
+ * </p>
+ * <p>
+ * Connection pooling is enabled by default for anonymous or manager connections, but
+ * not when binding as a specific user.
+ * </p>
+ *
+ * @see <a href="http://java.sun.com/products/jndi/tutorial/ldap/connect/pool.html">The Java
+ * tutorial's guide to LDAP connection pooling</a>
+ *
+ * @author Robert Sanders
+ * @author Luke Taylor
+ * @version $Id$
+ *
+ */
+public class DefaultInitialDirContextFactory implements InitialDirContextFactory,
+    MessageSourceAware {
+
+    //~ Static fields/initializers =============================================
+
+    private static final Log logger = LogFactory.getLog(org.acegisecurity.ldap.DefaultInitialDirContextFactory.class);
+
+    private static final String CONNECTION_POOL_KEY = "com.sun.jndi.ldap.connect.pool";
+
+    private static final String AUTH_TYPE_NONE = "none";
+
+    //~ Instance fields ========================================================
+
+    protected MessageSourceAccessor messages = AcegiMessageSource.getAccessor();
+
+    /**
+     * The LDAP url of the server (and root context) to connect to.
+     */
+    private String providerUrl;
+
+    /**
+     * The root DN. This is worked out from the url.
+     * It is used by client classes when forming a full DN for
+     * bind authentication (for example).
+     */
+    private String rootDn = null;
+
+    /**
+     * If your LDAP server does not allow anonymous searches then
+     * you will need to provide a "manager" user's DN to log in with.
+     */
+    private String managerDn = null;
+
+    /**
+     * The manager user's password.
+     */
+    private String managerPassword = "manager_password_not_set";
+
+    /** Type of authentication within LDAP; default is simple. */
+    private String authenticationType = "simple";
+
+    /**
+     * The INITIAL_CONTEXT_FACTORY used to create the JNDI Factory.
+     * Default is "com.sun.jndi.ldap.LdapCtxFactory"; you <b>should not</b>
+     * need to set this unless you have unusual needs.
+     */
+    private String initialContextFactory = "com.sun.jndi.ldap.LdapCtxFactory";
+
+    /** Allows extra environment variables to be added at config time. */
+    private Map extraEnvVars = null;
+
+    /**
+     * Use the LDAP Connection pool; if true, then the
+     * LDAP environment property "com.sun.jndi.ldap.connect.pool" is added
+     * to any other JNDI properties.
+     */
+    private boolean useConnectionPool = true;
+
+    //~ Constructors ===========================================================
+
+    public DefaultInitialDirContextFactory(String providerUrl) {
+        this.providerUrl = providerUrl;
+
+        Assert.hasLength(providerUrl, "An LDAP connection URL must be supplied.");
+
+        StringTokenizer st = new StringTokenizer(providerUrl);
+
+        // Work out rootDn from the first URL and check that the other URLs (if any) match
+        while(st.hasMoreTokens()) {
+            String url = st.nextToken();
+            String urlRootDn = LdapUtils.parseRootDnFromUrl(url);
+
+            org.acegisecurity.ldap.DefaultInitialDirContextFactory.logger.info(" URL '" + url +"', root DN is '" + urlRootDn + "'");
+
+            if(rootDn == null) {
+                rootDn = urlRootDn;
+            } else if (!rootDn.equals(urlRootDn)) {
+                throw new IllegalArgumentException("Root DNs must be the same when using multiple URLs");
+            }
+        }
+
+        // This doesn't necessarily hold for embedded servers.
+        //Assert.isTrue(uri.getScheme().equals("ldap"), "Ldap URL must start with 'ldap://'");
+    }
+
+
+    //~ Methods ================================================================
+
+    /**
+     * Connects anonymously unless a manager user has been specified, in which case
+     * it will bind as the manager.
+     *
+     * @return the resulting context object.
+     */
+    public DirContext newInitialDirContext() {
+
+        if (managerDn != null) {
+            return newInitialDirContext(managerDn, managerPassword);
+        }
+
+        Hashtable env = getEnvironment();
+        env.put(Context.SECURITY_AUTHENTICATION, org.acegisecurity.ldap.DefaultInitialDirContextFactory.AUTH_TYPE_NONE);
+
+        return connect(env);
+    }
+
+    public DirContext newInitialDirContext(String username, String password) {
+        Hashtable env = getEnvironment();
+
+        // Don't pool connections for individual users
+        if (!username.equals(managerDn)) {
+            env.remove(org.acegisecurity.ldap.DefaultInitialDirContextFactory.CONNECTION_POOL_KEY);
+        }
+
+        env.put(Context.SECURITY_PRINCIPAL, username);
+        env.put(Context.SECURITY_CREDENTIALS, password);
+
+        return connect(env);
+    }
+
+    /**
+     * @return the Hashtable describing the base DirContext that will be created,
+     * minus the username/password if any.
+     */
+    protected Hashtable getEnvironment() {
+        Hashtable env = new Hashtable();
+
+        env.put(Context.SECURITY_AUTHENTICATION, authenticationType);
+        env.put(Context.INITIAL_CONTEXT_FACTORY, initialContextFactory);
+        env.put(Context.PROVIDER_URL, providerUrl);
+
+        if (useConnectionPool) {
+            env.put(org.acegisecurity.ldap.DefaultInitialDirContextFactory.CONNECTION_POOL_KEY, "true");
+        }
+
+        if ((extraEnvVars != null) && (extraEnvVars.size() > 0)) {
+            env.putAll(extraEnvVars);
+        }
+
+        return env;
+    }
+
+    private InitialDirContext connect(Hashtable env) {
+
+        if (org.acegisecurity.ldap.DefaultInitialDirContextFactory.logger.isDebugEnabled()) {
+            Hashtable envClone = (Hashtable)env.clone();
+
+            if (envClone.containsKey(Context.SECURITY_CREDENTIALS)) {
+                envClone.put(Context.SECURITY_CREDENTIALS, "******");
+            }
+
+            org.acegisecurity.ldap.DefaultInitialDirContextFactory.logger.debug("Creating InitialDirContext with environment " + envClone);
+        }
+
+        try {
+            return new InitialDirContext(env);
+
+        } catch(CommunicationException ce) {
+            throw new LdapDataAccessException(messages.getMessage(
+                            "DefaultIntitalDirContextFactory.communicationFailure",
+                            "Unable to connect to LDAP server"), ce);
+        } catch(javax.naming.AuthenticationException ae) {
+            throw new BadCredentialsException(messages.getMessage(
+                            "DefaultIntitalDirContextFactory.badCredentials",
+                            "Bad credentials"), ae);
+        } catch (NamingException nx) {
+            throw new LdapDataAccessException(messages.getMessage(
+                            "DefaultIntitalDirContextFactory.unexpectedException",
+                            "Failed to obtain InitialDirContext due to unexpected exception"), nx);
+        }
+    }
+
+    /**
+     * Returns the root DN of the configured provider URL. For example,
+     * if the URL is <tt>ldap://monkeymachine.co.uk:389/dc=acegisecurity,dc=org</tt>
+     * the value will be <tt>dc=acegisecurity,dc=org</tt>.
+     *
+     * @return the root DN calculated from the path of the LDAP url.
+     */
+    public String getRootDn() {
+        return rootDn;
+    }
+
+    public void setAuthenticationType(String authenticationType) {
+        Assert.hasLength(authenticationType, "LDAP Authentication type must not be empty or null");
+        this.authenticationType = authenticationType;
+    }
+
+    public void setInitialContextFactory(String initialContextFactory) {
+        Assert.hasLength(initialContextFactory, "Initial context factory name cannot be empty or null");
+        this.initialContextFactory = initialContextFactory;
+    }
+
+    /**
+     * @param managerDn The name of the "manager" user for default authentication.
+     */
+    public void setManagerDn(String managerDn) {
+        Assert.hasLength(managerDn, "Manager user name  cannot be empty or null.");
+        this.managerDn = managerDn;
+    }
+
+    /**
+     * @param managerPassword The "manager" user's password.
+     */
+    public void setManagerPassword(String managerPassword) {
+        Assert.hasLength(managerPassword, "Manager password must not be empty or null.");
+        this.managerPassword = managerPassword;
+    }
+
+    /**
+     * @param extraEnvVars extra environment variables to be added at config time.
+     */
+    public void setExtraEnvVars(Map extraEnvVars) {
+        Assert.notNull(extraEnvVars, "Extra environment map cannot be null.");
+        this.extraEnvVars = extraEnvVars;
+    }
+
+    public void setMessageSource(MessageSource messageSource) {
+        this.messages = new MessageSourceAccessor(messageSource);
+    }
+
+    /**
+     * Connection pooling is enabled by default for anonymous or "manager"
+     * connections when using the default Sun provider. To disable all
+     * connection pooling, set this property to false.
+     *
+     * @param useConnectionPool whether to pool connections for non-specific users.
+     */
+    public void setUseConnectionPool(boolean useConnectionPool) {
+        this.useConnectionPool = useConnectionPool;
+    }
+}

+ 29 - 0
core/src/main/java/org/acegisecurity/ldap/InitialDirContextFactory.java

@@ -0,0 +1,29 @@
+package org.acegisecurity.ldap;
+
+import javax.naming.directory.DirContext;
+
+/**
+ * Access point for obtaining LDAP contexts.
+ *
+ * @see org.acegisecurity.ldap.DefaultInitialDirContextFactory
+ *
+ * @author Luke Taylor
+ * @version $Id$
+ */
+public interface InitialDirContextFactory {
+
+    /**
+     * Provides an initial context without specific user information.
+     */
+    DirContext newInitialDirContext();
+
+    /**
+     * Provides an initial context by binding as a specific user.
+     */
+    DirContext newInitialDirContext(String userDn, String password);
+
+    /**
+     * @return The DN of the contexts returned by this factory.
+     */
+    String getRootDn();
+}

+ 36 - 0
core/src/main/java/org/acegisecurity/ldap/LdapDataAccessException.java

@@ -0,0 +1,36 @@
+/* Copyright 2004, 2005 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.acegisecurity.ldap;
+
+import org.acegisecurity.AuthenticationServiceException;
+
+/**
+ * Used to wrap unexpected NamingExceptions while accessing the LDAP server
+ * or for other LDAP-related data problems such as data we can't handle.
+ *
+ * @author Luke Taylor
+ * @version $Id$
+ */
+public class LdapDataAccessException extends AuthenticationServiceException {
+
+    public LdapDataAccessException(String msg) {
+        super(msg);
+    }
+
+    public LdapDataAccessException(String msg, Throwable ex) {
+        super(msg, ex);
+    }
+}

+ 71 - 0
core/src/main/java/org/acegisecurity/ldap/LdapUserInfo.java

@@ -0,0 +1,71 @@
+/* Copyright 2004, 2005 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.acegisecurity.ldap;
+
+import javax.naming.directory.Attributes;
+import javax.naming.directory.DirContext;
+import javax.naming.NamingException;
+
+/**
+ * A user representation which is used internally by the Ldap provider.
+ *
+ * It contains the user's distinguished name and a set of attributes that
+ * have been retrieved from the Ldap server.
+ * <p>
+ * An instance may be created as the result of a search, or when user information
+ * is retrieved during authentication.
+ * </p>
+ * <p>
+ * An instance of this class will be used by the <tt>LdapAuthenticationProvider</tt>
+ * to construct the final user details object that it returns.
+ * </p>
+ *
+ * @author Luke Taylor
+ * @version $Id$
+ */
+public class LdapUserInfo {
+
+    //~ Instance fields ========================================================
+
+    private String dn;
+    private Attributes attributes;
+
+    //~ Constructors ===========================================================
+
+    /**
+     *
+     * @param dn the full DN of the user
+     * @param attributes any attributes loaded from the user's directory entry.
+     */
+    public LdapUserInfo(String dn, Attributes attributes) {
+        this.dn = dn;
+        this.attributes = attributes;
+    }
+
+    //~ Methods ================================================================
+
+    public String getDn() {
+        return dn;
+    }
+
+    public String getRelativeName(DirContext ctx) throws NamingException {
+        return LdapUtils.getRelativeName(dn, ctx);
+    }
+
+    public Attributes getAttributes() {
+        return (Attributes)attributes.clone();
+    }
+}

+ 41 - 0
core/src/main/java/org/acegisecurity/ldap/LdapUserSearch.java

@@ -0,0 +1,41 @@
+/* Copyright 2004, 2005 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.acegisecurity.ldap;
+
+/**
+ * Obtains a user's information from the LDAP directory given a login name.
+ * <p>
+ * May be optionally used to configure the LDAP authentication implementation when
+ * a more sophisticated approach is required than just using a simple username->DN
+ * mapping.
+ * </p>
+ *
+ * @author Luke Taylor
+ * @version $Id$
+ */
+public interface LdapUserSearch {
+
+    /**
+     * Locates a single user in the directory and returns the LDAP information
+     * for that user.
+     *
+     * @param username the login name supplied to the authentication service.
+     * @return an LdapUserInfo object containing the user's full DN and requested attributes.
+     * TODO: Need to optionally supply required attributes here for the search.
+     */
+    LdapUserInfo searchForUser(String username);
+
+}

+ 149 - 0
core/src/main/java/org/acegisecurity/ldap/LdapUtils.java

@@ -0,0 +1,149 @@
+/* Copyright 2004, 2005 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.acegisecurity.ldap;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.util.Assert;
+
+import javax.naming.Context;
+import javax.naming.NamingException;
+import java.io.UnsupportedEncodingException;
+import java.net.URI;
+import java.net.URISyntaxException;
+
+/**
+ * LDAP Utility methods.
+ *
+ * @author Luke Taylor
+ * @version $Id$
+ */
+public class LdapUtils {
+    //~ Static fields/initializers =============================================
+
+    private static final Log logger = LogFactory.getLog(LdapUtils.class);
+
+    //~ Methods ================================================================
+
+    public static void closeContext(Context ctx) {
+        try {
+            if (ctx != null) {
+                ctx.close();
+            }
+        } catch (NamingException e) {
+            logger.error("Failed to close context.", e);
+        }
+    }
+
+    /**
+     * Parses the supplied LDAP URL.
+     * @param url the URL (e.g. <tt>ldap://monkeymachine:11389/dc=acegisecurity,dc=org</tt>).
+     * @return the URI object created from the URL
+     * @throws IllegalArgumentException if the URL is null, empty or the URI syntax is invalid.
+     */
+    public static URI parseLdapUrl(String url) {
+        Assert.hasLength(url);
+
+        try {
+            return new URI(url);
+        } catch (URISyntaxException e) {
+            IllegalArgumentException iae = new IllegalArgumentException("Unable to parse url: " + url);
+            iae.initCause(e);
+            throw iae;
+        }
+    }
+
+    public static byte[] getUtf8Bytes(String s) {
+        try {
+            return s.getBytes("UTF-8");
+        } catch (UnsupportedEncodingException e) {
+            // Should be impossible since UTF-8 is required by all implementations
+            throw new IllegalStateException("Failed to convert string to UTF-8 bytes. Shouldn't be possible");
+        }
+    }
+
+    public static String escapeNameForFilter(String name) {
+        // TODO: Implement escaping as defined in RFC 2254
+        // Think this is probably not needed as filter args should be escaped automatically
+        // by the search methods.
+
+        return name;
+    }
+
+    /**
+     * Obtains the part of a DN relative to a supplied base context.
+     * <p>
+     * If the DN is "cn=bob,ou=people,dc=acegisecurity,dc=org" and the base context
+     * name is "ou=people,dc=acegisecurity,dc=org" it would return "cn=bob".
+     * </p>
+     *
+     * @param fullDn the DN
+     * @param baseCtx the context to work out the name relative to.
+     * @return the
+     * @throws NamingException any exceptions thrown by the context are propagated.
+     */
+    public static String getRelativeName(String fullDn, Context baseCtx) throws NamingException {
+        String baseDn = baseCtx.getNameInNamespace();
+
+        if (baseDn.length() == 0) {
+            return fullDn;
+        }
+
+        if (baseDn.equals(fullDn)) {
+            return "";
+        }
+
+        int index = fullDn.lastIndexOf(baseDn);
+
+        Assert.isTrue(index > 0, "Context base DN is not contained in the full DN");
+
+        // remove the base name and preceding comma.
+        return fullDn.substring(0, index - 1);
+    }
+
+    /**
+     * Works out the root DN for an LDAP URL.
+     * <p>
+     * For example, the URL <tt>ldap://monkeymachine:11389/dc=acegisecurity,dc=org</tt>
+     * has the root DN "dc=acegisecurity,dc=org".
+     *
+     *
+     * @param url the LDAP URL
+     * @return the root DN
+     */
+    public static String parseRootDnFromUrl(String url) {
+        Assert.hasLength(url);
+
+        String urlRootDn = null;
+
+        if (url.startsWith("ldap:") || url.startsWith("ldaps:")) {
+
+            URI uri = parseLdapUrl(url);
+
+            urlRootDn = uri.getPath();
+
+        } else {
+            // Assume it's an embedded server
+            urlRootDn = url;
+        }
+
+        if (urlRootDn.startsWith("/")) {
+            urlRootDn = urlRootDn.substring(1);
+        }
+
+        return urlRootDn;
+    }
+}

+ 181 - 0
core/src/main/java/org/acegisecurity/ldap/search/FilterBasedLdapUserSearch.java

@@ -0,0 +1,181 @@
+/* Copyright 2004, 2005 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.acegisecurity.ldap.search;
+
+import org.acegisecurity.userdetails.UsernameNotFoundException;
+import org.acegisecurity.BadCredentialsException;
+import org.acegisecurity.ldap.LdapUserSearch;
+import org.acegisecurity.ldap.LdapUtils;
+import org.acegisecurity.ldap.InitialDirContextFactory;
+import org.acegisecurity.ldap.LdapUserInfo;
+import org.acegisecurity.ldap.LdapDataAccessException;
+import org.springframework.util.Assert;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import javax.naming.directory.SearchControls;
+import javax.naming.directory.SearchResult;
+import javax.naming.directory.DirContext;
+import javax.naming.NamingException;
+import javax.naming.NamingEnumeration;
+
+/**
+ * LdapUserSearch implementation which uses an Ldap filter to locate the user.
+ *
+ * @author Robert Sanders
+ * @author Luke Taylor
+ * @version $Id$
+ */
+public class FilterBasedLdapUserSearch implements LdapUserSearch {
+    //~ Static fields/initializers =============================================
+
+    private static final Log logger = LogFactory.getLog(FilterBasedLdapUserSearch.class);
+
+    //~ Instance fields ========================================================
+
+    /**
+     * Context name to search in, relative to the root DN of the configured
+     * InitialDirContextFactory.
+     */
+    private String searchBase = "";
+
+    /**
+     * If true then searches the entire subtree as identified by context,
+     * if false (the default) then only searches the level identified by the context.
+     */
+//    private boolean searchSubtree = false;
+
+    private int searchScope = SearchControls.ONELEVEL_SCOPE;
+
+    /**
+     * The filter expression used in the user search. This is an LDAP
+     * search filter (as defined in 'RFC 2254') with optional arguments. See the documentation
+     * for the <tt>search</tt> methods in {@link javax.naming.directory.DirContext DirContext}
+     * for more information.
+     * <p>
+     * In this case, the username is the only parameter.
+     * </p>
+     * Possible examples are:
+     * <ul>
+     * <li>(uid={0}) - this would search for a username match on the uid attribute.</li>
+     * </ul>
+     * TODO: more examples.
+     *
+     */
+    private String searchFilter;
+
+    /**
+     * The time (in milliseconds) which to wait before the search fails;
+     * the default is zero, meaning forever.
+     */
+    private int searchTimeLimit = 0;
+
+    private InitialDirContextFactory initialDirContextFactory;
+
+    //~ Methods ================================================================
+
+    public FilterBasedLdapUserSearch(String searchBase,
+                                     String searchFilter,
+                                     InitialDirContextFactory initialDirContextFactory) {
+        Assert.notNull(initialDirContextFactory, "initialDirContextFactory must not be null");
+        Assert.notNull(searchFilter, "searchFilter must not be null.");
+        Assert.notNull(searchBase, "searchBase must not be null (an empty string is acceptable).");
+
+        this.searchFilter = searchFilter;
+        this.initialDirContextFactory = initialDirContextFactory;
+        this.searchBase = searchBase;
+
+        if(searchBase.length() == 0) {
+            logger.info("SearchBase not set. Searches will be performed from the root: " +
+                    initialDirContextFactory.getRootDn());
+        }
+    }
+
+    //~ Methods ================================================================
+
+    /**
+     * Return the LdapUserInfo containing the user's information, or null if
+     * no SearchResult is found.
+     *
+     * @param username the username to search for.
+     */
+    public LdapUserInfo searchForUser(String username) {
+        DirContext ctx = initialDirContextFactory.newInitialDirContext();
+        SearchControls ctls = new SearchControls();
+        ctls.setTimeLimit( searchTimeLimit );
+        ctls.setSearchScope( searchScope );
+
+        if (logger.isDebugEnabled()) {
+            logger.debug("Searching for user '" + username + "', in context " + ctx +
+                    ", with user search " + this.toString());
+        }
+
+        try {
+            String[] args = new String[] { LdapUtils.escapeNameForFilter(username) };
+
+            NamingEnumeration results = ctx.search(searchBase, searchFilter, args, ctls);
+
+            if (!results.hasMore()) {
+                throw new UsernameNotFoundException("User " + username + " not found in directory.");
+            }
+
+            SearchResult searchResult = (SearchResult)results.next();
+
+            if (results.hasMore()) {
+               throw new BadCredentialsException("Expected a single user but search returned multiple results");
+            }
+
+            StringBuffer userDn = new StringBuffer(searchResult.getName());
+
+            if (searchBase.length() > 0) {
+                userDn.append(",");
+                userDn.append(searchBase);
+            }
+
+            userDn.append(",");
+            userDn.append(ctx.getNameInNamespace());
+
+            return new LdapUserInfo(userDn.toString(), searchResult.getAttributes());
+
+        } catch(NamingException ne) {
+            throw new LdapDataAccessException("User Couldn't be found due to exception", ne);
+        } finally {
+            LdapUtils.closeContext(ctx);
+        }
+    }
+
+    public void setSearchSubtree(boolean searchSubtree) {
+//        this.searchSubtree = searchSubtree;
+        this.searchScope = searchSubtree ?
+                SearchControls.SUBTREE_SCOPE : SearchControls.ONELEVEL_SCOPE;
+    }
+
+    public void setSearchTimeLimit(int searchTimeLimit) {
+        this.searchTimeLimit = searchTimeLimit;
+    }
+
+    public String toString() {
+        StringBuffer sb = new StringBuffer();
+
+        sb.append("[ searchFilter: '").append(searchFilter).append("', ");
+        sb.append("searchBase: '").append(searchBase).append("'");
+        sb.append(", scope: ").append(searchScope ==
+                SearchControls.SUBTREE_SCOPE ? "subtree" : "single-level, ");
+        sb.append("searchTimeLimit: ").append(searchTimeLimit).append(" ]");
+
+        return sb.toString();
+    }
+}

+ 5 - 0
core/src/main/java/org/acegisecurity/ldap/search/package.html

@@ -0,0 +1,5 @@
+<html>
+<body>
+<tt>LdapUserSearch</tt> implementations. These may be used to locate the user in the directory.
+</body>
+</html>