ソースを参照

Initial LDAP provider checkin.

Luke Taylor 20 年 前
コミット
ce3d6f2129
27 ファイル変更2585 行追加0 行削除
  1. 249 0
      core/src/main/java/org/acegisecurity/providers/ldap/DefaultInitialDirContextFactory.java
  2. 45 0
      core/src/main/java/org/acegisecurity/providers/ldap/InitialDirContextFactory.java
  3. 138 0
      core/src/main/java/org/acegisecurity/providers/ldap/LdapAuthenticationProvider.java
  4. 39 0
      core/src/main/java/org/acegisecurity/providers/ldap/LdapAuthenticator.java
  5. 43 0
      core/src/main/java/org/acegisecurity/providers/ldap/LdapAuthoritiesPopulator.java
  6. 31 0
      core/src/main/java/org/acegisecurity/providers/ldap/LdapDataAccessException.java
  7. 75 0
      core/src/main/java/org/acegisecurity/providers/ldap/LdapUserDetails.java
  8. 96 0
      core/src/main/java/org/acegisecurity/providers/ldap/LdapUtils.java
  9. 118 0
      core/src/main/java/org/acegisecurity/providers/ldap/authenticator/AbstractLdapAuthenticator.java
  10. 97 0
      core/src/main/java/org/acegisecurity/providers/ldap/authenticator/BindAuthenticator.java
  11. 163 0
      core/src/main/java/org/acegisecurity/providers/ldap/authenticator/FilterBasedLdapUserSearch.java
  12. 36 0
      core/src/main/java/org/acegisecurity/providers/ldap/authenticator/LdapShaPasswordEncoder.java
  13. 43 0
      core/src/main/java/org/acegisecurity/providers/ldap/authenticator/LdapUserSearch.java
  14. 165 0
      core/src/main/java/org/acegisecurity/providers/ldap/authenticator/PasswordComparisonAuthenticator.java
  15. 285 0
      core/src/main/java/org/acegisecurity/providers/ldap/populator/DefaultLdapAuthoritiesPopulator.java
  16. 25 0
      core/src/test/java/org/acegisecurity/providers/ldap/AbstractLdapServerTestCase.java
  17. 140 0
      core/src/test/java/org/acegisecurity/providers/ldap/InitialDirContextFactoryTests.java
  18. 102 0
      core/src/test/java/org/acegisecurity/providers/ldap/LdapAuthenticationProviderTests.java
  19. 94 0
      core/src/test/java/org/acegisecurity/providers/ldap/LdapTestServer.java
  20. 73 0
      core/src/test/java/org/acegisecurity/providers/ldap/authenticator/BindAuthenticatorTests.java
  21. 87 0
      core/src/test/java/org/acegisecurity/providers/ldap/authenticator/FilterBasedLdapUserSearchTests.java
  22. 19 0
      core/src/test/java/org/acegisecurity/providers/ldap/authenticator/MockUserSearch.java
  23. 58 0
      core/src/test/java/org/acegisecurity/providers/ldap/authenticator/PasswordComparisonAuthenticatorMockTests.java
  24. 133 0
      core/src/test/java/org/acegisecurity/providers/ldap/authenticator/PasswordComparisonAuthenticatorTests.java
  25. 76 0
      core/src/test/java/org/acegisecurity/providers/ldap/populator/DefaultLdapAuthoritiesPopulatorTests.java
  26. 101 0
      core/src/test/resources/org/acegisecurity/providers/ldap/apacheds-context.xml
  27. 54 0
      core/src/test/resources/org/acegisecurity/providers/ldap/ldif/acegisecurity.ldif

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

@@ -0,0 +1,249 @@
+/* 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.providers.ldap;
+
+import java.util.Hashtable;
+import java.util.Map;
+import java.net.URI;
+import javax.naming.Context;
+import javax.naming.NamingException;
+import javax.naming.CommunicationException;
+import javax.naming.directory.InitialDirContext;
+import javax.naming.directory.DirContext;
+
+import org.springframework.dao.DataAccessResourceFailureException;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.util.Assert;
+import org.acegisecurity.BadCredentialsException;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * 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 <tt>url</tt> property.
+ * This should be in the form <tt>ldap://monkeymachine.co.uk:389/dc=acegisecurity,dc=org</tt>.
+ * </p>
+ * <p>
+ * To obtain an initial context, th 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 or if a manager login has been configured
+ * using the properties <tt>managerDn</tt> and <tt>managerPassword</tt> it will bind as
+ * that user.
+ * </p>
+ * <p>
+ * Connection pooling is enabled 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
+ * Connection Pooling</a>
+ *
+ * @author Robert Sanders
+ * @author Luke Taylor
+ * @version $Id$
+ *
+ */
+public class DefaultInitialDirContextFactory implements InitialDirContextFactory,
+    InitializingBean {
+    
+    //~ Static fields/initializers =============================================
+
+    private static final Log logger = LogFactory.getLog(DefaultInitialDirContextFactory.class);
+
+    private static final String CONNECTION_POOL_KEY = "com.sun.jndi.ldap.connect.pool";
+
+    //~ Instance fields ========================================================
+
+    /**
+     * The LDAP url of the server (and root context) to connect to.
+     * TODO: Allow a backup URL for a replication server.
+     */
+    private String url;
+
+    /**
+     * 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;
+
+    /**
+     * 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 = null;
+
+    /** 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;    
+
+    //~ Methods ================================================================
+
+    /**
+     * Connects anonymously unless a manager user has been specified, in which case
+     * it will bind as the manager.
+     *
+     * @return the resulting
+     */
+    public DirContext newInitialDirContext() {
+
+        if (managerDn != null) {
+            return newInitialDirContext(managerDn, managerPassword);
+        }
+
+        return connect(getEnvironment());
+    }
+
+    public DirContext newInitialDirContext(String username, String password) {
+        Hashtable env = getEnvironment();
+
+        // Don't pool connections for individual users
+        if(!username.equals(managerDn)) {
+            env.remove(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.INITIAL_CONTEXT_FACTORY, initialContextFactory);
+        env.put(Context.PROVIDER_URL, url);
+        env.put(Context.SECURITY_AUTHENTICATION, authenticationType);
+
+        if (useConnectionPool) {
+            env.put(CONNECTION_POOL_KEY, "true");
+        }
+
+        if ((extraEnvVars != null) && (extraEnvVars.size() > 0)) {
+            env.putAll(extraEnvVars);
+        }
+
+        return env;
+    }
+
+    private InitialDirContext connect(Hashtable env) {
+        
+// Prints the password, so don't use except for debugging.
+//        logger.debug("Creating initial context with env " + env);
+
+        try {
+            return new InitialDirContext(env);
+
+        } catch(CommunicationException ce) {
+            throw new DataAccessResourceFailureException("Unable to connect to LDAP Server.", ce);
+        } catch(javax.naming.AuthenticationException ae) {
+            throw new BadCredentialsException("Authentication to LDAP server failed.", ae);
+        } catch (NamingException nx) {
+            throw new LdapDataAccessException("Failed to obtain InitialDirContext", nx);
+        }
+    }
+
+    public void afterPropertiesSet() throws Exception {
+        Assert.hasLength(url, "An LDAP connection URL must be supplied.");
+
+        URI uri = new URI(url);
+
+        rootDn = uri.getPath();
+
+        if(rootDn.startsWith("/")) { // I think this is always true.
+            rootDn = rootDn.substring(1);
+        }
+
+        Assert.isTrue(uri.getScheme().equals("ldap"), "Ldap URL must start with 'ldap://'");
+
+    }
+
+    /**
+     * 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);
+        this.authenticationType = authenticationType;
+    }
+
+    public void setInitialContextFactory(String initialContextFactory) {
+        Assert.hasLength(initialContextFactory);
+        this.initialContextFactory = initialContextFactory;
+    }
+
+    /**
+     * @param managerDn The name of the "manager" user for default authentication.
+     */
+    public void setManagerDn(String managerDn) {
+        this.managerDn = managerDn;
+    }
+
+    /**
+     * @param managerPassword The "manager" user's password.
+     */
+    public void setManagerPassword(String managerPassword) {
+        this.managerPassword = managerPassword;
+    }
+
+    public void setUrl(String url) {
+        this.url = url;
+    }
+
+    /**
+     * @param extraEnvVars extra environment variables to be added at config time.
+     */
+    public void setExtraEnvVars(Map extraEnvVars) {
+        this.extraEnvVars = extraEnvVars;
+    }
+
+}

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

@@ -0,0 +1,45 @@
+/* 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.providers.ldap;
+
+import javax.naming.directory.InitialDirContext;
+import javax.naming.directory.DirContext;
+
+/**
+ * Access point for obtaining LDAP contexts.
+ *
+ * @see 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();
+}

+ 138 - 0
core/src/main/java/org/acegisecurity/providers/ldap/LdapAuthenticationProvider.java

@@ -0,0 +1,138 @@
+/* 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.providers.ldap;
+
+import org.acegisecurity.providers.dao.AbstractUserDetailsAuthenticationProvider;
+import org.acegisecurity.providers.UsernamePasswordAuthenticationToken;
+import org.acegisecurity.*;
+import org.acegisecurity.userdetails.UserDetails;
+import org.acegisecurity.userdetails.User;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.util.Assert;
+
+import javax.naming.directory.Attributes;
+
+/**
+ * The class responsible for LDAP authentication.
+ *
+ * <p>
+ * There are many ways in which an LDAP directory can be configured so this class
+ * delegates most of its responsibilites to two separate strategy interfaces,
+ * {@link LdapAuthenticator} and {@link LdapAuthoritiesPopulator}.
+ * </p>
+ *
+ * <h3>LdapAuthenticator</h3>
+ *
+ * This interface is responsible for performing the user authentication and retrieving
+ * the user's information from the directory. Example implementations are
+ * {@link org.acegisecurity.providers.ldap.authenticator.BindAuthenticator BindAuthenticator}
+ * which authenticates the user by "binding" as that user, and
+ * {@link org.acegisecurity.providers.ldap.authenticator.PasswordComparisonAuthenticator PasswordComparisonAuthenticator}
+ * which performs a comparison of the supplied password with the value stored in the directory,
+ * either by retrieving the password or performing an LDAP "compare" operation.
+ * <p>
+ * The task of retrieving the user attributes is delegated to the authenticator
+ * because the permissions on the attributes may depend on the type of authentication
+ * being used; for example, if binding as the user, it may be necessary to read them
+ * with the user's own permissions (using the same context used for the bind operation).
+ * </p>
+ *
+ * <h3>LdapAuthoritiesPopulator</h3>
+ *
+ * Once the user has been authenticated, this interface is called to obtain the set of
+ * granted authorities for the user. The
+ * {@link org.acegisecurity.providers.ldap.populator.DefaultLdapAuthoritiesPopulator DefaultLdapAuthoritiesPopulator}
+ * can be configured to obtain user role information from the user's attributes and/or to perform
+ * a search for "groups" that the user is a member of and map these to roles.
+ * <p>
+ * A custom implementation could obtain the roles from a completely different source,
+ * for example from a database.
+ * </p>
+ *
+ *
+ * @author Luke Taylor
+ * @version $Id$
+ */
+public class LdapAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
+    //~ Static fields/initializers =============================================
+
+    private static final Log logger = LogFactory.getLog(LdapAuthenticationProvider.class);
+
+    //~ Instance fields ========================================================
+
+    private LdapAuthenticator authenticator;
+
+    private LdapAuthoritiesPopulator ldapAuthoritiesPopulator;
+
+    //~ Methods ================================================================
+
+    protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
+    }
+
+    protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
+        logger.debug("Retrieving user " + username);
+
+        String password = (String)authentication.getCredentials();
+        Assert.notNull(password, "Null password was supplied in authentication token");
+
+        LdapUserDetails ldapUser = authenticator.authenticate(username, password);
+
+        return createUserDetails(username, password, ldapUser.getDn(), ldapUser.getAttributes());
+    }
+
+    protected void doAfterPropertiesSet() throws Exception {
+        super.doAfterPropertiesSet();
+        Assert.notNull(authenticator, "An LdapAuthenticator must be supplied");
+        Assert.notNull(ldapAuthoritiesPopulator, "An LdapAuthoritiesPopulator must be supplied");
+
+        // TODO: Check that the role attributes specified for the populator will be retrieved
+        // by the authenticator. If not, add them to the authenticator's list and log a
+        // warning.
+    }
+
+    /**
+     * Creates the user final <tt>UserDetails</tt> object that will be returned by the provider
+     * once the user has been authenticated.
+     * <p>
+     * The <tt>LdapAuthoritiesPopulator</tt> will be used to create the granted authorites for the
+     * user.
+     * </p>
+     * <p>
+     * Can be overridden to customize the mapping of user attributes to additional user information.
+     * </p>
+     *
+     * @param username The user login, as passed to the provider
+     * @param password The submitted password
+     * @param userDn The DN of the user in the Ldap system.
+     * @param attributes The user attributes retrieved from the Ldap system.
+     * @return The UserDetails for the successfully authenticated user.
+     */
+    protected UserDetails createUserDetails(String username, String password, String userDn, Attributes attributes) {
+
+        return new User(username, password, true, true, true, true,
+                ldapAuthoritiesPopulator.getGrantedAuthorities(username, userDn, attributes));
+
+    }
+
+    public void setAuthenticator(LdapAuthenticator authenticator) {
+        this.authenticator = authenticator;
+    }
+
+    public void setLdapAuthoritiesPopulator(LdapAuthoritiesPopulator ldapAuthoritiesPopulator) {
+        this.ldapAuthoritiesPopulator = ldapAuthoritiesPopulator;
+    }
+}

