diff --git a/core/src/main/java/org/acegisecurity/providers/ldap/DefaultInitialDirContextFactory.java b/core/src/main/java/org/acegisecurity/providers/ldap/DefaultInitialDirContextFactory.java new file mode 100644 index 0000000000..424a819f2b --- /dev/null +++ b/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 DirContext references. + *
+ * The directory location is configured using by setting the url property. + * This should be in the form ldap://monkeymachine.co.uk:389/dc=acegisecurity,dc=org. + *
+ *+ * To obtain an initial context, th client calls the newInitialDirContext + * method. There are two signatures - one with no arguments and one which allows + * binding with a specific username and password. + *
+ *+ * The no-args version will bind anonymously or if a manager login has been configured + * using the properties managerDn and managerPassword it will bind as + * that user. + *
+ *+ * Connection pooling is enabled for anonymous or manager connections, but not when binding + * as a specific user. + *
+ * + * @see The Java tutorial's guide to + * Connection Pooling + * + * @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 should not + * 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 ldap://monkeymachine.co.uk:389/dc=acegisecurity,dc=org + * the value will be dc=acegisecurity,dc=org. + * + * @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; + } + +} diff --git a/core/src/main/java/org/acegisecurity/providers/ldap/InitialDirContextFactory.java b/core/src/main/java/org/acegisecurity/providers/ldap/InitialDirContextFactory.java new file mode 100644 index 0000000000..33a4b97097 --- /dev/null +++ b/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(); +} diff --git a/core/src/main/java/org/acegisecurity/providers/ldap/LdapAuthenticationProvider.java b/core/src/main/java/org/acegisecurity/providers/ldap/LdapAuthenticationProvider.java new file mode 100644 index 0000000000..060776e474 --- /dev/null +++ b/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. + * + *+ * 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}. + *
+ * + *+ * 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). + *
+ * + *+ * A custom implementation could obtain the roles from a completely different source, + * for example from a database. + *
+ * + * + * @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 UserDetails object that will be returned by the provider + * once the user has been authenticated. + *+ * The LdapAuthoritiesPopulator will be used to create the granted authorites for the + * user. + *
+ *+ * Can be overridden to customize the mapping of user attributes to additional user information. + *
+ * + * @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; + } +} diff --git a/core/src/main/java/org/acegisecurity/providers/ldap/LdapAuthenticator.java b/core/src/main/java/org/acegisecurity/providers/ldap/LdapAuthenticator.java new file mode 100644 index 0000000000..dc251dcf7d --- /dev/null +++ b/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. + *+ * The LdapAuthenticationProvider calls this interface to authenticate a user + * and obtain the information for that user from the directory. + *
+ * + * + * @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 (not 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); +} diff --git a/core/src/main/java/org/acegisecurity/providers/ldap/LdapAuthoritiesPopulator.java b/core/src/main/java/org/acegisecurity/providers/ldap/LdapAuthoritiesPopulator.java new file mode 100644 index 0000000000..4bb12004a7 --- /dev/null +++ b/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. + *+ * Used by the LdapAuthenticationProvider once a user has been + * authenticated to create the final user details object. + *
+ * + * @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); + +} diff --git a/core/src/main/java/org/acegisecurity/providers/ldap/LdapDataAccessException.java b/core/src/main/java/org/acegisecurity/providers/ldap/LdapDataAccessException.java new file mode 100644 index 0000000000..8b669205d9 --- /dev/null +++ b/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); + } +} diff --git a/core/src/main/java/org/acegisecurity/providers/ldap/LdapUserDetails.java b/core/src/main/java/org/acegisecurity/providers/ldap/LdapUserDetails.java new file mode 100644 index 0000000000..424d590973 --- /dev/null +++ b/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. + *+ * An instance may be created as the result of a search, or when user information + * is retrieved during authentication. + *
+ *+ * An instance of this class will be used by the LdapAuthenticationProvider + * to construct the final user details object that it returns. + *
+ * + * @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(); + } +} diff --git a/core/src/main/java/org/acegisecurity/providers/ldap/LdapUtils.java b/core/src/main/java/org/acegisecurity/providers/ldap/LdapUtils.java new file mode 100644 index 0000000000..aabb4ca0aa --- /dev/null +++ b/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. + *+ * 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". + *
+ * + * @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); + } +} diff --git a/core/src/main/java/org/acegisecurity/providers/ldap/authenticator/AbstractLdapAuthenticator.java b/core/src/main/java/org/acegisecurity/providers/ldap/authenticator/AbstractLdapAuthenticator.java new file mode 100644 index 0000000000..01999bd1fa --- /dev/null +++ b/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 InitialDirContextfactory. + */ + 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."); + } +} diff --git a/core/src/main/java/org/acegisecurity/providers/ldap/authenticator/BindAuthenticator.java b/core/src/main/java/org/acegisecurity/providers/ldap/authenticator/BindAuthenticator.java new file mode 100644 index 0000000000..b68d041112 --- /dev/null +++ b/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; + } + +} diff --git a/core/src/main/java/org/acegisecurity/providers/ldap/authenticator/FilterBasedLdapUserSearch.java b/core/src/main/java/org/acegisecurity/providers/ldap/authenticator/FilterBasedLdapUserSearch.java new file mode 100644 index 0000000000..019a4bb974 --- /dev/null +++ b/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 search methods in {@link javax.naming.directory.DirContext DirContext} + * for more information. + *+ * In this case, the username is the only parameter. + *
+ * Possible examples are: + *+ * 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. + *
+ * + * @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); + +} diff --git a/core/src/main/java/org/acegisecurity/providers/ldap/authenticator/PasswordComparisonAuthenticator.java b/core/src/main/java/org/acegisecurity/providers/ldap/authenticator/PasswordComparisonAuthenticator.java new file mode 100644 index 0000000000..0a1c3607aa --- /dev/null +++ b/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. + *+ * 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. + *
+ *+ * 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}. + *
+ * + * @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; + } +} diff --git a/core/src/main/java/org/acegisecurity/providers/ldap/populator/DefaultLdapAuthoritiesPopulator.java b/core/src/main/java/org/acegisecurity/providers/ldap/populator/DefaultLdapAuthoritiesPopulator.java new file mode 100644 index 0000000000..e2dd17635d --- /dev/null +++ b/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. + *+ * It obtains roles by + *
+ * If the userRolesAttributes property is set, any matching + * attributes amongst those retrieved for the user will have their values added + * to the list of roles. + * If userRolesAttributes is null, no attributes will be mapped to roles. + *
+ *+ * A typical group search scenario would be where each group/role is specified using + * the groupOfNames (or groupOfUniqueNames) LDAP objectClass + * and the user's DN is listed in the member (or uniqueMember) attribute + * to indicate that they should be assigned that role. The following LDIF sample + * has the groups stored under the DN ou=groups,dc=acegisecurity,dc=org + * and a group called "developers" with "ben" and "marissa" as members: + * + *
+ * 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 + *+ * + *
+ * The group search is performed within a DN specified by the groupSearchBase + * property, which should be relative to the root DN of its InitialDirContextFactory. + * If the search base is null, group searching is disabled. The filter used in the search is defined by the + * groupSearchFilter 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 groupRoleAttribute property (the default is "cn"). + *
+ *+ * <bean id="ldapAuthoritiesPopulator" class="org.acegisecurity.providers.ldap.populator.DefaultLdapAuthoritiesPopulator"> + * TODO + * </bean> + *
+ * + * + * @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."); + } + } +} diff --git a/core/src/test/java/org/acegisecurity/providers/ldap/AbstractLdapServerTestCase.java b/core/src/test/java/org/acegisecurity/providers/ldap/AbstractLdapServerTestCase.java new file mode 100644 index 0000000000..3e0d3a4c0d --- /dev/null +++ b/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); + } +} diff --git a/core/src/test/java/org/acegisecurity/providers/ldap/InitialDirContextFactoryTests.java b/core/src/test/java/org/acegisecurity/providers/ldap/InitialDirContextFactoryTests.java new file mode 100644 index 0000000000..ffe263eb33 --- /dev/null +++ b/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()); + } + +} \ No newline at end of file diff --git a/core/src/test/java/org/acegisecurity/providers/ldap/LdapAuthenticationProviderTests.java b/core/src/test/java/org/acegisecurity/providers/ldap/LdapAuthenticationProviderTests.java new file mode 100644 index 0000000000..c8e6c0801f --- /dev/null +++ b/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."); + } + } +} diff --git a/core/src/test/java/org/acegisecurity/providers/ldap/LdapTestServer.java b/core/src/test/java/org/acegisecurity/providers/ldap/LdapTestServer.java new file mode 100644 index 0000000000..ec101c62ca --- /dev/null +++ b/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(); + } +} diff --git a/core/src/test/java/org/acegisecurity/providers/ldap/authenticator/BindAuthenticatorTests.java b/core/src/test/java/org/acegisecurity/providers/ldap/authenticator/BindAuthenticatorTests.java new file mode 100644 index 0000000000..1879440fee --- /dev/null +++ b/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) { +// } +// } +} + diff --git a/core/src/test/java/org/acegisecurity/providers/ldap/authenticator/FilterBasedLdapUserSearchTests.java b/core/src/test/java/org/acegisecurity/providers/ldap/authenticator/FilterBasedLdapUserSearchTests.java new file mode 100644 index 0000000000..bde70301bd --- /dev/null +++ b/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()); + } +} diff --git a/core/src/test/java/org/acegisecurity/providers/ldap/authenticator/MockUserSearch.java b/core/src/test/java/org/acegisecurity/providers/ldap/authenticator/MockUserSearch.java new file mode 100644 index 0000000000..7c2fae09a3 --- /dev/null +++ b/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; + } +} diff --git a/core/src/test/java/org/acegisecurity/providers/ldap/authenticator/PasswordComparisonAuthenticatorMockTests.java b/core/src/test/java/org/acegisecurity/providers/ldap/authenticator/PasswordComparisonAuthenticatorMockTests.java new file mode 100644 index 0000000000..334ceb7879 --- /dev/null +++ b/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; + } + } + +} diff --git a/core/src/test/java/org/acegisecurity/providers/ldap/authenticator/PasswordComparisonAuthenticatorTests.java b/core/src/test/java/org/acegisecurity/providers/ldap/authenticator/PasswordComparisonAuthenticatorTests.java new file mode 100644 index 0000000000..32d13fef86 --- /dev/null +++ b/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) { + } + } +} \ No newline at end of file diff --git a/core/src/test/java/org/acegisecurity/providers/ldap/populator/DefaultLdapAuthoritiesPopulatorTests.java b/core/src/test/java/org/acegisecurity/providers/ldap/populator/DefaultLdapAuthoritiesPopulatorTests.java new file mode 100644 index 0000000000..2ac3cb2d4c --- /dev/null +++ b/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")); + } +} diff --git a/core/src/test/resources/org/acegisecurity/providers/ldap/apacheds-context.xml b/core/src/test/resources/org/acegisecurity/providers/ldap/apacheds-context.xml new file mode 100644 index 0000000000..9b1259a8d2 --- /dev/null +++ b/core/src/test/resources/org/acegisecurity/providers/ldap/apacheds-context.xml @@ -0,0 +1,101 @@ + + + + +