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

This commit is contained in:
Luke Taylor 2006-04-16 13:56:36 +00:00
parent 0c1ab7f98c
commit 7f24e209a6
8 changed files with 797 additions and 0 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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