+ 39 - 0
core/src/main/java/org/acegisecurity/providers/ldap/LdapAuthenticator.java

@@ -0,0 +1,39 @@
+/* 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.providers.ldap;
+
+/**
+ * The strategy interface for locating and authenticating an Ldap user.
+ * <p>
+ * The LdapAuthenticationProvider calls this interface to authenticate a user
+ * and obtain the information for that user from the directory.
+ * </p>
+ *
+ *
+ * @author Luke Taylor
+ * @version $Id$
+ */
+public interface LdapAuthenticator {
+    /**
+     * Authenticates as a user and obtains additional user information
+     * from the directory.
+     *
+     * @param username the user's login name (<em>not</em> their DN).
+     * @param password the user's password supplied at login.
+     * @return the details of the successfully authenticated user.
+     */
+    LdapUserDetails authenticate(String username, String password);
+}

+ 43 - 0
core/src/main/java/org/acegisecurity/providers/ldap/LdapAuthoritiesPopulator.java

@@ -0,0 +1,43 @@
+/* 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.providers.ldap;
+
+import org.acegisecurity.GrantedAuthority;
+
+import javax.naming.directory.Attributes;
+
+/**
+ * Obtains a list of granted authorities for an Ldap user.
+ * <p>
+ * Used by the <tt>LdapAuthenticationProvider</tt> once a user has been
+ * authenticated to create the final user details object.
+ * </p>
+ *
+ * @author Luke Taylor
+ * @version $Id$
+ */
+public interface LdapAuthoritiesPopulator {
+
+    /**
+     *
+     * @param username the login name which was passed to the LDAP provider.
+     * @param userDn the full DN of the user
+     * @param userAttributes the user's LDAP attributes that were retrieved from the directory.
+     * @return the granted authorities for the given user.
+     */
+    GrantedAuthority[] getGrantedAuthorities(String username, String userDn, Attributes userAttributes);
+
+}

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

@@ -0,0 +1,31 @@
+/* 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.providers.ldap;
+
+import org.springframework.dao.UncategorizedDataAccessException;
+
+/**
+ * Used to wrap unexpected NamingExceptions while accessing the LDAP server.
+ *
+ * @author Luke Taylor
+ * @version $Id$
+ */
+public class LdapDataAccessException extends UncategorizedDataAccessException {
+
+    public LdapDataAccessException(String msg, Throwable ex) {
+        super(msg, ex);
+    }
+}

+ 75 - 0
core/src/main/java/org/acegisecurity/providers/ldap/LdapUserDetails.java

@@ -0,0 +1,75 @@
+/* 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.providers.ldap;
+
+import org.acegisecurity.userdetails.User;
+import org.acegisecurity.GrantedAuthority;
+import org.acegisecurity.GrantedAuthorityImpl;
+
+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 LdapUserDetails {
+
+    //~ 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 LdapUserDetails(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();
+    }
+}

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

@@ -0,0 +1,96 @@
+/* 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.providers.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;
+
+/**
+ * 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);
+        }
+    }
+
+    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);
+    }
+}

+ 118 - 0
core/src/main/java/org/acegisecurity/providers/ldap/authenticator/AbstractLdapAuthenticator.java

@@ -0,0 +1,118 @@
+/* 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.providers.ldap.authenticator;
+
+import org.acegisecurity.providers.ldap.LdapAuthenticator;
+import org.acegisecurity.providers.ldap.InitialDirContextFactory;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.util.Assert;
+
+import java.text.MessageFormat;
+
+/**
+ * @author Luke Taylor
+ * @version $Id$
+ */
+public abstract class AbstractLdapAuthenticator implements LdapAuthenticator,
+    InitializingBean {
+
+    //~ Instance fields ========================================================
+
+    private String userDnPattern = null;
+    private MessageFormat userDnFormat = null;
+    private InitialDirContextFactory initialDirContextFactory;
+    private LdapUserSearch userSearch;
+    private String[] userAttributes = null;
+
+    //~ Methods ================================================================
+
+    /**
+     * Returns the DN of the user, worked out from the userDNPattern property.
+     * The returned value includes the root DN of the provider
+     * URL used to configure the <tt>InitialDirContextfactory</tt>.
+     */
+    protected String getUserDn(String username) {
+        if(userDnFormat == null) {
+            return null;
+        }
+
+        String rootDn = initialDirContextFactory.getRootDn();
+        String userDn;
+
+        synchronized( userDnFormat ) {
+            userDn = userDnFormat.format(new String[] {username});
+        }
+
+        if(rootDn.length() > 0) {
+            userDn = userDn + "," + rootDn;
+        }
+
+        return userDn;
+    }
+
+    /**
+     * Sets the pattern which will be used to supply a DN for the user.
+     * The pattern should be the name relative to the root DN.
+     * The pattern argument {0} will contain the username.
+     * An example would be "cn={0},ou=people".
+     */
+    public void setUserDnPattern(String dnPattern) {
+        this.userDnPattern = dnPattern;
+        userDnFormat = null;
+
+        if(dnPattern != null) {
+            userDnFormat = new MessageFormat(dnPattern);
+        }
+    }
+
+    public String[] getUserAttributes() {
+        return userAttributes;
+    }
+
+    public String getUserDnPattern() {
+        return userDnPattern;
+    }
+
+    public void setUserSearch(LdapUserSearch userSearch) {
+        this.userSearch = userSearch;
+    }
+
+    protected LdapUserSearch getUserSearch() {
+        return userSearch;
+    }
+
+    public void setInitialDirContextFactory(InitialDirContextFactory initialDirContextFactory) {
+        this.initialDirContextFactory = initialDirContextFactory;
+    }
+
+    /**
+     * Sets the user attributes which will be retrieved from the directory.
+     * 
+     * @param userAttributes
+     */
+    public void setUserAttributes(String[] userAttributes) {
+        this.userAttributes = userAttributes;
+    }
+
+    protected InitialDirContextFactory getInitialDirContextFactory() {
+        return initialDirContextFactory;
+    }
+
+    public void afterPropertiesSet() throws Exception {
+        Assert.notNull(initialDirContextFactory, "initialDirContextFactory must be supplied.");
+        Assert.isTrue(userDnPattern != null || userSearch != null, "Either an LdapUserSearch or DN pattern (or both) must be supplied.");
+    }
+}

+ 97 - 0
core/src/main/java/org/acegisecurity/providers/ldap/authenticator/BindAuthenticator.java

