View Javadoc

1   /* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
2    *
3    * Licensed under the Apache License, Version 2.0 (the "License");
4    * you may not use this file except in compliance with the License.
5    * You may obtain a copy of the License at
6    *
7    *     http://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
14   */
15  
16  package org.acegisecurity.ldap;
17  
18  import org.acegisecurity.AcegiMessageSource;
19  import org.acegisecurity.BadCredentialsException;
20  
21  import org.apache.commons.logging.Log;
22  import org.apache.commons.logging.LogFactory;
23  
24  import org.springframework.context.MessageSource;
25  import org.springframework.context.MessageSourceAware;
26  import org.springframework.context.support.MessageSourceAccessor;
27  
28  import org.springframework.util.Assert;
29  
30  import java.util.Hashtable;
31  import java.util.Map;
32  import java.util.StringTokenizer;
33  
34  import javax.naming.CommunicationException;
35  import javax.naming.Context;
36  import javax.naming.NamingException;
37  import javax.naming.OperationNotSupportedException;
38  import javax.naming.ldap.InitialLdapContext;
39  import javax.naming.directory.DirContext;
40  import javax.naming.directory.InitialDirContext;
41  
42  
43  /**
44   * Encapsulates the information for connecting to an LDAP server and provides an access point for obtaining
45   * <tt>DirContext</tt> references.
46   * <p>
47   * The directory location is configured using by setting the constructor argument
48   * <tt>providerUrl</tt>. This should be in the form <tt>ldap://monkeymachine.co.uk:389/dc=acegisecurity,dc=org</tt>.
49   * The Sun JNDI provider also supports lists of space-separated URLs, each of which will be tried in turn until a
50   * connection is obtained.
51   * </p>
52   * <p>To obtain an initial context, the client calls the <tt>newInitialDirContext</tt> method. There are two
53   * signatures - one with no arguments and one which allows binding with a specific username and password.
54   * </p>
55   * <p>The no-args version will bind anonymously unless a manager login has been configured using the properties
56   * <tt>managerDn</tt> and <tt>managerPassword</tt>, in which case it will bind as the manager user.</p>
57   * <p>Connection pooling is enabled by default for anonymous or manager connections, but not when binding as a
58   * specific user.</p>
59   *
60   * @author Robert Sanders
61   * @author Luke Taylor
62   * @version $Id: DefaultInitialDirContextFactory.java 1784 2007-02-24 21:00:24Z luke_t $
63   *
64   * @see <a href="http://java.sun.com/products/jndi/tutorial/ldap/connect/pool.html">The Java tutorial's guide to LDAP
65   *      connection pooling</a>
66   */
67  public class DefaultInitialDirContextFactory implements InitialDirContextFactory, MessageSourceAware {
68      //~ Static fields/initializers =====================================================================================
69  
70      private static final Log logger = LogFactory.getLog(DefaultInitialDirContextFactory.class);
71      private static final String CONNECTION_POOL_KEY = "com.sun.jndi.ldap.connect.pool";
72      private static final String AUTH_TYPE_NONE = "none";
73  
74      //~ Instance fields ================================================================================================
75  
76      /** Allows extra environment variables to be added at config time. */
77      private Map extraEnvVars = null;
78      protected MessageSourceAccessor messages = AcegiMessageSource.getAccessor();
79  
80      /** Type of authentication within LDAP; default is simple. */
81      private String authenticationType = "simple";
82  
83      /**
84       * The INITIAL_CONTEXT_FACTORY used to create the JNDI Factory. Default is
85       * "com.sun.jndi.ldap.LdapCtxFactory"; you <b>should not</b> need to set this unless you have unusual needs.
86       */
87      private String initialContextFactory = "com.sun.jndi.ldap.LdapCtxFactory";
88  
89      /**
90       * If your LDAP server does not allow anonymous searches then you will need to provide a "manager" user's
91       * DN to log in with.
92       */
93      private String managerDn = null;
94  
95      /** The manager user's password. */
96      private String managerPassword = "manager_password_not_set";
97  
98      /** The LDAP url of the server (and root context) to connect to. */
99      private String providerUrl;
100 
101     /**
102      * The root DN. This is worked out from the url. It is used by client classes when forming a full DN for
103      * bind authentication (for example).
104      */
105     private String rootDn = null;
106 
107     /**
108      * Use the LDAP Connection pool; if true, then the LDAP environment property
109      * "com.sun.jndi.ldap.connect.pool" is added to any other JNDI properties.
110      */
111     private boolean useConnectionPool = true;
112 
113     /** Set to true for ldap v3 compatible servers */
114     private boolean useLdapContext = false;
115 
116     //~ Constructors ===================================================================================================
117 
118     /**
119      * Create and initialize an instance to the LDAP url provided
120      *
121      * @param providerUrl a String of the form <code>ldap://localhost:389/base_dn<code>
122      */
123     public DefaultInitialDirContextFactory(String providerUrl) {
124         this.setProviderUrl(providerUrl);
125     }
126 
127     //~ Methods ========================================================================================================
128 
129     /**
130      * Set the LDAP url
131      *
132      * @param providerUrl a String of the form <code>ldap://localhost:389/base_dn<code>
133      */
134     private void setProviderUrl(String providerUrl) {
135         Assert.hasLength(providerUrl, "An LDAP connection URL must be supplied.");
136 
137         this.providerUrl = providerUrl;
138 
139         StringTokenizer st = new StringTokenizer(providerUrl);
140 
141         // Work out rootDn from the first URL and check that the other URLs (if any) match
142         while (st.hasMoreTokens()) {
143             String url = st.nextToken();
144             String urlRootDn = LdapUtils.parseRootDnFromUrl(url);
145 
146             logger.info(" URL '" + url + "', root DN is '" + urlRootDn + "'");
147 
148             if (rootDn == null) {
149                 rootDn = urlRootDn;
150             } else if (!rootDn.equals(urlRootDn)) {
151                 throw new IllegalArgumentException("Root DNs must be the same when using multiple URLs");
152             }
153         }
154 
155         // This doesn't necessarily hold for embedded servers.
156         //Assert.isTrue(uri.getScheme().equals("ldap"), "Ldap URL must start with 'ldap://'");
157     }
158 
159     /**
160      * Get the LDAP url
161      *
162      * @return the url
163      */
164     private String getProviderUrl() {
165         return providerUrl;
166     }
167 
168     private InitialDirContext connect(Hashtable env) {
169         if (logger.isDebugEnabled()) {
170             Hashtable envClone = (Hashtable) env.clone();
171 
172             if (envClone.containsKey(Context.SECURITY_CREDENTIALS)) {
173                 envClone.put(Context.SECURITY_CREDENTIALS, "******");
174             }
175 
176             logger.debug("Creating InitialDirContext with environment " + envClone);
177         }
178 
179         try {
180             return useLdapContext ? new InitialLdapContext(env, null) : new InitialDirContext(env);
181         } catch (NamingException ne) {
182             if ((ne instanceof javax.naming.AuthenticationException)
183                     || (ne instanceof OperationNotSupportedException)) {
184                 throw new BadCredentialsException(messages.getMessage("DefaultIntitalDirContextFactory.badCredentials",
185                         "Bad credentials"), ne);
186             }
187 
188             if (ne instanceof CommunicationException) {
189                 throw new LdapDataAccessException(messages.getMessage(
190                         "DefaultIntitalDirContextFactory.communicationFailure", "Unable to connect to LDAP server"), ne);
191             }
192 
193             throw new LdapDataAccessException(messages.getMessage(
194                     "DefaultIntitalDirContextFactory.unexpectedException",
195                     "Failed to obtain InitialDirContext due to unexpected exception"), ne);
196         }
197     }
198 
199     /**
200      * Sets up the environment parameters for creating a new context.
201      *
202      * @return the Hashtable describing the base DirContext that will be created, minus the username/password if any.
203      */
204     protected Hashtable getEnvironment() {
205         Hashtable env = new Hashtable();
206 
207         env.put(Context.SECURITY_AUTHENTICATION, authenticationType);
208         env.put(Context.INITIAL_CONTEXT_FACTORY, initialContextFactory);
209         env.put(Context.PROVIDER_URL, getProviderUrl());
210 
211         if (useConnectionPool) {
212             env.put(CONNECTION_POOL_KEY, "true");
213         }
214 
215         if ((extraEnvVars != null) && (extraEnvVars.size() > 0)) {
216             env.putAll(extraEnvVars);
217         }
218 
219         return env;
220     }
221 
222     /**
223      * Returns the root DN of the configured provider URL. For example, if the URL is
224      * <tt>ldap://monkeymachine.co.uk:389/dc=acegisecurity,dc=org</tt> the value will be
225      * <tt>dc=acegisecurity,dc=org</tt>.
226      *
227      * @return the root DN calculated from the path of the LDAP url.
228      */
229     public String getRootDn() {
230         return rootDn;
231     }
232 
233     /**
234      * Connects anonymously unless a manager user has been specified, in which case it will bind as the
235      * manager.
236      *
237      * @return the resulting context object.
238      */
239     public DirContext newInitialDirContext() {
240         if (managerDn != null) {
241             return newInitialDirContext(managerDn, managerPassword);
242         }
243 
244         Hashtable env = getEnvironment();
245         env.put(Context.SECURITY_AUTHENTICATION, AUTH_TYPE_NONE);
246 
247         return connect(env);
248     }
249 
250     public DirContext newInitialDirContext(String username, String password) {
251         Hashtable env = getEnvironment();
252 
253         // Don't pool connections for individual users
254         if (!username.equals(managerDn)) {
255             env.remove(CONNECTION_POOL_KEY);
256         }
257 
258         env.put(Context.SECURITY_PRINCIPAL, username);
259         env.put(Context.SECURITY_CREDENTIALS, password);
260 
261         return connect(env);
262     }
263 
264     public void setAuthenticationType(String authenticationType) {
265         Assert.hasLength(authenticationType, "LDAP Authentication type must not be empty or null");
266         this.authenticationType = authenticationType;
267     }
268 
269     /**
270      * Sets any custom environment variables which will be added to the those returned
271      * by the <tt>getEnvironment</tt> method.
272      *
273      * @param extraEnvVars extra environment variables to be added at config time.
274      */
275     public void setExtraEnvVars(Map extraEnvVars) {
276         Assert.notNull(extraEnvVars, "Extra environment map cannot be null.");
277         this.extraEnvVars = extraEnvVars;
278     }
279 
280     public void setInitialContextFactory(String initialContextFactory) {
281         Assert.hasLength(initialContextFactory, "Initial context factory name cannot be empty or null");
282         this.initialContextFactory = initialContextFactory;
283     }
284 
285     /**
286      * Sets the directory user to authenticate as when obtaining a context using the
287      * <tt>newInitialDirContext()</tt> method.
288      * If no name is supplied then the context will be obtained anonymously.
289      *
290      * @param managerDn The name of the "manager" user for default authentication.
291      */
292     public void setManagerDn(String managerDn) {
293         Assert.hasLength(managerDn, "Manager user name  cannot be empty or null.");
294         this.managerDn = managerDn;
295     }
296 
297     /**
298      * Sets the password which will be used in combination with the manager DN.
299      *
300      * @param managerPassword The "manager" user's password.
301      */
302     public void setManagerPassword(String managerPassword) {
303         Assert.hasLength(managerPassword, "Manager password must not be empty or null.");
304         this.managerPassword = managerPassword;
305     }
306 
307     public void setMessageSource(MessageSource messageSource) {
308         this.messages = new MessageSourceAccessor(messageSource);
309     }
310 
311     /**
312      * Connection pooling is enabled by default for anonymous or "manager" connections when using the default
313      * Sun provider. To disable all connection pooling, set this property to false.
314      *
315      * @param useConnectionPool whether to pool connections for non-specific users.
316      */
317     public void setUseConnectionPool(boolean useConnectionPool) {
318         this.useConnectionPool = useConnectionPool;
319     }
320 
321     public void setUseLdapContext(boolean useLdapContext) {
322         this.useLdapContext = useLdapContext;
323     }
324 }