View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *     http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.shiro.realm.ldap;
20  
21  import org.apache.shiro.util.StringUtils;
22  import org.slf4j.Logger;
23  import org.slf4j.LoggerFactory;
24  
25  import javax.naming.Context;
26  import javax.naming.NamingException;
27  import javax.naming.ldap.InitialLdapContext;
28  import javax.naming.ldap.LdapContext;
29  import java.util.HashMap;
30  import java.util.Hashtable;
31  import java.util.Map;
32  
33  /**
34   * {@link LdapContextFactory} implementation using the default Sun/Oracle JNDI Ldap API, utilizing JNDI
35   * environment properties and an {@link javax.naming.InitialContext}.
36   * <h2>Configuration</h2>
37   * This class basically wraps a default template JNDI environment properties Map.  This properties map is the base
38   * configuration template used to acquire JNDI {@link LdapContext} connections at runtime.  The
39   * {@link #getLdapContext(Object, Object)} method implementation merges this default template with other properties
40   * accessible at runtime only (for example per-method principals and credentials).  The constructed runtime map is the
41   * one used to acquire the {@link LdapContext}.
42   * <p/>
43   * The template can be configured directly via the {@link #getEnvironment()}/{@link #setEnvironment(java.util.Map)}
44   * properties directly if necessary, but it is usually more convenient to use the supporting wrapper get/set methods
45   * for various environment properties.  These wrapper methods interact with the environment
46   * template on your behalf, leaving your configuration cleaner and easier to understand.
47   * <p/>
48   * For example, consider the following two identical configurations:
49   * <pre>
50   * [main]
51   * ldapRealm = org.apache.shiro.realm.ldap.JndiLdapRealm
52   * ldapRealm.contextFactory.url = ldap://localhost:389
53   * ldapRealm.contextFactory.authenticationMechanism = DIGEST-MD5
54   * </pre>
55   * and
56   * <pre>
57   * [main]
58   * ldapRealm = org.apache.shiro.realm.ldap.JndiLdapRealm
59   * ldapRealm.contextFactory.environment[java.naming.provider.url] = ldap://localhost:389
60   * ldapRealm.contextFactory.environment[java.naming.security.authentication] = DIGEST-MD5
61   * </pre>
62   * As you can see, the 2nd configuration block is a little more difficult to read and also requires knowledge
63   * of the underlying JNDI Context property keys.  The first is easier to read and understand.
64   * <p/>
65   * Note that occasionally it will be necessary to use the latter configuration style to set environment properties
66   * where no corresponding wrapper method exists.  In this case, the hybrid approach is still a little easier to read.
67   * For example:
68   * <pre>
69   * [main]
70   * ldapRealm = org.apache.shiro.realm.ldap.JndiLdapRealm
71   * ldapRealm.contextFactory.url = ldap://localhost:389
72   * ldapRealm.contextFactory.authenticationMechanism = DIGEST-MD5
73   * ldapRealm.contextFactory.environment[some.other.obscure.jndi.key] = some value
74   * </pre>
75   *
76   * @since 1.1
77   */
78  public class JndiLdapContextFactory implements LdapContextFactory {
79  
80      /*-------------------------------------------
81       |             C O N S T A N T S            |
82       ===========================================*/
83      /**
84       * The Sun LDAP property used to enable connection pooling.  This is used in the default implementation
85       * to enable LDAP connection pooling.
86       */
87      protected static final String SUN_CONNECTION_POOLING_PROPERTY = "com.sun.jndi.ldap.connect.pool";
88      protected static final String DEFAULT_CONTEXT_FACTORY_CLASS_NAME = "com.sun.jndi.ldap.LdapCtxFactory";
89      protected static final String SIMPLE_AUTHENTICATION_MECHANISM_NAME = "simple";
90      protected static final String DEFAULT_REFERRAL = "follow";
91  
92      private static final Logger log = LoggerFactory.getLogger(JndiLdapContextFactory.class);
93  
94      /*-------------------------------------------
95       |    I N S T A N C E   V A R I A B L E S   |
96       ============================================*/
97      private Map<String, Object> environment;
98      private boolean poolingEnabled;
99      private String systemPassword;
100     private String systemUsername;
101 
102     /*-------------------------------------------
103      |         C O N S T R U C T O R S          |
104      ===========================================*/
105 
106     /**
107      * Default no-argument constructor that initializes the backing {@link #getEnvironment() environment template} with
108      * the {@link #setContextFactoryClassName(String) contextFactoryClassName} equal to
109      * {@code com.sun.jndi.ldap.LdapCtxFactory} (the Sun/Oracle default) and the default
110      * {@link #setReferral(String) referral} behavior to {@code follow}.
111      */
112     public JndiLdapContextFactory() {
113         this.environment = new HashMap<String, Object>();
114         setContextFactoryClassName(DEFAULT_CONTEXT_FACTORY_CLASS_NAME);
115         setReferral(DEFAULT_REFERRAL);
116         poolingEnabled = true;
117     }
118 
119     /*-------------------------------------------
120      |  A C C E S S O R S / M O D I F I E R S   |
121      ===========================================*/
122 
123     /**
124      * Sets the type of LDAP authentication mechanism to use when connecting to the LDAP server.
125      * This is a wrapper method for setting the JNDI {@link #getEnvironment() environment template}'s
126      * {@link Context#SECURITY_AUTHENTICATION} property.
127      * <p/>
128      * "none" (i.e. anonymous) and "simple" authentications are supported automatically and don't need to be configured
129      * via this property.  However, if you require a different mechanism, such as a SASL or External mechanism, you
130      * must configure that explicitly via this property.  See the
131      * <a href="http://download-llnw.oracle.com/javase/tutorial/jndi/ldap/auth_mechs.html">JNDI LDAP
132      * Authentication Mechanisms</a> for more information.
133      *
134      * @param authenticationMechanism the type of LDAP authentication to perform.
135      * @see <a href="http://download-llnw.oracle.com/javase/tutorial/jndi/ldap/auth_mechs.html">
136      *      http://download-llnw.oracle.com/javase/tutorial/jndi/ldap/auth_mechs.html</a>
137      */
138     public void setAuthenticationMechanism(String authenticationMechanism) {
139         setEnvironmentProperty(Context.SECURITY_AUTHENTICATION, authenticationMechanism);
140     }
141 
142     /**
143      * Returns the type of LDAP authentication mechanism to use when connecting to the LDAP server.
144      * This is a wrapper method for getting the JNDI {@link #getEnvironment() environment template}'s
145      * {@link Context#SECURITY_AUTHENTICATION} property.
146      * <p/>
147      * If this property remains un-configured (i.e. {@code null} indicating the
148      * {@link #setAuthenticationMechanism(String)} method wasn't used), this indicates that the default JNDI
149      * "none" (anonymous) and "simple" authentications are supported automatically.  Any non-null value returned
150      * represents an explicitly configured mechanism (e.g. a SASL or external mechanism). See the
151      * <a href="http://download-llnw.oracle.com/javase/tutorial/jndi/ldap/auth_mechs.html">JNDI LDAP
152      * Authentication Mechanisms</a> for more information.
153      *
154      * @return the type of LDAP authentication mechanism to use when connecting to the LDAP server.
155      * @see <a href="http://download-llnw.oracle.com/javase/tutorial/jndi/ldap/auth_mechs.html">
156      *      http://download-llnw.oracle.com/javase/tutorial/jndi/ldap/auth_mechs.html</a>
157      */
158     public String getAuthenticationMechanism() {
159         return (String) getEnvironmentProperty(Context.SECURITY_AUTHENTICATION);
160     }
161 
162     /**
163      * The name of the ContextFactory class to use. This defaults to the SUN LDAP JNDI implementation
164      * but can be overridden to use custom LDAP factories.
165      * <p/>
166      * This is a wrapper method for setting the JNDI environment's {@link Context#INITIAL_CONTEXT_FACTORY} property.
167      *
168      * @param contextFactoryClassName the context factory that should be used.
169      */
170     public void setContextFactoryClassName(String contextFactoryClassName) {
171         setEnvironmentProperty(Context.INITIAL_CONTEXT_FACTORY, contextFactoryClassName);
172     }
173 
174     /**
175      * Sets the name of the ContextFactory class to use. This defaults to the SUN LDAP JNDI implementation
176      * but can be overridden to use custom LDAP factories.
177      * <p/>
178      * This is a wrapper method for getting the JNDI environment's {@link Context#INITIAL_CONTEXT_FACTORY} property.
179      *
180      * @return the name of the ContextFactory class to use.
181      */
182     public String getContextFactoryClassName() {
183         return (String) getEnvironmentProperty(Context.INITIAL_CONTEXT_FACTORY);
184     }
185 
186     /**
187      * Returns the base JNDI environment template to use when acquiring an LDAP connection (an {@link LdapContext}).
188      * This property is the base configuration template to use for all connections.  This template is then
189      * merged with appropriate runtime values as necessary in the
190      * {@link #getLdapContext(Object, Object)} implementation.  The merged environment instance is what is used to
191      * acquire the {@link LdapContext} at runtime.
192      * <p/>
193      * Most other get/set methods in this class act as thin proxy wrappers that interact with this property.  The
194      * benefit of using them is you have an easier-to-use configuration mechanism compared to setting map properties
195      * based on JNDI context keys.
196      *
197      * @return the base JNDI environment template to use when acquiring an LDAP connection (an {@link LdapContext})
198      */
199     public Map getEnvironment() {
200         return this.environment;
201     }
202 
203     /**
204      * Sets the base JNDI environment template to use when acquiring LDAP connections.  It is typically more common
205      * to use the other get/set methods in this class to set individual environment settings rather than use
206      * this method, but it is available for advanced users that want full control over the base JNDI environment
207      * settings.
208      * <p/>
209      * Note that this template only represents the base/default environment settings.  It is then merged with
210      * appropriate runtime values as necessary in the {@link #getLdapContext(Object, Object)} implementation.
211      * The merged environment instance is what is used to acquire the connection ({@link LdapContext}) at runtime.
212      *
213      * @param env the base JNDI environment template to use when acquiring LDAP connections.
214      */
215     @SuppressWarnings({"unchecked"})
216     public void setEnvironment(Map env) {
217         this.environment = env;
218     }
219 
220     /**
221      * Returns the environment property value bound under the specified key.
222      *
223      * @param name the name of the environment property
224      * @return the property value or {@code null} if the value has not been set.
225      */
226     private Object getEnvironmentProperty(String name) {
227         return this.environment.get(name);
228     }
229 
230     /**
231      * Will apply the value to the environment attribute if and only if the value is not null or empty.  If it is
232      * null or empty, the corresponding environment attribute will be removed.
233      *
234      * @param name  the environment property key
235      * @param value the environment property value.  A null/empty value will trigger removal.
236      */
237     private void setEnvironmentProperty(String name, String value) {
238         if (StringUtils.hasText(value)) {
239             this.environment.put(name, value);
240         } else {
241             this.environment.remove(name);
242         }
243     }
244 
245     /**
246      * Returns whether or not connection pooling should be used when possible and appropriate.  This property is NOT
247      * backed by the {@link #getEnvironment() environment template} like most other properties in this class.  It
248      * is a flag to indicate that pooling is preferred.  The default value is {@code true}.
249      * <p/>
250      * However, pooling will only actually be enabled if this property is {@code true} <em>and</em> the connection
251      * being created is for the {@link #getSystemUsername() systemUsername} user.  Connection pooling is not used for
252      * general authentication attempts by application end-users because the probability of re-use for that same
253      * user-specific connection after an authentication attempt is extremely low.
254      * <p/>
255      * If this attribute is {@code true} and it has been determined that the connection is being made with the
256      * {@link #getSystemUsername() systemUsername}, the
257      * {@link #getLdapContext(Object, Object)} implementation will set the Sun/Oracle-specific
258      * {@code com.sun.jndi.ldap.connect.pool} environment property to &quot;{@code true}&quot;.  This means setting
259      * this property is only likely to work if using the Sun/Oracle default context factory class (i.e. not using
260      * a custom {@link #getContextFactoryClassName() contextFactoryClassName}).
261      *
262      * @return whether or not connection pooling should be used when possible and appropriate
263      */
264     public boolean isPoolingEnabled() {
265         return poolingEnabled;
266     }
267 
268     /**
269      * Sets whether or not connection pooling should be used when possible and appropriate.  This property is NOT
270      * a wrapper to the {@link #getEnvironment() environment template} like most other properties in this class.  It
271      * is a flag to indicate that pooling is preferred.  The default value is {@code true}.
272      * <p/>
273      * However, pooling will only actually be enabled if this property is {@code true} <em>and</em> the connection
274      * being created is for the {@link #getSystemUsername() systemUsername} user.  Connection pooling is not used for
275      * general authentication attempts by application end-users because the probability of re-use for that same
276      * user-specific connection after an authentication attempt is extremely low.
277      * <p/>
278      * If this attribute is {@code true} and it has been determined that the connection is being made with the
279      * {@link #getSystemUsername() systemUsername}, the
280      * {@link #getLdapContext(Object, Object)} implementation will set the Sun/Oracle-specific
281      * {@code com.sun.jndi.ldap.connect.pool} environment property to &quot;{@code true}&quot;.  This means setting
282      * this property is only likely to work if using the Sun/Oracle default context factory class (i.e. not using
283      * a custom {@link #getContextFactoryClassName() contextFactoryClassName}).
284      *
285      * @param poolingEnabled whether or not connection pooling should be used when possible and appropriate
286      */
287     public void setPoolingEnabled(boolean poolingEnabled) {
288         this.poolingEnabled = poolingEnabled;
289     }
290 
291     /**
292      * Sets the LDAP referral behavior when creating a connection.  Defaults to {@code follow}.  See the Sun/Oracle LDAP
293      * <a href="http://java.sun.com/products/jndi/tutorial/ldap/referral/jndi.html">referral documentation</a> for more.
294      *
295      * @param referral the referral property.
296      * @see <a href="http://java.sun.com/products/jndi/tutorial/ldap/referral/jndi.html">Referrals in JNDI</a>
297      */
298     public void setReferral(String referral) {
299         setEnvironmentProperty(Context.REFERRAL, referral);
300     }
301 
302     /**
303      * Returns the LDAP referral behavior when creating a connection.  Defaults to {@code follow}.
304      * See the Sun/Oracle LDAP
305      * <a href="http://java.sun.com/products/jndi/tutorial/ldap/referral/jndi.html">referral documentation</a> for more.
306      *
307      * @return the LDAP referral behavior when creating a connection.
308      * @see <a href="http://java.sun.com/products/jndi/tutorial/ldap/referral/jndi.html">Referrals in JNDI</a>
309      */
310     public String getReferral() {
311         return (String) getEnvironmentProperty(Context.REFERRAL);
312     }
313 
314     /**
315      * The LDAP url to connect to. (e.g. ldap://&lt;ldapDirectoryHostname&gt;:&lt;port&gt;).  This must be configured.
316      *
317      * @param url the LDAP url to connect to. (e.g. ldap://&lt;ldapDirectoryHostname&gt;:&lt;port&gt;)
318      */
319     public void setUrl(String url) {
320         setEnvironmentProperty(Context.PROVIDER_URL, url);
321     }
322 
323     /**
324      * Returns the LDAP url to connect to. (e.g. ldap://&lt;ldapDirectoryHostname&gt;:&lt;port&gt;).
325      * This must be configured.
326      *
327      * @return the LDAP url to connect to. (e.g. ldap://&lt;ldapDirectoryHostname&gt;:&lt;port&gt;)
328      */
329     public String getUrl() {
330         return (String) getEnvironmentProperty(Context.PROVIDER_URL);
331     }
332 
333     /**
334      * Sets the password of the {@link #setSystemUsername(String) systemUsername} that will be used when creating an
335      * LDAP connection used for authorization queries.
336      * <p/>
337      * Note that setting this property is not required if the calling LDAP Realm does not perform authorization
338      * checks.
339      *
340      * @param systemPassword the password of the {@link #setSystemUsername(String) systemUsername} that will be used
341      *                       when creating an LDAP connection used for authorization queries.
342      */
343     public void setSystemPassword(String systemPassword) {
344         this.systemPassword = systemPassword;
345     }
346 
347     /**
348      * Returns the password of the {@link #setSystemUsername(String) systemUsername} that will be used when creating an
349      * LDAP connection used for authorization queries.
350      * <p/>
351      * Note that setting this property is not required if the calling LDAP Realm does not perform authorization
352      * checks.
353      *
354      * @return the password of the {@link #setSystemUsername(String) systemUsername} that will be used when creating an
355      *         LDAP connection used for authorization queries.
356      */
357     public String getSystemPassword() {
358         return this.systemPassword;
359     }
360 
361     /**
362      * Sets the system username that will be used when creating an LDAP connection used for authorization queries.
363      * The user must have the ability to query for authorization data for any application user.
364      * <p/>
365      * Note that setting this property is not required if the calling LDAP Realm does not perform authorization
366      * checks.
367      *
368      * @param systemUsername the system username that will be used when creating an LDAP connection used for
369      *                       authorization queries.
370      */
371     public void setSystemUsername(String systemUsername) {
372         this.systemUsername = systemUsername;
373     }
374 
375     /**
376      * Returns the system username that will be used when creating an LDAP connection used for authorization queries.
377      * The user must have the ability to query for authorization data for any application user.
378      * <p/>
379      * Note that setting this property is not required if the calling LDAP Realm does not perform authorization
380      * checks.
381      *
382      * @return the system username that will be used when creating an LDAP connection used for authorization queries.
383      */
384     public String getSystemUsername() {
385         return systemUsername;
386     }
387 
388     /*--------------------------------------------
389     |               M E T H O D S               |
390     ============================================*/
391 
392     /**
393      * This implementation delegates to {@link #getLdapContext(Object, Object)} using the
394      * {@link #getSystemUsername() systemUsername} and {@link #getSystemPassword() systemPassword} properties as
395      * arguments.
396      *
397      * @return the system LdapContext
398      * @throws NamingException if there is a problem connecting to the LDAP directory
399      */
400     public LdapContext getSystemLdapContext() throws NamingException {
401         return getLdapContext((Object)getSystemUsername(), getSystemPassword());
402     }
403 
404     /**
405      * Deprecated - use {@link #getLdapContext(Object, Object)} instead.  This will be removed before Apache Shiro 2.0.
406      *
407      * @param username the username to use when creating the connection.
408      * @param password the password to use when creating the connection.
409      * @return a {@code LdapContext} bound using the given username and password.
410      * @throws javax.naming.NamingException if there is an error creating the context.
411      * @deprecated the {@link #getLdapContext(Object, Object)} method should be used in all cases to ensure more than
412      *             String principals and credentials can be used.  Shiro no longer calls this method - it will be
413      *             removed before the 2.0 release.
414      */
415     @Deprecated
416     public LdapContext getLdapContext(String username, String password) throws NamingException {
417         return getLdapContext((Object) username, password);
418     }
419 
420     /**
421      * Returns {@code true} if LDAP connection pooling should be used when acquiring a connection based on the specified
422      * account principal, {@code false} otherwise.
423      * <p/>
424      * This implementation returns {@code true} only if {@link #isPoolingEnabled()} and the principal equals the
425      * {@link #getSystemUsername()}.  The reasoning behind this is that connection pooling is not desirable for
426      * general authentication attempts by application end-users because the probability of re-use for that same
427      * user-specific connection after an authentication attempt is extremely low.
428      *
429      * @param principal the principal under which the connection will be made
430      * @return {@code true} if LDAP connection pooling should be used when acquiring a connection based on the specified
431      *         account principal, {@code false} otherwise.
432      */
433     protected boolean isPoolingConnections(Object principal) {
434         return isPoolingEnabled() && principal != null && principal.equals(getSystemUsername());
435     }
436 
437     /**
438      * This implementation returns an LdapContext based on the configured JNDI/LDAP environment configuration.
439      * The environnmet (Map) used at runtime is created by merging the default/configured
440      * {@link #getEnvironment() environment template} with some runtime values as necessary (e.g. a principal and
441      * credential available at runtime only).
442      * <p/>
443      * After the merged Map instance is created, the LdapContext connection is
444      * {@link #createLdapContext(java.util.Hashtable) created} and returned.
445      *
446      * @param principal   the principal to use when acquiring a connection to the LDAP directory
447      * @param credentials the credentials (password, X.509 certificate, etc) to use when acquiring a connection to the
448      *                    LDAP directory
449      * @return the acquired {@code LdapContext} connection bound using the specified principal and credentials.
450      * @throws NamingException
451      * @throws IllegalStateException
452      */
453     public LdapContext getLdapContext(Object principal, Object credentials) throws NamingException,
454             IllegalStateException {
455 
456         String url = getUrl();
457         if (url == null) {
458             throw new IllegalStateException("An LDAP URL must be specified of the form ldap://<hostname>:<port>");
459         }
460 
461         //copy the environment template into the runtime instance that will be further edited based on
462         //the method arguments and other class attributes.
463         Hashtable<String, Object> env = new Hashtable<String, Object>(this.environment);
464 
465         Object authcMech = getAuthenticationMechanism();
466         if (authcMech == null && (principal != null || credentials != null)) {
467             //authenticationMechanism has not been set, but either a principal and/or credentials were
468             //supplied, indicating that at least a 'simple' authentication attempt is indeed occurring - the Shiro
469             //end-user just didn't configure it explicitly.  So we set it to be 'simple' here as a convenience;
470             //the Sun provider implementation already does this same logic, but by repeating that logic here, we ensure
471             //this convenience exists regardless of provider implementation):
472             env.put(Context.SECURITY_AUTHENTICATION, SIMPLE_AUTHENTICATION_MECHANISM_NAME);
473         }
474         if (principal != null) {
475             env.put(Context.SECURITY_PRINCIPAL, principal);
476         }
477         if (credentials != null) {
478             env.put(Context.SECURITY_CREDENTIALS, credentials);
479         }
480 
481         boolean pooling = isPoolingConnections(principal);
482         if (pooling) {
483             env.put(SUN_CONNECTION_POOLING_PROPERTY, "true");
484         }
485 
486         if (log.isDebugEnabled()) {
487             log.debug("Initializing LDAP context using URL [{}] and principal [{}] with pooling {}",
488                     new Object[]{url, principal, (pooling ? "enabled" : "disabled")});
489         }
490 
491         return createLdapContext(env);
492     }
493 
494     /**
495      * Creates and returns a new {@link javax.naming.ldap.InitialLdapContext} instance.  This method exists primarily
496      * to support testing where a mock LdapContext can be returned instead of actually creating a connection, but
497      * subclasses are free to provide a different implementation if necessary.
498      *
499      * @param env the JNDI environment settings used to create the LDAP connection
500      * @return an LdapConnection
501      * @throws NamingException if a problem occurs creating the connection
502      */
503     protected LdapContext createLdapContext(Hashtable env) throws NamingException {
504         return new InitialLdapContext(env, null);
505     }
506 
507 }