@@ -0,0 +1,97 @@
+/* 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.providers.ldap.authenticator;
+
+import org.acegisecurity.providers.ldap.*;
+import org.acegisecurity.BadCredentialsException;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import javax.naming.directory.DirContext;
+import javax.naming.directory.Attributes;
+import javax.naming.NamingException;
+
+/**
+ * An authenticator which binds as a user.
+ *
+ * @see AbstractLdapAuthenticator
+ *
+ * @author Luke Taylor
+ * @version $Id$
+ */
+public class BindAuthenticator extends AbstractLdapAuthenticator {
+
+    //~ Static fields/initializers =============================================
+
+    private static final Log logger = LogFactory.getLog(BindAuthenticator.class);
+
+    //~ Methods ================================================================
+
+    public LdapUserDetails authenticate(String username, String password) {
+
+        String dn = getUserDn(username);
+        LdapUserDetails user = null;
+
+        // If DN is pattern is configured, try authenticating with that directly
+        if(dn != null) {
+            user = authenticateWithDn(dn, password);
+        }
+
+        // Otherwise use the configured locator to find the user
+        // and authenticate with the returned DN.
+        if(user == null && getUserSearch() != null) {
+            LdapUserDetails userFromSearch = getUserSearch().searchForUser(username);
+            user = authenticateWithDn(userFromSearch.getDn(), password);
+        }
+
+        if(user == null) {
+            throw new BadCredentialsException("Failed to authenticate as " + username);
+        }
+
+        return user;
+
+    }
+
+    private LdapUserDetails authenticateWithDn(String userDn, String password) {
+        DirContext ctx = null;
+        LdapUserDetails user = null;
+        Attributes attributes = null;
+
+        if(logger.isDebugEnabled()) {
+            logger.debug("Binding with DN = " + userDn);
+        }
+
+        try {
+            ctx = getInitialDirContextFactory().newInitialDirContext(userDn, password);
+            attributes = ctx.getAttributes(
+                    LdapUtils.getRelativeName(userDn, ctx),
+                    getUserAttributes());
+            user = new LdapUserDetails(userDn, attributes);
+            
+        } catch(NamingException ne) {
+            throw new LdapDataAccessException("Failed to load attributes for user " + userDn, ne);
+        } catch(BadCredentialsException e) {
+            // This will be thrown if an invalid user name is used and the method may
+            // be called multiple times to try different names, so we trap the exception.            
+            logger.debug("Failed to bind as " + userDn, e);
+        } finally {
+            LdapUtils.closeContext(ctx);
+        }
+
+        return user;
+    }
+
+}

+ 163 - 0
core/src/main/java/org/acegisecurity/providers/ldap/authenticator/FilterBasedLdapUserSearch.java

@@ -0,0 +1,163 @@
+/* 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.providers.ldap.authenticator;
+
+import org.acegisecurity.providers.ldap.*;
+import org.acegisecurity.userdetails.UsernameNotFoundException;
+import org.acegisecurity.BadCredentialsException;
+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 ================================================================
+
+    /**
+     * Return the LdapUserDetails containing the user's information, or null if
+     * no SearchResult is found.
+     *
+     * @param username the username to search for.
+     */
+    public LdapUserDetails searchForUser(String username) {
+        DirContext ctx = initialDirContextFactory.newInitialDirContext();
+        SearchControls ctls = new SearchControls();
+        ctls.setTimeLimit( searchTimeLimit );
+        ctls.setSearchScope( searchScope );
+
+        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 LdapUserDetails(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 afterPropertiesSet() throws Exception {
+        Assert.notNull(initialDirContextFactory, "initialDirContextFactory must be set");
+        Assert.notNull(searchFilter, "searchFilter must be set.");
+
+        if(searchBase.equals("")) {
+            logger.info("No search base DN supplied. Search will be performed from the root: " +
+                    initialDirContextFactory.getRootDn());
+        }
+    }
+
+    public void setInitialDirContextFactory(InitialDirContextFactory initialDirContextFactory) {
+        this.initialDirContextFactory = initialDirContextFactory;
+    }
+
+    public void setSearchFilter(String searchFilter) {
+        this.searchFilter = searchFilter;
+    }
+
+    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 void setSearchBase(String searchBase) {
+        this.searchBase = searchBase;
+    }
+}

+ 36 - 0
core/src/main/java/org/acegisecurity/providers/ldap/authenticator/LdapShaPasswordEncoder.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.providers.ldap.authenticator;
+
+import org.acegisecurity.providers.encoding.ShaPasswordEncoder;
+
+/**
+ * A version of {@link ShaPasswordEncoder} which always uses
+ * Base-64 encoding and prepends the string
+ * "{SHA}" to the encoded hash.
+ *
+ * @author Luke Taylor
+ * @version $Id$
+ */
+public class LdapShaPasswordEncoder extends ShaPasswordEncoder {
+    public LdapShaPasswordEncoder() {
+        super.setEncodeHashAsBase64(true);
+    }
+
+    public String encodePassword(String rawPass, Object salt) {
+        return "{SHA}" + super.encodePassword(rawPass, salt);
+    }
+}

+ 43 - 0
core/src/main/java/org/acegisecurity/providers/ldap/authenticator/LdapUserSearch.java

@@ -0,0 +1,43 @@
+/* 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.providers.ldap.authenticator;
+
+import org.acegisecurity.providers.ldap.LdapUserDetails;
+
+/**
+ * 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 LdapUserDetails object containing the user's full DN and requested attributes.
+     * TODO: Need to optionally supply required attributes here for the search.
+     */
+    LdapUserDetails searchForUser(String username);
+
+}

+ 165 - 0
core/src/main/java/org/acegisecurity/providers/ldap/authenticator/PasswordComparisonAuthenticator.java

@@ -0,0 +1,165 @@
+/* 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.providers.ldap.authenticator;
+
+import org.acegisecurity.providers.ldap.LdapUserDetails;
+import org.acegisecurity.providers.ldap.LdapUtils;
+import org.acegisecurity.providers.encoding.PasswordEncoder;
+import org.acegisecurity.BadCredentialsException;
+import org.acegisecurity.userdetails.UsernameNotFoundException;
+import org.springframework.util.Assert;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import javax.naming.NamingEnumeration;
+import javax.naming.NamingException;
+import javax.naming.directory.SearchControls;
+import javax.naming.directory.DirContext;
+import javax.naming.directory.Attribute;
+
+/**
+ * An {@link org.acegisecurity.providers.ldap.LdapAuthenticator LdapAuthenticator}
+ * which compares the login password with the value stored in the directory.
+ * <p>
+ * This can be achieved either by retrieving the password attribute for the user
+ * and comparing it locally, or by peforming an LDAP "compare" operation.
+ * If the password attribute (default "userPassword") is found in the retrieved
+ * attributes it will be compared locally. If not, the remote comparison will be
+ * attempted.
+ * </p>
+ * <p>
+ * If passwords are stored in digest form in the repository, then a suitable
+ * {@link PasswordEncoder} implementation must be supplied. By default, passwords are
+ * encoded using the {@link LdapShaPasswordEncoder}.
+ * </p>
+ *
+ * @author Luke Taylor
+ * @version $Id$
+ */
+public class PasswordComparisonAuthenticator extends AbstractLdapAuthenticator {
+    //~ Static fields/initializers =============================================
+
+    private static final Log logger = LogFactory.getLog(PasswordComparisonAuthenticator.class);
+
+    private static final String[] NO_ATTRS = new String[0];
+
+    //~ Instance fields ========================================================
+
+    private String passwordAttributeName = "userPassword";
+
+    private String passwordCompareFilter = "(userPassword={0})";
+
+    private PasswordEncoder passwordEncoder = new LdapShaPasswordEncoder();
+
+    //~ Methods ================================================================
+
+    public LdapUserDetails authenticate(String username, String password) {
+
+        // locate the user and check the password
+        String userDn = getUserDn(username);
+        LdapUserDetails user = null;
+
+        DirContext ctx = getInitialDirContextFactory().newInitialDirContext();
+
+        try {
+            if(userDn != null) {
+                String relativeName = LdapUtils.getRelativeName(userDn, ctx);
+
+                user = new LdapUserDetails(userDn,
+                        ctx.getAttributes(relativeName, getUserAttributes()));
+            }
+
+            if(user == null && getUserSearch() != null) {
+                user = getUserSearch().searchForUser(username);
+            }
+
+            if(user == null) {
+                throw new UsernameNotFoundException(username);
+            }
+
+            Attribute passwordAttribute = user.getAttributes().get(passwordAttributeName);
+
+            if(passwordAttribute != null) {
+                Object retrievedPassword = passwordAttribute.get();
+
+                if(!(retrievedPassword instanceof String)) {
+                    // Assume it's binary
+                    retrievedPassword = new String((byte[])retrievedPassword);
+                }
+
+                if(!verifyPassword(password, (String)retrievedPassword)) {
+                    throw new BadCredentialsException("Invalid password.");
+                }
+
+            } else {
+
+                doPasswordCompare(ctx, user.getRelativeName(ctx), password);
+            }
+
+            return user;
+        } catch(NamingException ne) {
+            throw new BadCredentialsException("Authentication failed due to exception ", ne);
+        } finally {
+            LdapUtils.closeContext(ctx);
+        }
+    }
+
+    /**
+     * Allows the use of both simple and hashed passwords in the directory.
+     */
+    private boolean verifyPassword(String password, String ldapPassword) {
+        if(ldapPassword.equals(password)) {
+            return true;
+        }
+
+        if(passwordEncoder.isPasswordValid(ldapPassword, password, null)) {
+            return true;
+        }
+
+        return false;
+    }
+
+    private void doPasswordCompare(DirContext ctx, String name, String password) throws NamingException {
+        if(logger.isDebugEnabled()) {
+            logger.debug("Performing LDAP compare of password for " + name);
+        }
+
+        password = passwordEncoder.encodePassword(password, null);
+        byte[] passwordBytes = LdapUtils.getUtf8Bytes(password);
+
+        SearchControls ctls = new SearchControls();
+        ctls.setReturningAttributes(NO_ATTRS);
+        ctls.setSearchScope(SearchControls.OBJECT_SCOPE);
+
+        NamingEnumeration results = ctx.search(name, passwordCompareFilter,
+                new Object[]{passwordBytes}, ctls);
+
+        if(!results.hasMore()) {
+            throw new BadCredentialsException("Password comparison failed");
+        }
+    }
+
+    public void setPasswordAttributeName(String passwordAttribute) {
+        Assert.hasLength(passwordAttribute, "passwordAttribute must not be empty or null");
+        this.passwordAttributeName = passwordAttribute;
+        this.passwordCompareFilter = "(" + passwordAttributeName + "={0})";
+    }
+
+    public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
+        Assert.notNull(passwordEncoder, "Password Encoder must not be null.");
+        this.passwordEncoder = passwordEncoder;
+    }
+}

