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