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 java.util.Hashtable;
022import java.util.Map;
023import javax.naming.AuthenticationException;
024import javax.naming.Context;
025import javax.naming.NamingException;
026import javax.naming.ldap.InitialLdapContext;
027import javax.naming.ldap.LdapContext;
028
029import org.apache.shiro.util.StringUtils;
030import org.slf4j.Logger;
031import 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
048public 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}