+ 285 - 0
core/src/main/java/org/acegisecurity/providers/ldap/populator/DefaultLdapAuthoritiesPopulator.java

@@ -0,0 +1,285 @@
+/* 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.providers.ldap.populator;
+
+import org.acegisecurity.providers.ldap.LdapAuthoritiesPopulator;
+import org.acegisecurity.providers.ldap.LdapDataAccessException;
+import org.acegisecurity.providers.ldap.InitialDirContextFactory;
+import org.acegisecurity.providers.ldap.LdapUtils;
+import org.acegisecurity.GrantedAuthority;
+import org.acegisecurity.GrantedAuthorityImpl;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.util.Assert;
+import org.springframework.beans.factory.InitializingBean;
+
+import javax.naming.directory.Attributes;
+import javax.naming.directory.Attribute;
+import javax.naming.directory.SearchControls;
+import javax.naming.directory.SearchResult;
+import javax.naming.directory.DirContext;
+import javax.naming.NamingEnumeration;
+import javax.naming.NamingException;
+import java.util.Set;
+import java.util.HashSet;
+
+/**
+ * The default strategy for obtaining user role information from the directory.
+ * <p>
+ * It obtains roles by
+ * <ul>
+ * <li>Reading the values of the roles specified by the attribute names in the
+ * <tt>userRoleAttributes</tt> </li>
+ * <li>Performing a search for "groups" the user is a member of and adding
+ * those to the list of roles.</li>
+ * </ul>
+ * </p>
+ * <p>
+ * If the <tt>userRolesAttributes</tt> property is set, any matching
+ * attributes amongst those retrieved for the user will have their values added
+ * to the list of roles.
+ * If <tt>userRolesAttributes</tt> is null, no attributes will be mapped to roles.
+ * </p>
+ * <p>
+ * A typical group search scenario would be where each group/role is specified using
+ * the <tt>groupOfNames</tt> (or <tt>groupOfUniqueNames</tt>) LDAP objectClass
+ * and the user's DN is listed in the <tt>member</tt> (or <tt>uniqueMember</tt>) attribute
+ * to indicate that they should be assigned that role. The following LDIF sample
+ * has the groups stored under the DN <tt>ou=groups,dc=acegisecurity,dc=org</tt>
+ * and a group called "developers" with "ben" and "marissa" as members:
+ *
+ * <pre>
+ * dn: ou=groups,dc=acegisecurity,dc=org
+ * objectClass: top
+ * objectClass: organizationalUnit
+ * ou: groups
+ *
+ * dn: cn=developers,ou=groups,dc=acegisecurity,dc=org
+ * objectClass: groupOfNames
+ * objectClass: top
+ * cn: developers
+ * description: Acegi Security Developers
+ * member: uid=ben,ou=people,dc=acegisecurity,dc=org
+ * member: uid=marissa,ou=people,dc=acegisecurity,dc=org
+ * ou: developer
+ * </pre>
+ * </p>
+ * <p>
+ * The group search is performed within a DN specified by the <tt>groupSearchBase</tt>
+ * property, which should be relative to the root DN of its <tt>InitialDirContextFactory</tt>.
+ * If the search base is null, group searching is disabled. The filter used in the search is defined by the
+ * <tt>groupSearchFilter</tt> property, with the filter argument {0} being the full DN of the user. You can also specify which attribute defines the role name by
+ * setting the <tt>groupRoleAttribute</tt> property (the default is "cn").
+ * </p>
+ * <p>
+ * &lt;bean id="ldapAuthoritiesPopulator" class="org.acegisecurity.providers.ldap.populator.DefaultLdapAuthoritiesPopulator">
+ * TODO
+ * &lt;/bean>
+ * </p>
+ *
+ *
+ * @author Luke Taylor
+ * @version $Id$
+ */
+public class DefaultLdapAuthoritiesPopulator implements LdapAuthoritiesPopulator,
+    InitializingBean {
+    //~ Static fields/initializers =============================================
+
+    private static final Log logger = LogFactory.getLog(DefaultLdapAuthoritiesPopulator.class);
+
+    //~ Instance fields ========================================================
+
+    /** Attributes of the User's LDAP Object that contain role name information. */
+    private String[] userRoleAttributes = null;
+
+    private String rolePrefix = "";
+
+    /** The base DN from which the search for group membership should be performed */
+    private String groupSearchBase = null;
+
+    /** The pattern to be used for the user search. {0} is the user's DN */
+    private String groupSearchFilter = "(member={0})";
+
+    /** The ID of the attribute which contains the role name for a group */
+    private String groupRoleAttribute = "cn";
+
+    /** Whether group searches should be performed over the full sub-tree from the base DN */
+    // private boolean searchSubtree = false;
+
+    /** Internal variable, tied to searchSubTree property */
+    private int searchScope = SearchControls.ONELEVEL_SCOPE;
+
+    private boolean convertToUpperCase = true;
+
+    /** An initial context factory is only required if searching for groups is required. */
+    private InitialDirContextFactory initialDirContextFactory = null;
+
+    //~ Methods ================================================================
+
+    /**
+     *
+     * @param username the login name passed to the authentication provider.
+     * @param userDn the user's DN.
+     * @param userAttributes the attributes retrieved from the user's directory entry.
+     * @return the full set of roles granted to the user.
+     */
+    public GrantedAuthority[] getGrantedAuthorities(String username, String userDn, Attributes userAttributes) {
+        logger.debug("Getting authorities for user " + userDn);
+
+        Set roles = getRolesFromUserAttributes(userDn, userAttributes);
+
+        Set groupRoles = getGroupMembershipRoles(userDn, userAttributes);
+
+        if(groupRoles != null) {
+            roles.addAll(groupRoles);
+        }
+
+        return (GrantedAuthority[])roles.toArray(new GrantedAuthority[roles.size()]);
+    }
+
+    protected Set getRolesFromUserAttributes(String userDn, Attributes userAttributes) {
+        Set userRoles = new HashSet();
+
+        for(int i=0; userRoleAttributes != null && i < userRoleAttributes.length; i++) {
+            Attribute roleAttribute = userAttributes.get(userRoleAttributes[i]);
+
+            addAttributeValuesToRoleSet(roleAttribute, userRoles);
+        }
+
+        return userRoles;
+    }
+
+    /**
+     * Searches for groups the user is a member of.
+     *
+     * @param userDn the user's distinguished name.
+     * @param userAttributes
+     * @return the set of roles obtained from a group membership search.
+     */
+    protected Set getGroupMembershipRoles(String userDn, Attributes userAttributes) {
+        Set userRoles = new HashSet();
+
+        if (groupSearchBase == null) {
+            return null;
+        }
+
+        DirContext ctx = initialDirContextFactory.newInitialDirContext();
+        SearchControls ctls = new SearchControls();
+
+        ctls.setSearchScope(searchScope);
+        ctls.setReturningAttributes(new String[] {groupRoleAttribute});
+
+        try {
+            NamingEnumeration groups =
+                    ctx.search(groupSearchBase, groupSearchFilter, new String[]{userDn}, ctls);
+
+            while (groups.hasMore()) {
+                SearchResult result = (SearchResult) groups.next();
+                Attributes attrs = result.getAttributes();
+
+                // There should only be one role attribute.
+                NamingEnumeration groupRoleAttributes = attrs.getAll();
+
+                while(groupRoleAttributes.hasMore()) {
+                    Attribute roleAttribute = (Attribute) groupRoleAttributes.next();
+
+                    addAttributeValuesToRoleSet(roleAttribute, userRoles);
+                }
+            }
+        } catch (NamingException e) {
+
+        } finally {
+            LdapUtils.closeContext(ctx);
+        }
+
+        return userRoles;
+    }
+
+    private void addAttributeValuesToRoleSet(Attribute roleAttribute, Set roles) {
+        if(roleAttribute == null) {
+            return;
+        }
+
+        try {
+            NamingEnumeration attributeRoles = roleAttribute.getAll();
+
+            while(attributeRoles.hasMore()) {
+                Object role = attributeRoles.next();
+
+                // We only handle Strings for the time being
+                if(role instanceof String) {
+                    if(convertToUpperCase) {
+                        role = ((String)role).toUpperCase();
+                    }
+
+                    roles.add(new GrantedAuthorityImpl(rolePrefix + role));
+                } else {
+                    logger.warn("Non-String value found for role attribute " + roleAttribute.getID());
+                }
+            }
+        } catch(NamingException ne) {
+            throw new LdapDataAccessException("Error retrieving values for role attribute " +
+                    roleAttribute.getID(), ne);
+        }
+    }
+
+    protected String[] getUserRoleAttributes() {
+        return userRoleAttributes;
+    }
+
+    public void setUserRoleAttributes(String[] userRoleAttributes) {
+        this.userRoleAttributes = userRoleAttributes;
+    }
+
+    public void setRolePrefix(String rolePrefix) {
+        Assert.notNull(rolePrefix, "rolePrefix must not be null");
+        this.rolePrefix = rolePrefix;
+    }
+
+    public void setGroupSearchBase(String groupSearchBase) {
+        this.groupSearchBase = groupSearchBase;
+    }
+
+    public void setGroupSearchFilter(String groupSearchFilter) {
+        Assert.notNull(groupSearchFilter, "groupSearchFilter must not be null");
+        this.groupSearchFilter = groupSearchFilter;
+    }
+
+    public void setGroupRoleAttribute(String groupRoleAttribute) {
+        Assert.notNull(groupRoleAttribute, "groupRoleAttribute must not be null");
+        this.groupRoleAttribute = groupRoleAttribute;
+    }
+
+    public void setSearchSubtree(boolean searchSubtree) {
+    //    this.searchSubtree = searchSubtree;
+        this.searchScope = searchSubtree ?
+                SearchControls.SUBTREE_SCOPE : SearchControls.ONELEVEL_SCOPE;
+    }
+
+    public void setConvertToUpperCase(boolean convertToUpperCase) {
+        this.convertToUpperCase = convertToUpperCase;
+    }
+
+    public void setInitialDirContextFactory(InitialDirContextFactory initialDirContextFactory) {
+        this.initialDirContextFactory = initialDirContextFactory;
+    }
+
+    public void afterPropertiesSet() throws Exception {
+        if(initialDirContextFactory == null && groupSearchBase != null) {
+            throw new IllegalArgumentException("initialDirContextFactory is required for group role searches.");
+        }
+    }
+}

