001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.apache.shiro.realm.ldap;
020
021import org.apache.shiro.util.StringUtils;
022import org.slf4j.Logger;
023import org.slf4j.LoggerFactory;
024
025import javax.naming.Context;
026import javax.naming.NamingException;
027import javax.naming.ldap.InitialLdapContext;
028import javax.naming.ldap.LdapContext;
029import java.util.HashMap;
030import java.util.Hashtable;
031import java.util.Map;
032
033/**
034 * {@link LdapContextFactory} implementation using the default Sun/Oracle JNDI Ldap API, utilizing JNDI
035 * environment properties and an {@link javax.naming.InitialContext}.
036 * <h2>Configuration</h2>
037 * This class basically wraps a default template JNDI environment properties Map.  This properties map is the base
038 * configuration template used to acquire JNDI {@link LdapContext} connections at runtime.  The
039 * {@link #getLdapContext(Object, Object)} method implementation merges this default template with other properties
040 * accessible at runtime only (for example per-method principals and credentials).  The constructed runtime map is the
041 * one used to acquire the {@link LdapContext}.
042 * <p/>
043 * The template can be configured directly via the {@link #getEnvironment()}/{@link #setEnvironment(java.util.Map)}
044 * properties directly if necessary, but it is usually more convenient to use the supporting wrapper get/set methods
045 * for various environment properties.  These wrapper methods interact with the environment
046 * template on your behalf, leaving your configuration cleaner and easier to understand.
047 * <p/>
048 * For example, consider the following two identical configurations:
049 * <pre>
050 * [main]
051 * ldapRealm = org.apache.shiro.realm.ldap.JndiLdapRealm
052 * ldapRealm.contextFactory.url = ldap://localhost:389
053 * ldapRealm.contextFactory.authenticationMechanism = DIGEST-MD5
054 * </pre>
055 * and
056 * <pre>
057 * [main]
058 * ldapRealm = org.apache.shiro.realm.ldap.JndiLdapRealm
059 * ldapRealm.contextFactory.environment[java.naming.provider.url] = ldap://localhost:389
060 * ldapRealm.contextFactory.environment[java.naming.security.authentication] = DIGEST-MD5
061 * </pre>
062 * As you can see, the 2nd configuration block is a little more difficult to read and also requires knowledge
063 * of the underlying JNDI Context property keys.  The first is easier to read and understand.
064 * <p/>
065 * Note that occasionally it will be necessary to use the latter configuration style to set environment properties
066 * where no corresponding wrapper method exists.  In this case, the hybrid approach is still a little easier to read.
067 * For example:
068 * <pre>
069 * [main]
070 * ldapRealm = org.apache.shiro.realm.ldap.JndiLdapRealm
071 * ldapRealm.contextFactory.url = ldap://localhost:389
072 * ldapRealm.contextFactory.authenticationMechanism = DIGEST-MD5
073 * ldapRealm.contextFactory.environment[some.other.obscure.jndi.key] = some value
074 * </pre>
075 *
076 * @since 1.1
077 */
078public class JndiLdapContextFactory implements LdapContextFactory {
079
080    /*-------------------------------------------
081     |             C O N S T A N T S            |
082     ===========================================*/
083    /**
084     * The Sun LDAP property used to enable connection pooling.  This is used in the default implementation
085     * to enable LDAP connection pooling.
086     */
087    protected static final String SUN_CONNECTION_POOLING_PROPERTY = "com.sun.jndi.ldap.connect.pool";
088    protected static final String DEFAULT_CONTEXT_FACTORY_CLASS_NAME = "com.sun.jndi.ldap.LdapCtxFactory";
089    protected static final String SIMPLE_AUTHENTICATION_MECHANISM_NAME = "simple";
090    protected static final String DEFAULT_REFERRAL = "follow";
091
092    private static final Logger log = LoggerFactory.getLogger(JndiLdapContextFactory.class);
093
094    /*-------------------------------------------
095     |    I N S T A N C E   V A R I A B L E S   |
096     ============================================*/
097    private Map<String, Object> environment;
098    private boolean poolingEnabled;
099    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}