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 java.util.Hashtable;
22  import java.util.Map;
23  import javax.naming.AuthenticationException;
24  import javax.naming.Context;
25  import javax.naming.NamingException;
26  import javax.naming.ldap.InitialLdapContext;
27  import javax.naming.ldap.LdapContext;
28  
29  import org.apache.shiro.util.StringUtils;
30  import org.slf4j.Logger;
31  import org.slf4j.LoggerFactory;
32  
33  /**
34   * <p>Default implementation of {@link LdapContextFactory} that can be configured or extended to
35   * customize the way {@link javax.naming.ldap.LdapContext} objects are retrieved.</p>
36   * <p/>
37   * <p>This implementation of {@link LdapContextFactory} is used by the {@link AbstractLdapRealm} if a
38   * factory is not explictly configured.</p>
39   * <p/>
40   * <p>Connection pooling is enabled by default on this factory, but can be disabled using the
41   * {@link #usePooling} property.</p>
42   *
43   * @since 0.2
44   * @deprecated replaced by the {@link JndiLdapContextFactory} implementation.  This implementation will be removed
45   * prior to Shiro 2.0
46   */
47  @Deprecated
48  public class DefaultLdapContextFactory implements LdapContextFactory {
49  
50      //TODO - complete JavaDoc
51  
52      /*--------------------------------------------
53      |             C O N S T A N T S             |
54      ============================================*/
55      /**
56       * The Sun LDAP property used to enable connection pooling.  This is used in the default implementation
57       * to enable LDAP connection pooling.
58       */
59      protected static final String SUN_CONNECTION_POOLING_PROPERTY = "com.sun.jndi.ldap.connect.pool";
60      private static final String SIMPLE_AUTHENTICATION_MECHANISM_NAME = "simple";
61  
62      /*--------------------------------------------
63      |    I N S T A N C E   V A R I A B L E S    |
64      ============================================*/
65  
66      private static final Logger log = LoggerFactory.getLogger(DefaultLdapContextFactory.class);
67  
68      protected String authentication = SIMPLE_AUTHENTICATION_MECHANISM_NAME;
69  
70      protected String principalSuffix = null;
71  
72      protected String searchBase = null;
73  
74      protected String contextFactoryClassName = "com.sun.jndi.ldap.LdapCtxFactory";
75  
76      protected String url = null;
77  
78      protected String referral = "follow";
79  
80      protected String systemUsername = null;
81  
82      protected String systemPassword = null;
83  
84      private boolean usePooling = true;
85  
86      private Map<String, String> additionalEnvironment;
87  
88      /*--------------------------------------------
89      |         C O N S T R U C T O R S           |
90      ============================================*/
91  
92      /*--------------------------------------------
93      |  A C C E S S O R S / M O D I F I E R S    |
94      ============================================*/
95  
96      /**
97       * Sets the type of LDAP authentication to perform when connecting to the LDAP server.  Defaults to "simple"
98       *
99       * @param authentication the type of LDAP authentication to perform.
100      */
101     public void setAuthentication(String authentication) {
102         this.authentication = authentication;
103     }
104 
105     /**
106      * A suffix appended to the username. This is typically for
107      * domain names.  (e.g. "@MyDomain.local")
108      *
109      * @param principalSuffix the suffix.
110      */
111     public void setPrincipalSuffix(String principalSuffix) {
112         this.principalSuffix = principalSuffix;
113     }
114 
115     /**
116      * The search base for the search to perform in the LDAP server.
117      * (e.g. OU=OrganizationName,DC=MyDomain,DC=local )
118      *
119      * @param searchBase the search base.
120      * @deprecated this attribute existed, but was never used in Shiro 1.x.  It will be removed prior to Shiro 2.0.
121      */
122     @Deprecated
123     public void setSearchBase(String searchBase) {
124         this.searchBase = searchBase;
125     }
126 
127     /**
128      * The context factory to use. This defaults to the SUN LDAP JNDI implementation
129      * but can be overridden to use custom LDAP factories.
130      *
131      * @param contextFactoryClassName the context factory that should be used.
132      */
133     public void setContextFactoryClassName(String contextFactoryClassName) {
134         this.contextFactoryClassName = contextFactoryClassName;
135     }
136 
137     /**
138      * The LDAP url to connect to. (e.g. ldap://<ldapDirectoryHostname>:<port>)
139      *
140      * @param url the LDAP url.
141      */
142     public void setUrl(String url) {
143         this.url = url;
144     }
145 
146     /**
147      * Sets the LDAP referral property.  Defaults to "follow"
148      *
149      * @param referral the referral property.
150      */
151     public void setReferral(String referral) {
152         this.referral = referral;
153     }
154 
155     /**
156      * The system username that will be used when connecting to the LDAP server to retrieve authorization
157      * information about a user.  This must be specified for LDAP authorization to work, but is not required for
158      * only authentication.
159      *
160      * @param systemUsername the username to use when logging into the LDAP server for authorization.
161      */
162     public void setSystemUsername(String systemUsername) {
163         this.systemUsername = systemUsername;
164     }
165 
166 
167     /**
168      * The system password that will be used when connecting to the LDAP server to retrieve authorization
169      * information about a user.  This must be specified for LDAP authorization to work, but is not required for
170      * only authentication.
171      *
172      * @param systemPassword the password to use when logging into the LDAP server for authorization.
173      */
174     public void setSystemPassword(String systemPassword) {
175         this.systemPassword = systemPassword;
176     }
177 
178     /**
179      * Determines whether or not LdapContext pooling is enabled for connections made using the system
180      * user account.  In the default implementation, this simply
181      * sets the <tt>com.sun.jndi.ldap.connect.pool</tt> property in the LDAP context environment.  If you use an
182      * LDAP Context Factory that is not Sun's default implementation, you will need to override the
183      * default behavior to use this setting in whatever way your underlying LDAP ContextFactory
184      * supports.  By default, pooling is enabled.
185      *
186      * @param usePooling true to enable pooling, or false to disable it.
187      */
188     public void setUsePooling(boolean usePooling) {
189         this.usePooling = usePooling;
190     }
191 
192     /**
193      * These entries are added to the environment map before initializing the LDAP context.
194      *
195      * @param additionalEnvironment additional environment entries to be configured on the LDAP context.
196      */
197     public void setAdditionalEnvironment(Map<String, String> additionalEnvironment) {
198         this.additionalEnvironment = additionalEnvironment;
199     }
200 
201     /*--------------------------------------------
202     |               M E T H O D S               |
203     ============================================*/
204     public LdapContext getSystemLdapContext() throws NamingException {
205         return getLdapContext(systemUsername, systemPassword);
206     }
207 
208     /**
209      * Deprecated - use {@link #getLdapContext(Object, Object)} instead.  This will be removed before Apache Shiro 2.0.
210      *
211      * @param username the username to use when creating the connection.
212      * @param password the password to use when creating the connection.
213      * @return a {@code LdapContext} bound using the given username and password.
214      * @throws javax.naming.NamingException if there is an error creating the context.
215      * @deprecated the {@link #getLdapContext(Object, Object)} method should be used in all cases to ensure more than
216      *             String principals and credentials can be used.  Shiro no longer calls this method - it will be
217      *             removed before the 2.0 release.
218      */
219     @Deprecated
220     public LdapContext getLdapContext(String username, String password) throws NamingException {
221         if (username != null && principalSuffix != null) {
222             username += principalSuffix;
223         }
224         return getLdapContext((Object) username, password);
225     }
226 
227     public LdapContext getLdapContext(Object principal, Object credentials) throws NamingException {
228         if (url == null) {
229             throw new IllegalStateException("An LDAP URL must be specified of the form ldap://<hostname>:<port>");
230         }
231 
232         Hashtable<String, Object> env = new Hashtable<String, Object>();
233 
234         env.put(Context.SECURITY_AUTHENTICATION, authentication);
235         if (principal != null) {
236             env.put(Context.SECURITY_PRINCIPAL, principal);
237         }
238         if (credentials!= null) {
239             env.put(Context.SECURITY_CREDENTIALS, credentials);
240         }
241         env.put(Context.INITIAL_CONTEXT_FACTORY, contextFactoryClassName);
242         env.put(Context.PROVIDER_URL, url);
243         env.put(Context.REFERRAL, referral);
244 
245         // Only pool connections for system contexts
246         if (usePooling && principal != null && principal.equals(systemUsername)) {
247             // Enable connection pooling
248             env.put(SUN_CONNECTION_POOLING_PROPERTY, "true");
249         }
250 
251         if (additionalEnvironment != null) {
252             env.putAll(additionalEnvironment);
253         }
254 
255         if (log.isDebugEnabled()) {
256             log.debug("Initializing LDAP context using URL [" + url + "] and username [" + systemUsername + "] " +
257                     "with pooling [" + (usePooling ? "enabled" : "disabled") + "]");
258         }
259 
260         // validate the config before creating the context
261         validateAuthenticationInfo(env);
262 
263         return createLdapContext(env);
264     }
265 
266     /**
267      * Creates and returns a new {@link javax.naming.ldap.InitialLdapContext} instance.  This method exists primarily
268      * to support testing where a mock LdapContext can be returned instead of actually creating a connection, but
269      * subclasses are free to provide a different implementation if necessary.
270      *
271      * @param env the JNDI environment settings used to create the LDAP connection
272      * @return an LdapConnection
273      * @throws NamingException if a problem occurs creating the connection
274      */
275     protected LdapContext createLdapContext(Hashtable env) throws NamingException {
276         return new InitialLdapContext(env, null);
277     }
278 
279 
280     /**
281      * Validates the configuration in the JNDI <code>environment</code> settings and throws an exception if a problem
282      * exists.
283      * <p/>
284      * This implementation will throw a {@link AuthenticationException} if the authentication mechanism is set to
285      * 'simple', the principal is non-empty, and the credentials are empty (as per
286      * <a href="http://tools.ietf.org/html/rfc4513#section-5.1.2">rfc4513 section-5.1.2</a>).
287      *
288      * @param environment the JNDI environment settings to be validated
289      * @throws AuthenticationException if a configuration problem is detected
290      */
291     private void validateAuthenticationInfo(Hashtable<String, Object> environment)
292         throws AuthenticationException
293     {
294         // validate when using Simple auth both principal and credentials are set
295         if(SIMPLE_AUTHENTICATION_MECHANISM_NAME.equals(environment.get(Context.SECURITY_AUTHENTICATION))) {
296 
297             // only validate credentials if we have a non-empty principal
298             if( environment.get(Context.SECURITY_PRINCIPAL) != null &&
299                 StringUtils.hasText( String.valueOf( environment.get(Context.SECURITY_PRINCIPAL) ))) {
300 
301                 Object credentials = environment.get(Context.SECURITY_CREDENTIALS);
302 
303                 // from the FAQ, we need to check for empty credentials:
304                 // http://docs.oracle.com/javase/tutorial/jndi/ldap/faq.html
305                 if( credentials == null ||
306                     (credentials instanceof byte[] && ((byte[])credentials).length <= 0) || // empty byte[]
307                     (credentials instanceof char[] && ((char[])credentials).length <= 0) || // empty char[]
308                     (String.class.isInstance(credentials) && !StringUtils.hasText(String.valueOf(credentials)))) {
309 
310                     throw new javax.naming.AuthenticationException("LDAP Simple authentication requires both a "
311                                                                        + "principal and credentials.");
312                 }
313             }
314         }
315     }
316 
317 }