+ 25 - 0
core/src/test/java/org/acegisecurity/providers/ldap/AbstractLdapServerTestCase.java

@@ -0,0 +1,25 @@
+package org.acegisecurity.providers.ldap;
+
+import junit.framework.TestCase;
+
+/**
+ * @author Luke Taylor
+ * @version $Id$
+ */
+public abstract class AbstractLdapServerTestCase extends TestCase {
+    protected static final String ROOT_DN = "dc=acegisecurity,dc=org";
+    protected static final String PROVIDER_URL = "ldap://monkeymachine:389/"+ROOT_DN;
+    //protected static final String PROVIDER_URL = "ldap://localhost:10389/" + ROOT_DN;
+    protected static final String MANAGER_USER = "cn=manager," + ROOT_DN;
+    protected static final String MANAGER_PASSWORD = "acegisecurity";
+
+
+//    protected static final LdapTestServer server = new LdapTestServer();
+
+    protected AbstractLdapServerTestCase() {
+    }
+
+    protected AbstractLdapServerTestCase(String string) {
+        super(string);
+    }
+}

+ 140 - 0
core/src/test/java/org/acegisecurity/providers/ldap/InitialDirContextFactoryTests.java

@@ -0,0 +1,140 @@
+package org.acegisecurity.providers.ldap;
+
+import javax.naming.Context;
+import javax.naming.directory.DirContext;
+import java.util.Hashtable;
+
+import org.springframework.dao.DataAccessResourceFailureException;
+import org.acegisecurity.BadCredentialsException;
+
+/**
+ * Tests {@link InitialDirContextFactory}.
+ *
+ * @author Luke Taylor
+ * @version $Id$
+ */
+public class InitialDirContextFactoryTests extends AbstractLdapServerTestCase {
+
+    public void testNonLdapUrlIsRejected() throws Exception {
+        DefaultInitialDirContextFactory idf = new DefaultInitialDirContextFactory();
+
+        idf.setUrl("http://acegisecurity.org/dc=acegisecurity,dc=org");
+
+        try {
+            idf.afterPropertiesSet();
+            fail("Expected exception for non 'ldap://' URL");
+        } catch(IllegalArgumentException expected) {
+        }
+    }
+
+    public void testConnectionFailure() throws Exception {
+        DefaultInitialDirContextFactory idf = new DefaultInitialDirContextFactory();
+        // Use the wrong port
+        idf.setUrl("ldap://localhost:60389");
+        Hashtable env = new Hashtable();
+        env.put("com.sun.jndi.ldap.connect.timeout", "200");
+        idf.setExtraEnvVars(env);
+        idf.afterPropertiesSet();
+        try {
+            idf.newInitialDirContext();
+            fail("Connection succeeded unexpectedly");
+        } catch(DataAccessResourceFailureException expected) {
+        }
+    }
+
+    public void testAnonymousBindSucceeds() throws Exception {
+        DefaultInitialDirContextFactory idf = new DefaultInitialDirContextFactory();
+        idf.setUrl(PROVIDER_URL);
+        idf.afterPropertiesSet();
+        DirContext ctx = idf.newInitialDirContext();
+        // Connection pooling should be set by default for anon users.
+        assertEquals("true",ctx.getEnvironment().get("com.sun.jndi.ldap.connect.pool"));
+        ctx.close();
+    }
+
+    public void testBindAsManagerSucceeds() throws Exception {
+        DefaultInitialDirContextFactory idf = new DefaultInitialDirContextFactory();
+        idf.setUrl(PROVIDER_URL);
+        idf.setManagerPassword(MANAGER_PASSWORD);
+        idf.setManagerDn(MANAGER_USER);
+        idf.afterPropertiesSet();
+        DirContext ctx = idf.newInitialDirContext();
+        assertEquals("true",ctx.getEnvironment().get("com.sun.jndi.ldap.connect.pool"));
+        ctx.close();
+    }
+
+    public void testInvalidPasswordCausesBadCredentialsException() throws Exception {
+        DefaultInitialDirContextFactory idf = new DefaultInitialDirContextFactory();
+        idf.setUrl(PROVIDER_URL);
+        idf.setManagerDn(MANAGER_USER);
+        idf.setManagerPassword("wrongpassword");
+        idf.afterPropertiesSet();
+        try {
+            DirContext ctx = idf.newInitialDirContext();
+            fail("Authentication with wrong credentials should fail.");
+        } catch(BadCredentialsException expected) {
+        }
+    }
+
+    public void testConnectionAsSpecificUserSucceeds() throws Exception {
+        DefaultInitialDirContextFactory idf = new DefaultInitialDirContextFactory();
+        idf.setUrl(PROVIDER_URL);
+        idf.afterPropertiesSet();
+        DirContext ctx = idf.newInitialDirContext("uid=Bob,ou=people,dc=acegisecurity,dc=org",
+                "bobspassword");
+        // We don't want pooling for specific users.
+        assertNull(ctx.getEnvironment().get("com.sun.jndi.ldap.connect.pool"));
+        ctx.close();
+    }
+
+    public void testEnvironment() {
+        DefaultInitialDirContextFactory idf = new DefaultInitialDirContextFactory();
+        idf.setUrl("ldap://acegisecurity.org/");
+
+        // check basic env
+        Hashtable env = idf.getEnvironment();
+        assertEquals("com.sun.jndi.ldap.LdapCtxFactory", env.get(Context.INITIAL_CONTEXT_FACTORY));
+        assertEquals("ldap://acegisecurity.org/", env.get(Context.PROVIDER_URL));
+        assertEquals("simple",env.get(Context.SECURITY_AUTHENTICATION));
+        assertNull(env.get(Context.SECURITY_PRINCIPAL));
+        assertNull(env.get(Context.SECURITY_CREDENTIALS));
+
+        // Ctx factory.
+        idf.setInitialContextFactory("org.acegisecurity.NonExistentCtxFactory");
+        env = idf.getEnvironment();
+        assertEquals("org.acegisecurity.NonExistentCtxFactory", env.get(Context.INITIAL_CONTEXT_FACTORY));
+
+        // Auth type
+        idf.setAuthenticationType("myauthtype");
+        env = idf.getEnvironment();
+        assertEquals("myauthtype", env.get(Context.SECURITY_AUTHENTICATION));
+
+        // Check extra vars
+        Hashtable extraVars = new Hashtable();
+        extraVars.put("extravar", "extravarvalue");
+        idf.setExtraEnvVars(extraVars);
+        env = idf.getEnvironment();
+        assertEquals("extravarvalue", env.get("extravar"));
+    }
+
+    public void testBaseDnIsParsedFromCorrectlyFromUrl() throws Exception {
+        DefaultInitialDirContextFactory idf = new DefaultInitialDirContextFactory();
+
+        idf.setUrl("ldap://acegisecurity.org/dc=acegisecurity,dc=org");
+        idf.afterPropertiesSet();
+        assertEquals("dc=acegisecurity,dc=org", idf.getRootDn());
+
+        // Check with an empty root
+        idf = new DefaultInitialDirContextFactory();
+        idf.setUrl("ldap://acegisecurity.org/");
+        idf.afterPropertiesSet();
+        assertEquals("", idf.getRootDn());
+
+        // Empty root without trailing slash
+        idf = new DefaultInitialDirContextFactory();
+        idf.setUrl("ldap://acegisecurity.org");
+        idf.afterPropertiesSet();
+        assertEquals("", idf.getRootDn());
+    }
+
+}

+ 102 - 0
core/src/test/java/org/acegisecurity/providers/ldap/LdapAuthenticationProviderTests.java

@@ -0,0 +1,102 @@
+package org.acegisecurity.providers.ldap;
+
+import javax.naming.directory.Attributes;
+import javax.naming.directory.BasicAttributes;
+
+import org.acegisecurity.GrantedAuthority;
+import org.acegisecurity.GrantedAuthorityImpl;
+import org.acegisecurity.BadCredentialsException;
+import org.acegisecurity.Authentication;
+import org.acegisecurity.providers.UsernamePasswordAuthenticationToken;
+import org.acegisecurity.providers.ldap.authenticator.FilterBasedLdapUserSearch;
+import org.acegisecurity.providers.ldap.authenticator.BindAuthenticator;
+import org.acegisecurity.providers.ldap.populator.DefaultLdapAuthoritiesPopulator;
+import org.acegisecurity.userdetails.UserDetails;
+
+/**
+ * @author Luke Taylor
+ * @version $Id$
+ */
+public class LdapAuthenticationProviderTests extends AbstractLdapServerTestCase {
+    DefaultInitialDirContextFactory dirCtxFactory;
+
+
+    public LdapAuthenticationProviderTests(String string) {
+        super(string);
+    }
+
+    public LdapAuthenticationProviderTests() {
+        super();
+    }
+
+    public void testNormalUsage() throws Exception {
+        LdapAuthenticationProvider ldapProvider = new LdapAuthenticationProvider();
+
+        ldapProvider.setAuthenticator(new MockAuthenticator());
+        ldapProvider.setLdapAuthoritiesPopulator(new MockAuthoritiesPopulator());
+        ldapProvider.afterPropertiesSet();
+
+        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("bob","bobspassword");
+        UserDetails user = ldapProvider.retrieveUser("bob", token);
+        assertEquals(1, user.getAuthorities().length);
+        assertTrue(user.getAuthorities()[0].equals("ROLE_USER"));
+        ldapProvider.additionalAuthenticationChecks(user, token);
+
+    }
+
+    public void testIntegration() throws Exception {
+        LdapAuthenticationProvider ldapProvider = new LdapAuthenticationProvider();
+
+        // Connection information
+        DefaultInitialDirContextFactory dirCtxFactory = new DefaultInitialDirContextFactory();
+        dirCtxFactory.setUrl(PROVIDER_URL);
+        dirCtxFactory.setManagerDn(MANAGER_USER);
+        dirCtxFactory.setManagerPassword(MANAGER_PASSWORD);
+        dirCtxFactory.afterPropertiesSet();
+        BindAuthenticator authenticator = new BindAuthenticator();
+        //PasswordComparisonAuthenticator authenticator = new PasswordComparisonAuthenticator();
+        authenticator.setInitialDirContextFactory(dirCtxFactory);
+        //authenticator.setUserDnPattern("cn={0},ou=people");
+
+        FilterBasedLdapUserSearch userSearch = new FilterBasedLdapUserSearch();
+        userSearch.setSearchBase("ou=people");
+        userSearch.setSearchFilter("(cn={0})");
+        userSearch.setInitialDirContextFactory(dirCtxFactory);
+        userSearch.afterPropertiesSet();
+
+        authenticator.setUserSearch(userSearch);
+
+        authenticator.afterPropertiesSet();
+
+        DefaultLdapAuthoritiesPopulator populator;
+        populator = new DefaultLdapAuthoritiesPopulator();
+        populator.setRolePrefix("ROLE_");
+        populator.setInitialDirContextFactory(dirCtxFactory);
+        populator.setGroupSearchBase("ou=groups");
+        populator.afterPropertiesSet();
+
+        ldapProvider.setLdapAuthoritiesPopulator(populator);
+        ldapProvider.setAuthenticator(authenticator);
+        Authentication auth = ldapProvider.authenticate(new UsernamePasswordAuthenticationToken("Ben Alex","benspassword"));
+        assertEquals(2, auth.getAuthorities().length);
+    }
+
+    class MockAuthoritiesPopulator implements LdapAuthoritiesPopulator {
+
+        public GrantedAuthority[] getGrantedAuthorities(String userDn, String dn, Attributes userAttributes) {
+            return new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_USER") };
+        }
+    }
+
+    class MockAuthenticator implements LdapAuthenticator {
+        Attributes userAttributes = new BasicAttributes("cn","bob");
+
+        public LdapUserDetails authenticate(String username, String password) {
+            if(username.equals("bob") && password.equals("bobspassword")) {
+
+                return new LdapUserDetails("cn=bob,ou=people,dc=acegisecurity,dc=org", userAttributes);
+            }
+            throw new BadCredentialsException("Authentication of Bob failed.");
+        }
+    }
+}

