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     */
019    package org.apache.shiro.realm.ldap;
020    
021    import java.util.Hashtable;
022    import java.util.Map;
023    import javax.naming.AuthenticationException;
024    import javax.naming.Context;
025    import javax.naming.NamingException;
026    import javax.naming.ldap.InitialLdapContext;
027    import javax.naming.ldap.LdapContext;
028    
029    import org.apache.shiro.util.StringUtils;
030    import org.slf4j.Logger;
031    import org.slf4j.LoggerFactory;
032    
033    /**
034     * <p>Default implementation of {@link LdapContextFactory} that can be configured or extended to
035     * customize the way {@link javax.naming.ldap.LdapContext} objects are retrieved.</p>
036     * <p/>
037     * <p>This implementation of {@link LdapContextFactory} is used by the {@link AbstractLdapRealm} if a
038     * factory is not explictly configured.</p>
039     * <p/>
040     * <p>Connection pooling is enabled by default on this factory, but can be disabled using the
041     * {@link #usePooling} property.</p>
042     *
043     * @since 0.2
044     * @deprecated replaced by the {@link JndiLdapContextFactory} implementation.  This implementation will be removed
045     * prior to Shiro 2.0
046     */
047    @Deprecated
048    public class DefaultLdapContextFactory implements LdapContextFactory {
049    
050        //TODO - complete JavaDoc
051    
052        /*--------------------------------------------
053        |             C O N S T A N T S             |
054        ============================================*/
055        /**
056         * The Sun LDAP property used to enable connection pooling.  This is used in the default implementation
057         * to enable LDAP connection pooling.
058         */
059        protected static final String SUN_CONNECTION_POOLING_PROPERTY = "com.sun.jndi.ldap.connect.pool";
060        private static final String SIMPLE_AUTHENTICATION_MECHANISM_NAME = "simple";
061    
062        /*--------------------------------------------
063        |    I N S T A N C E   V A R I A B L E S    |
064        ============================================*/
065    
066        private static final Logger log = LoggerFactory.getLogger(DefaultLdapContextFactory.class);
067    
068        protected String authentication = SIMPLE_AUTHENTICATION_MECHANISM_NAME;
069    
070        protected String principalSuffix = null;
071    
072        protected String searchBase = null;
073    
074        protected String contextFactoryClassName = "com.sun.jndi.ldap.LdapCtxFactory";
075    
076        protected String url = null;
077    
078        protected String referral = "follow";
079    
080        protected String systemUsername = null;
081    
082        protected String systemPassword = null;
083    
084        private boolean usePooling = true;
085    
086        private Map<String, String> additionalEnvironment;
087    
088        /*--------------------------------------------
089        |         C O N S T R U C T O R S           |
090        ============================================*/
091    
092        /*--------------------------------------------
093        |  A C C E S S O R S / M O D I F I E R S    |
094        ============================================*/
095    
096        /**
097         * Sets the type of LDAP authentication to perform when connecting to the LDAP server.  Defaults to "simple"
098         *
099         * @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    }