|
@@ -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;
|
|
|
+ }
|
|
|
+}
|