+ 94 - 0
core/src/test/java/org/acegisecurity/providers/ldap/LdapTestServer.java

@@ -0,0 +1,94 @@
+package org.acegisecurity.providers.ldap;
+
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.support.ClassPathXmlApplicationContext;
+import org.springframework.core.io.ClassPathResource;
+import org.apache.ldap.server.configuration.MutableServerStartupConfiguration;
+import org.apache.ldap.server.jndi.ServerContextFactory;
+
+import javax.naming.Context;
+import javax.naming.NamingException;
+import javax.naming.NameAlreadyBoundException;
+import javax.naming.directory.InitialDirContext;
+import javax.naming.directory.Attributes;
+import javax.naming.directory.BasicAttributes;
+import javax.naming.directory.Attribute;
+import javax.naming.directory.BasicAttribute;
+import javax.naming.directory.DirContext;
+import java.io.IOException;
+import java.util.Properties;
+
+/**
+ * @author Luke Taylor
+ * @version $Id$
+ */
+public class LdapTestServer {
+
+    //~ Instance fields ========================================================
+    
+    private DirContext serverContext;
+
+    //~ Constructors ================================================================
+
+    public LdapTestServer() {
+        startLdapServer();
+        createManagerUser();
+    }
+
+    //~ Methods ================================================================
+
+    private void startLdapServer() {
+        ApplicationContext factory = new ClassPathXmlApplicationContext( "org/acegisecurity/providers/ldap/apacheds-context.xml");
+        MutableServerStartupConfiguration cfg = ( MutableServerStartupConfiguration ) factory.getBean( "configuration" );
+        ClassPathResource ldifDir = new ClassPathResource("org/acegisecurity/providers/ldap/ldif");
+
+        try {
+            cfg.setLdifDirectory(ldifDir.getFile());
+        } catch (IOException e) {
+            System.err.println("Failed to set LDIF directory for server");
+            e.printStackTrace();
+        }
+
+        Properties env = ( Properties ) factory.getBean( "environment" );
+
+        env.setProperty( Context.PROVIDER_URL, "dc=acegisecurity,dc=org" );
+        env.setProperty( Context.INITIAL_CONTEXT_FACTORY, ServerContextFactory.class.getName() );
+        env.putAll( cfg.toJndiEnvironment() );
+
+        try {
+            serverContext = new InitialDirContext( env );
+        } catch (NamingException e) {
+            System.err.println("Failed to start Apache DS");
+            e.printStackTrace();
+        }
+    }
+
+    private void createManagerUser() {
+        Attributes user = new BasicAttributes( "cn", "manager" , true );
+        user.put( "userPassword", "acegisecurity" );
+        Attribute objectClass = new BasicAttribute("objectClass");
+        user.put( objectClass );
+        objectClass.add( "top" );
+        objectClass.add( "person" );
+        objectClass.add( "organizationalPerson" );
+        objectClass.add( "inetOrgPerson" );
+        user.put( "sn", "Manager" );
+        user.put( "cn", "manager" );
+        try {
+            serverContext.createSubcontext("cn=manager", user );
+        } catch(NameAlreadyBoundException ignore) {
+            System.out.println("Manager user already exists.");
+        } catch (NamingException ne) {
+            System.err.println("Failed to create manager user.");
+            ne.printStackTrace();
+        }
+    }
+
+    public DirContext getServerContext() {
+        return serverContext;
+    }
+
+    public static void main(String[] args) {
+        new LdapTestServer();
+    }
+}

+ 73 - 0
core/src/test/java/org/acegisecurity/providers/ldap/authenticator/BindAuthenticatorTests.java

@@ -0,0 +1,73 @@
+package org.acegisecurity.providers.ldap.authenticator;
+
+import org.acegisecurity.providers.ldap.DefaultInitialDirContextFactory;
+import org.acegisecurity.providers.ldap.LdapUserDetails;
+import org.acegisecurity.providers.ldap.AbstractLdapServerTestCase;
+import org.acegisecurity.BadCredentialsException;
+
+/**
+ * Tests {@link BindAuthenticator}.
+ *
+ * @author Luke Taylor
+ * @version $Id$
+ */
+public class BindAuthenticatorTests extends AbstractLdapServerTestCase {
+
+    private DefaultInitialDirContextFactory dirCtxFactory;
+    private BindAuthenticator authenticator;
+
+    public void setUp() throws Exception {
+        // Connection information
+        dirCtxFactory = new DefaultInitialDirContextFactory();
+        dirCtxFactory.setUrl(PROVIDER_URL);
+        dirCtxFactory.afterPropertiesSet();
+        authenticator = new BindAuthenticator();
+        authenticator.setInitialDirContextFactory(dirCtxFactory);
+    }
+
+    public void testUserDnPatternReturnsCorrectDn() throws Exception {
+        authenticator.setUserDnPattern("cn={0},ou=people");
+        assertEquals("cn=Joe,ou=people,"+ ROOT_DN, authenticator.getUserDn("Joe"));
+    }
+
+    public void testAuthenticationWithCorrectPasswordSucceeds() throws Exception {
+        authenticator.setUserDnPattern("uid={0},ou=people");
+        LdapUserDetails user = authenticator.authenticate("bob","bobspassword");
+    }
+
+    public void testAuthenticationWithWrongPasswordFails() {
+        BindAuthenticator authenticator = new BindAuthenticator();
+
+        authenticator.setInitialDirContextFactory(dirCtxFactory);
+        authenticator.setUserDnPattern("uid={0},ou=people");
+
+        try {
+            authenticator.authenticate("bob","wrongpassword");
+            fail("Shouldn't be able to bind with wrong password");
+        } catch(BadCredentialsException expected) {
+        }
+    }
+
+    public void testAuthenticationWithUserSearch() throws Exception {
+        LdapUserDetails user = new LdapUserDetails("uid=bob,ou=people," + ROOT_DN, null);
+        authenticator.setUserSearch(new MockUserSearch(user));
+        authenticator.afterPropertiesSet();
+        authenticator.authenticate("bob","bobspassword");
+    }
+
+
+// Apache DS falls apart with unknown DNs.
+//
+//    public void testAuthenticationWithInvalidUserNameFails() {
+//        BindAuthenticator authenticator = new BindAuthenticator();
+//
+//        authenticator.setInitialDirContextFactory(dirCtxFactory);
+//        authenticator.setUserDnPattern("cn={0},ou=people");
+//        try {
+//            authenticator.authenticate("Baz","bobspassword");
+//            fail("Shouldn't be able to bind with invalid username");
+//        } catch(BadCredentialsException expected) {
+//        }
+//    }
+}
+

+ 87 - 0
core/src/test/java/org/acegisecurity/providers/ldap/authenticator/FilterBasedLdapUserSearchTests.java

@@ -0,0 +1,87 @@
+package org.acegisecurity.providers.ldap.authenticator;
+
+import org.acegisecurity.providers.ldap.AbstractLdapServerTestCase;
+import org.acegisecurity.providers.ldap.DefaultInitialDirContextFactory;
+import org.acegisecurity.providers.ldap.LdapUserDetails;
+import org.acegisecurity.userdetails.UsernameNotFoundException;
+import org.acegisecurity.BadCredentialsException;
+
+/**
+ * Tests for FilterBasedLdapUserSearch.
+ * 
+ * @author Luke Taylor
+ * @version $Id$
+ */
+public class FilterBasedLdapUserSearchTests extends AbstractLdapServerTestCase {
+    private DefaultInitialDirContextFactory dirCtxFactory;
+    private FilterBasedLdapUserSearch locator;
+
+    public void setUp() throws Exception {
+        dirCtxFactory = new DefaultInitialDirContextFactory();
+        dirCtxFactory.setUrl(PROVIDER_URL);
+        dirCtxFactory.setManagerDn(MANAGER_USER);
+        dirCtxFactory.setManagerPassword(MANAGER_PASSWORD);
+        dirCtxFactory.afterPropertiesSet();
+        locator = new FilterBasedLdapUserSearch();
+        locator.setSearchSubtree(false);
+        locator.setSearchTimeLimit(0);
+        locator.setInitialDirContextFactory(dirCtxFactory);
+    }
+
+    public FilterBasedLdapUserSearchTests(String string) {
+        super(string);
+    }
+
+    public FilterBasedLdapUserSearchTests() {
+        super();
+    }
+
+    public void testBasicSearch() throws Exception {
+        locator.setSearchBase("ou=people");
+        locator.setSearchFilter("(uid={0})");
+        locator.afterPropertiesSet();
+        LdapUserDetails bob = locator.searchForUser("Bob");
+        assertEquals("uid=bob,ou=people,"+ROOT_DN, bob.getDn());
+    }
+
+    public void testSubTreeSearchSucceeds() throws Exception {
+        // Don't set the searchBase, so search from the root.
+        locator.setSearchFilter("(uid={0})");
+        locator.setSearchSubtree(true);
+        locator.afterPropertiesSet();
+        LdapUserDetails bob = locator.searchForUser("Bob");
+        assertEquals("uid=bob,ou=people,"+ROOT_DN, bob.getDn());
+    }
+
+    public void testSearchForInvalidUserFails() {
+        locator.setSearchBase("ou=people");
+        locator.setSearchFilter("(uid={0})");
+
+        try {
+            locator.searchForUser("Joe");
+            fail("Expected UsernameNotFoundException for non-existent user.");
+        } catch (UsernameNotFoundException expected) {
+        }
+    }
+
+    public void testFailsOnMultipleMatches() {
+        locator.setSearchBase("ou=people");
+        locator.setSearchFilter("(cn=*)");
+
+        try {
+            locator.searchForUser("Ignored");
+            fail("Expected exception for multiple search matches.");
+        } catch (BadCredentialsException expected) {
+        }
+    }
+
+    /** Try some funny business with filters. */
+    public void testExtraFilterPartToExcludeBob() {
+        locator.setSearchBase("ou=people");
+        locator.setSearchFilter("(&(cn=*)(!(uid={0})))");
+
+        // Search for bob, get back ben...
+        LdapUserDetails ben = locator.searchForUser("bob");
+        assertEquals("cn=Ben Alex,ou=people,"+ROOT_DN, ben.getDn());
+    }
+}

+ 19 - 0
core/src/test/java/org/acegisecurity/providers/ldap/authenticator/MockUserSearch.java

@@ -0,0 +1,19 @@
+package org.acegisecurity.providers.ldap.authenticator;
+
+import org.acegisecurity.providers.ldap.LdapUserDetails;
+
+/**
+ * @author Luke Taylor
+ * @version $Id$
+ */
+public class MockUserSearch implements LdapUserSearch {
+    LdapUserDetails user;
+
+    public MockUserSearch(LdapUserDetails user) {
+        this.user = user;
+    }
+
+    public LdapUserDetails searchForUser(String username) {
+        return user;
+    }
+}

+ 58 - 0
core/src/test/java/org/acegisecurity/providers/ldap/authenticator/PasswordComparisonAuthenticatorMockTests.java

@@ -0,0 +1,58 @@
+package org.acegisecurity.providers.ldap.authenticator;
+
+import org.jmock.Mock;
+import org.jmock.MockObjectTestCase;
+import org.acegisecurity.providers.ldap.InitialDirContextFactory;
+
+import javax.naming.directory.DirContext;
+import javax.naming.directory.BasicAttributes;
+import javax.naming.directory.Attributes;
+
+/**
+ * @author Luke Taylor
+ * @version $Id$
+ */
+public class PasswordComparisonAuthenticatorMockTests extends MockObjectTestCase {
+
+    public void testLdapCompareIsUsedWhenPasswordIsNotRetrieved() throws Exception {
+        Mock mockCtx = new Mock(DirContext.class);
+
+        PasswordComparisonAuthenticator authenticator = new PasswordComparisonAuthenticator();
+        authenticator.setUserDnPattern("cn={0},ou=people");
+        authenticator.setInitialDirContextFactory(
+                new MockInitialDirContextFactory((DirContext)mockCtx.proxy(),
+                        "dc=acegisecurity,dc=org"));
+        // Get the mock to return an empty attribute set
+        mockCtx.expects(atLeastOnce()).method("getNameInNamespace").will(returnValue("dc=acegisecurity,dc=org"));
+        mockCtx.expects(once()).method("getAttributes").with(eq("cn=Bob,ou=people"), NULL).will(returnValue(new BasicAttributes()));
+        // Setup a single return value (i.e. success)
+        Attributes searchResults = new BasicAttributes("", null);
+        mockCtx.expects(once()).method("search").with(eq("cn=Bob,ou=people"),
+                eq("(userPassword={0})"), NOT_NULL, NOT_NULL).will(returnValue(searchResults.getAll()));
+        mockCtx.expects(once()).method("close");
+        authenticator.authenticate("Bob", "bobspassword");
+    }
+
+    class MockInitialDirContextFactory implements InitialDirContextFactory {
+        DirContext ctx;
+        String baseDn;
+
+        public MockInitialDirContextFactory(DirContext ctx, String baseDn) {
+            this.baseDn = baseDn;
+            this.ctx = ctx;
+        }
+
+        public DirContext newInitialDirContext() {
+            return ctx;
+        }
+
+        public DirContext newInitialDirContext(String username, String password) {
+            return ctx;
+        }
+
+        public String getRootDn() {
+            return baseDn;
+        }
+    }
+
+}

+ 133 - 0
core/src/test/java/org/acegisecurity/providers/ldap/authenticator/PasswordComparisonAuthenticatorTests.java

@@ -0,0 +1,133 @@
+package org.acegisecurity.providers.ldap.authenticator;
+
+import org.acegisecurity.providers.ldap.DefaultInitialDirContextFactory;
+import org.acegisecurity.providers.ldap.LdapUserDetails;
+import org.acegisecurity.providers.ldap.AbstractLdapServerTestCase;
+import org.acegisecurity.providers.encoding.PlaintextPasswordEncoder;
+import org.acegisecurity.BadCredentialsException;
+import org.acegisecurity.userdetails.UsernameNotFoundException;
+
+import javax.naming.directory.BasicAttributes;
+
+/**
+ * @author Luke Taylor
+ * @version $Id$
+ */
+public class PasswordComparisonAuthenticatorTests extends AbstractLdapServerTestCase {
+    private DefaultInitialDirContextFactory dirCtxFactory;
+    private PasswordComparisonAuthenticator authenticator;
+
+    public void setUp() throws Exception {
+        // Connection information
+        dirCtxFactory = new DefaultInitialDirContextFactory();
+        dirCtxFactory.setUrl(PROVIDER_URL);
+        dirCtxFactory.setManagerDn(MANAGER_USER);
+        dirCtxFactory.setManagerPassword(MANAGER_PASSWORD);
+        dirCtxFactory.afterPropertiesSet();
+        authenticator = new PasswordComparisonAuthenticator();
+        authenticator.setInitialDirContextFactory(dirCtxFactory);
+        authenticator.setUserDnPattern("uid={0},ou=people");
+    }
+
+    public void tearDown() {
+       // com.sun.jndi.ldap.LdapPoolManager.showStats(System.out);
+    }
+
+    public void testLdapCompareSucceedsWithCorrectPassword() {
+        // Don't retrieve the password
+        authenticator.setUserAttributes(new String[] {"cn", "sn"});
+        // Bob has a plaintext password.
+        authenticator.setPasswordEncoder(new PlaintextPasswordEncoder());
+        authenticator.authenticate("Bob", "bobspassword");
+    }
+
+    public void testLdapCompareSucceedsWithShaEncodedPassword() {
+        authenticator = new PasswordComparisonAuthenticator();
+        authenticator.setInitialDirContextFactory(dirCtxFactory);
+        authenticator.setUserDnPattern("cn={0},ou=people");
+        // Don't retrieve the password
+        authenticator.setUserAttributes(new String[] {"cn", "sn"});
+        authenticator.authenticate("Ben Alex", "benspassword");
+    }
+
+    public void testPasswordEncoderCantBeNull() {
+        try {
+            authenticator.setPasswordEncoder(null);
+            fail("Password encoder can't be null");
+        } catch(IllegalArgumentException expected) {
+        }
+    }
+
+    public void testLdapPasswordCompareFailsWithWrongPassword() {
+        // Don't retrieve the password
+        authenticator.setUserAttributes(new String[] {"cn", "sn"});
+
+        try {
+            authenticator.authenticate("Bob", "wrongpassword");
+            fail("Authentication should fail with wrong password.");
+        } catch(BadCredentialsException expected) {
+        }
+    }
+
+    public void testLocalPasswordComparisonSucceedsWithCorrectPassword() {
+        authenticator.authenticate("Bob", "bobspassword");
+    }
+
+    public void testLocalCompareSucceedsWithShaEncodedPassword() {
+        authenticator = new PasswordComparisonAuthenticator();
+        authenticator.setInitialDirContextFactory(dirCtxFactory);
+        authenticator.setUserDnPattern("cn={0},ou=people");
+        authenticator.authenticate("Ben Alex", "benspassword");
+    }
+
+    public void testLocalPasswordComparisonFailsWithWrongPassword() {
+        try {
+            authenticator.authenticate("Bob", "wrongpassword");
+            fail("Authentication should fail with wrong password.");
+        } catch(BadCredentialsException expected) {
+        }
+    }
+
+    public void testAllAttributesAreRetrivedByDefault() {
+        LdapUserDetails user = authenticator.authenticate("Bob", "bobspassword");
+        System.out.println(user.getAttributes().toString());
+        assertEquals("User should have 5 attributes", 5, user.getAttributes().size());
+
+    }
+
+    public void testOnlySpecifiedAttributesAreRetrieved() throws Exception {
+        authenticator.setUserAttributes(new String[] {"cn", "sn"});
+        authenticator.setPasswordEncoder(new PlaintextPasswordEncoder());
+        LdapUserDetails user = authenticator.authenticate("Bob", "bobspassword");
+        assertEquals("Should have retrieved 2 attributes (cn, sn)",2, user.getAttributes().size());
+        assertEquals("Bob Hamilton", user.getAttributes().get("cn").get());
+        assertEquals("Hamilton", user.getAttributes().get("sn").get());
+    }
+
+    public void testUseOfDifferentPasswordAttribute() {
+        authenticator.setPasswordAttributeName("sn");
+        authenticator.authenticate("Bob", "Hamilton");
+    }
+
+    public void testWithUserSearch() {
+        LdapUserDetails user = new LdapUserDetails("uid=Bob,ou=people" + ROOT_DN,
+                new BasicAttributes("userPassword","bobspassword"));
+        authenticator.setUserDnPattern(null);
+        assertNull(authenticator.getUserDnPattern());
+        assertNull(authenticator.getUserDn("Bob"));
+        authenticator.setUserSearch(new MockUserSearch(user));
+        authenticator.authenticate("ShouldntBeUsed","bobspassword");
+    }
+
+    public void testFailedSearchGivesUserNotFoundException() throws Exception {
+        authenticator.setUserDnPattern(null);
+        authenticator.setUserSearch(new MockUserSearch(null));
+        authenticator.afterPropertiesSet();
+
+        try {
+            authenticator.authenticate("Joe","password");
+            fail("Expected exception on failed user search");
+        } catch (UsernameNotFoundException expected) {
+        }
+    }
+}

+ 76 - 0
core/src/test/java/org/acegisecurity/providers/ldap/populator/DefaultLdapAuthoritiesPopulatorTests.java

@@ -0,0 +1,76 @@
+package org.acegisecurity.providers.ldap.populator;
+
+import javax.naming.directory.Attributes;
+import javax.naming.directory.BasicAttributes;
+import javax.naming.directory.BasicAttribute;
+
+import org.acegisecurity.GrantedAuthority;
+import org.acegisecurity.providers.ldap.AbstractLdapServerTestCase;
+import org.acegisecurity.providers.ldap.DefaultInitialDirContextFactory;
+
+import java.util.Set;
+import java.util.HashSet;
+
+/**
+ * @author Luke Taylor
+ * @version $Id$
+ */
+public class DefaultLdapAuthoritiesPopulatorTests extends AbstractLdapServerTestCase {
+    private DefaultInitialDirContextFactory dirCtxFactory;
+    private DefaultLdapAuthoritiesPopulator populator;
+
+    public void setUp() {
+        dirCtxFactory = new DefaultInitialDirContextFactory();
+        dirCtxFactory.setUrl(PROVIDER_URL);
+        dirCtxFactory.setManagerDn(MANAGER_USER);
+        dirCtxFactory.setManagerPassword(MANAGER_PASSWORD);
+
+        populator = new DefaultLdapAuthoritiesPopulator();
+        populator.setRolePrefix("ROLE_");
+    }
+
+    public void testCtxFactoryMustBeSetIfSearchBaseIsSet() throws Exception {
+        populator.setGroupSearchBase("");
+
+        try {
+            populator.afterPropertiesSet();
+            fail("expected exception.");
+        } catch (IllegalArgumentException expected) {
+        }
+    }
+
+    public void testUserAttributeMappingToRoles() {
+        populator.setUserRoleAttributes(new String[] {"userRole", "otherUserRole"});
+        populator.getUserRoleAttributes();
+
+        Attributes userAttrs = new BasicAttributes();
+        BasicAttribute attr = new BasicAttribute("userRole", "role1");
+        attr.add("role2");
+        userAttrs.put(attr);
+        attr = new BasicAttribute("otherUserRole", "role3");
+        attr.add("role2"); // duplicate
+        userAttrs.put(attr);
+
+        GrantedAuthority[] authorities = populator.getGrantedAuthorities("Ignored", "Ignored", userAttrs);
+        assertEquals("User should have three roles", 3, authorities.length);
+    }
+
+    public void testGroupSearch() throws Exception {
+        populator.setInitialDirContextFactory(dirCtxFactory);
+        populator.setGroupSearchBase("ou=groups");
+        populator.setGroupRoleAttribute("ou");
+        populator.setSearchSubtree(true);
+        populator.setSearchSubtree(false);
+        populator.setConvertToUpperCase(true);
+        populator.setGroupSearchFilter("member={0}");
+        populator.afterPropertiesSet();
+
+        GrantedAuthority[] authorities = populator.getGrantedAuthorities("Ben", "cn=Ben Alex,ou=people,"+ROOT_DN, new BasicAttributes());
+        assertEquals("Should have 2 roles", 2, authorities.length);
+        Set roles = new HashSet();
+        roles.add(authorities[0].toString());
+        roles.add(authorities[1].toString());
+        assertTrue(roles.contains("ROLE_DEVELOPER"));
+        assertTrue(roles.contains("ROLE_MANAGER"));
+    }
+}

+ 101 - 0
core/src/test/resources/org/acegisecurity/providers/ldap/apacheds-context.xml

@@ -0,0 +1,101 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN"
+  "http://www.springframework.org/dtd/spring-beans.dtd">
+
+<beans>
+  <!-- JNDI environment variable -->
+  <bean id="environment" class="org.springframework.beans.factory.config.PropertiesFactoryBean">
+    <property name="properties">
+      <props>
+    	  <!--prop key="asn.1.berlib.provider">org.apache.ldap.common.berlib.asn1.SnickersProvider</prop -->
+    	  <!--prop key="asn.1.berlib.provider">org.apache.asn1new.ldap.TwixProvider</prop-->
+    	  <prop key="java.naming.security.authentication">simple</prop>
+    	  <prop key="java.naming.security.principal">uid=admin,ou=system</prop>
+        <prop key="java.naming.security.credentials">secret</prop>
+        <prop key="java.naming.ldap.attributes.binary">
+          photo personalSignature audio jpegPhoto javaSerializedData userPassword
+          userCertificate cACertificate authorityRevocationList certificateRevocationList
+          crossCertificatePair x500UniqueIdentifier krb5Key
+        </prop>
+      </props>
+    </property>
+  </bean>
+
+  <!-- StartupConfiguration to start ApacheDS -->
+  <bean id="configuration" class="org.apache.ldap.server.configuration.MutableServerStartupConfiguration">
+    <property name="workingDirectory"><value>${java.io.tmpdir}/apache_ds</value></property>
+    <property name="allowAnonymousAccess"><value>true</value></property>
+    <property name="accessControlEnabled"><value>false</value></property>
+    <property name="ldapPort"><value>10389</value></property>
+    <property name="contextPartitionConfigurations">
+      <set>
+        <ref bean="acegiPartitionConfiguration"/>
+      </set>
+    </property>
+
+    <!-- Bootstrap schemas -->
+<!--    <property name="bootstrapSchemas">
+      <set>
+        <bean class="org.apache.ldap.server.schema.bootstrap.AutofsSchema"/>
+        <bean class="org.apache.ldap.server.schema.bootstrap.CorbaSchema"/>
+        <bean class="org.apache.ldap.server.schema.bootstrap.CoreSchema"/>
+
+
+      </set>
+    </property>
+    -->
+    <!-- Interceptor configurations -->
+    <!--property name="interceptorConfigurations">
+      <list>
+        <bean class="org.apache.ldap.server.configuration.MutableInterceptorConfiguration">
+          <property name="name"><value>normalizationService</value></property>
+          <property name="interceptor">
+            <bean class="org.apache.ldap.server.normalization.NormalizationService" />
+          </property>
+        </bean>
+        <bean class="org.apache.ldap.server.configuration.MutableInterceptorConfiguration">
+          <property name="name"><value>authenticationService</value></property>
+          <property name="interceptor">
+            <bean class="org.apache.ldap.server.authn.AuthenticationService" />
+          </property>
+        </bean>
+
+
+      </list>
+    </property>
+    -->
+  </bean>
+
+  <!-- Additional ContextPartitionConfiguration -->
+  <bean id="acegiPartitionConfiguration" class="org.apache.ldap.server.configuration.MutableDirectoryPartitionConfiguration">
+    <property name="name"><value>acegisecurity</value></property>
+    <property name="suffix"><value>dc=acegisecurity,dc=org</value></property>
+    <property name="indexedAttributes">
+      <set>
+        <value>objectClass</value>
+        <value>ou</value>
+        <value>uid</value>
+      </set>
+    </property>
+    <property name="contextEntry">
+      <value>
+        objectClass: top
+        objectClass: domain
+        objectClass: extensibleObject
+        dc: apache
+      </value>
+    </property>
+  </bean>
+
+  <!-- Custom editors required to launch ApacheDS -->
+  <bean class="org.springframework.beans.factory.config.CustomEditorConfigurer">
+    <property name="customEditors">
+      <map>
+        <entry key="javax.naming.directory.Attributes">
+          <bean class="org.apache.ldap.server.configuration.AttributesPropertyEditor"/>
+        </entry>
+      </map>
+   </property>
+  </bean>
+</beans>

+ 54 - 0
core/src/test/resources/org/acegisecurity/providers/ldap/ldif/acegisecurity.ldif

@@ -0,0 +1,54 @@
+version: 1
+dn: dc=acegisecurity,dc=org
+objectClass: dcObject
+objectClass: organization
+dc: acegisecurity
+description: Acegi Security (Test LDAP DIT)
+o: Monkey Machine Ltd.
+
+dn: ou=people,dc=acegisecurity,dc=org
+objectClass: organizationalUnit
+description: All people in organisation
+ou: people
+
+dn: cn=Ben Alex,ou=people,dc=acegisecurity,dc=org
+objectClass: inetOrgPerson
+objectClass: organizationalPerson
+objectClass: person
+objectClass: top
+cn: Ben Alex
+sn: Alex
+uid: Ben
+userPassword:: e3NoYX1uRkNlYldqeGZhTGJISEcxUWs1VVU0dHJidlE9
+
+dn: uid=bob,ou=people,dc=acegisecurity,dc=org
+objectClass: inetOrgPerson
+objectClass: organizationalPerson
+objectClass: person
+objectClass: top
+cn: Bob Hamilton
+sn: Hamilton
+uid: bob
+userPassword:: Ym9ic3Bhc3N3b3Jk
+
+dn: ou=groups,dc=acegisecurity,dc=org
+objectClass: top
+objectClass: organizationalUnit
+ou: groups
+
+dn: cn=developers,ou=groups,dc=acegisecurity,dc=org
+objectClass: groupOfNames
+objectClass: top
+cn: developers
+description: Acegi Security Developers
+member: uid=bob,ou=people,dc=acegisecurity,dc=org
+member: cn=Ben Alex,ou=people,dc=acegisecurity,dc=org
+o: Acegi Security System for Spring
+ou: developer
+
+dn: cn=managers,ou=groups,dc=acegisecurity,dc=org
+objectClass: groupOfNames
+objectClass: top
+cn: managers
+member: cn=Ben Alex,ou=people,dc=acegisecurity,dc=org
+ou: manager