1 /* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
2 *
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
6 *
7 * http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15
16 package org.acegisecurity.ldap;
17
18 import org.acegisecurity.AcegiMessageSource;
19 import org.acegisecurity.BadCredentialsException;
20
21 import org.apache.commons.logging.Log;
22 import org.apache.commons.logging.LogFactory;
23
24 import org.springframework.context.MessageSource;
25 import org.springframework.context.MessageSourceAware;
26 import org.springframework.context.support.MessageSourceAccessor;
27
28 import org.springframework.util.Assert;
29
30 import java.util.Hashtable;
31 import java.util.Map;
32 import java.util.StringTokenizer;
33
34 import javax.naming.CommunicationException;
35 import javax.naming.Context;
36 import javax.naming.NamingException;
37 import javax.naming.OperationNotSupportedException;
38 import javax.naming.ldap.InitialLdapContext;
39 import javax.naming.directory.DirContext;
40 import javax.naming.directory.InitialDirContext;
41
42
43 /**
44 * Encapsulates the information for connecting to an LDAP server and provides an access point for obtaining
45 * <tt>DirContext</tt> references.
46 * <p>
47 * The directory location is configured using by setting the constructor argument
48 * <tt>providerUrl</tt>. This should be in the form <tt>ldap://monkeymachine.co.uk:389/dc=acegisecurity,dc=org</tt>.
49 * The Sun JNDI provider also supports lists of space-separated URLs, each of which will be tried in turn until a
50 * connection is obtained.
51 * </p>
52 * <p>To obtain an initial context, the client calls the <tt>newInitialDirContext</tt> method. There are two
53 * signatures - one with no arguments and one which allows binding with a specific username and password.
54 * </p>
55 * <p>The no-args version will bind anonymously unless a manager login has been configured using the properties
56 * <tt>managerDn</tt> and <tt>managerPassword</tt>, in which case it will bind as the manager user.</p>
57 * <p>Connection pooling is enabled by default for anonymous or manager connections, but not when binding as a
58 * specific user.</p>
59 *
60 * @author Robert Sanders
61 * @author Luke Taylor
62 * @version $Id: DefaultInitialDirContextFactory.java 1784 2007-02-24 21:00:24Z luke_t $
63 *
64 * @see <a href="http://java.sun.com/products/jndi/tutorial/ldap/connect/pool.html">The Java tutorial's guide to LDAP
65 * connection pooling</a>
66 */
67 public class DefaultInitialDirContextFactory implements InitialDirContextFactory, MessageSourceAware {
68 //~ Static fields/initializers =====================================================================================
69
70 private static final Log logger = LogFactory.getLog(DefaultInitialDirContextFactory.class);
71 private static final String CONNECTION_POOL_KEY = "com.sun.jndi.ldap.connect.pool";
72 private static final String AUTH_TYPE_NONE = "none";
73
74 //~ Instance fields ================================================================================================
75
76 /** Allows extra environment variables to be added at config time. */
77 private Map extraEnvVars = null;
78 protected MessageSourceAccessor messages = AcegiMessageSource.getAccessor();
79
80 /** Type of authentication within LDAP; default is simple. */
81 private String authenticationType = "simple";
82
83 /**
84 * The INITIAL_CONTEXT_FACTORY used to create the JNDI Factory. Default is
85 * "com.sun.jndi.ldap.LdapCtxFactory"; you <b>should not</b> need to set this unless you have unusual needs.
86 */
87 private String initialContextFactory = "com.sun.jndi.ldap.LdapCtxFactory";
88
89 /**
90 * If your LDAP server does not allow anonymous searches then you will need to provide a "manager" user's
91 * DN to log in with.
92 */
93 private String managerDn = null;
94
95 /** The manager user's password. */
96 private String managerPassword = "manager_password_not_set";
97
98 /** The LDAP url of the server (and root context) to connect to. */
99 private String providerUrl;
100
101 /**
102 * The root DN. This is worked out from the url. It is used by client classes when forming a full DN for
103 * bind authentication (for example).
104 */
105 private String rootDn = null;
106
107 /**
108 * Use the LDAP Connection pool; if true, then the LDAP environment property
109 * "com.sun.jndi.ldap.connect.pool" is added to any other JNDI properties.
110 */
111 private boolean useConnectionPool = true;
112
113 /** Set to true for ldap v3 compatible servers */
114 private boolean useLdapContext = false;
115
116 //~ Constructors ===================================================================================================
117
118 /**
119 * Create and initialize an instance to the LDAP url provided
120 *
121 * @param providerUrl a String of the form <code>ldap://localhost:389/base_dn<code>
122 */
123 public DefaultInitialDirContextFactory(String providerUrl) {
124 this.setProviderUrl(providerUrl);
125 }
126
127 //~ Methods ========================================================================================================
128
129 /**
130 * Set the LDAP url
131 *
132 * @param providerUrl a String of the form <code>ldap://localhost:389/base_dn<code>
133 */
134 private void setProviderUrl(String providerUrl) {
135 Assert.hasLength(providerUrl, "An LDAP connection URL must be supplied.");
136
137 this.providerUrl = providerUrl;
138
139 StringTokenizer st = new StringTokenizer(providerUrl);
140
141 // Work out rootDn from the first URL and check that the other URLs (if any) match
142 while (st.hasMoreTokens()) {
143 String url = st.nextToken();
144 String urlRootDn = LdapUtils.parseRootDnFromUrl(url);
145
146 logger.info(" URL '" + url + "', root DN is '" + urlRootDn + "'");
147
148 if (rootDn == null) {
149 rootDn = urlRootDn;
150 } else if (!rootDn.equals(urlRootDn)) {
151 throw new IllegalArgumentException("Root DNs must be the same when using multiple URLs");
152 }
153 }
154
155 // This doesn't necessarily hold for embedded servers.
156 //Assert.isTrue(uri.getScheme().equals("ldap"), "Ldap URL must start with 'ldap://'");
157 }
158
159 /**
160 * Get the LDAP url
161 *
162 * @return the url
163 */
164 private String getProviderUrl() {
165 return providerUrl;
166 }
167
168 private InitialDirContext connect(Hashtable env) {
169 if (logger.isDebugEnabled()) {
170 Hashtable envClone = (Hashtable) env.clone();
171
172 if (envClone.containsKey(Context.SECURITY_CREDENTIALS)) {
173 envClone.put(Context.SECURITY_CREDENTIALS, "******");
174 }
175
176 logger.debug("Creating InitialDirContext with environment " + envClone);
177 }
178
179 try {
180 return useLdapContext ? new InitialLdapContext(env, null) : new InitialDirContext(env);
181 } catch (NamingException ne) {
182 if ((ne instanceof javax.naming.AuthenticationException)
183 || (ne instanceof OperationNotSupportedException)) {
184 throw new BadCredentialsException(messages.getMessage("DefaultIntitalDirContextFactory.badCredentials",
185 "Bad credentials"), ne);
186 }
187
188 if (ne instanceof CommunicationException) {
189 throw new LdapDataAccessException(messages.getMessage(
190 "DefaultIntitalDirContextFactory.communicationFailure", "Unable to connect to LDAP server"), ne);
191 }
192
193 throw new LdapDataAccessException(messages.getMessage(
194 "DefaultIntitalDirContextFactory.unexpectedException",
195 "Failed to obtain InitialDirContext due to unexpected exception"), ne);
196 }
197 }
198
199 /**
200 * Sets up the environment parameters for creating a new context.
201 *
202 * @return the Hashtable describing the base DirContext that will be created, minus the username/password if any.
203 */
204 protected Hashtable getEnvironment() {
205 Hashtable env = new Hashtable();
206
207 env.put(Context.SECURITY_AUTHENTICATION, authenticationType);
208 env.put(Context.INITIAL_CONTEXT_FACTORY, initialContextFactory);
209 env.put(Context.PROVIDER_URL, getProviderUrl());
210
211 if (useConnectionPool) {
212 env.put(CONNECTION_POOL_KEY, "true");
213 }
214
215 if ((extraEnvVars != null) && (extraEnvVars.size() > 0)) {
216 env.putAll(extraEnvVars);
217 }
218
219 return env;
220 }
221
222 /**
223 * Returns the root DN of the configured provider URL. For example, if the URL is
224 * <tt>ldap://monkeymachine.co.uk:389/dc=acegisecurity,dc=org</tt> the value will be
225 * <tt>dc=acegisecurity,dc=org</tt>.
226 *
227 * @return the root DN calculated from the path of the LDAP url.
228 */
229 public String getRootDn() {
230 return rootDn;
231 }
232
233 /**
234 * Connects anonymously unless a manager user has been specified, in which case it will bind as the
235 * manager.
236 *
237 * @return the resulting context object.
238 */
239 public DirContext newInitialDirContext() {
240 if (managerDn != null) {
241 return newInitialDirContext(managerDn, managerPassword);
242 }
243
244 Hashtable env = getEnvironment();
245 env.put(Context.SECURITY_AUTHENTICATION, AUTH_TYPE_NONE);
246
247 return connect(env);
248 }
249
250 public DirContext newInitialDirContext(String username, String password) {
251 Hashtable env = getEnvironment();
252
253 // Don't pool connections for individual users
254 if (!username.equals(managerDn)) {
255 env.remove(CONNECTION_POOL_KEY);
256 }
257
258 env.put(Context.SECURITY_PRINCIPAL, username);
259 env.put(Context.SECURITY_CREDENTIALS, password);
260
261 return connect(env);
262 }
263
264 public void setAuthenticationType(String authenticationType) {
265 Assert.hasLength(authenticationType, "LDAP Authentication type must not be empty or null");
266 this.authenticationType = authenticationType;
267 }
268
269 /**
270 * Sets any custom environment variables which will be added to the those returned
271 * by the <tt>getEnvironment</tt> method.
272 *
273 * @param extraEnvVars extra environment variables to be added at config time.
274 */
275 public void setExtraEnvVars(Map extraEnvVars) {
276 Assert.notNull(extraEnvVars, "Extra environment map cannot be null.");
277 this.extraEnvVars = extraEnvVars;
278 }
279
280 public void setInitialContextFactory(String initialContextFactory) {
281 Assert.hasLength(initialContextFactory, "Initial context factory name cannot be empty or null");
282 this.initialContextFactory = initialContextFactory;
283 }
284
285 /**
286 * Sets the directory user to authenticate as when obtaining a context using the
287 * <tt>newInitialDirContext()</tt> method.
288 * If no name is supplied then the context will be obtained anonymously.
289 *
290 * @param managerDn The name of the "manager" user for default authentication.
291 */
292 public void setManagerDn(String managerDn) {
293 Assert.hasLength(managerDn, "Manager user name cannot be empty or null.");
294 this.managerDn = managerDn;
295 }
296
297 /**
298 * Sets the password which will be used in combination with the manager DN.
299 *
300 * @param managerPassword The "manager" user's password.
301 */
302 public void setManagerPassword(String managerPassword) {
303 Assert.hasLength(managerPassword, "Manager password must not be empty or null.");
304 this.managerPassword = managerPassword;
305 }
306
307 public void setMessageSource(MessageSource messageSource) {
308 this.messages = new MessageSourceAccessor(messageSource);
309 }
310
311 /**
312 * Connection pooling is enabled by default for anonymous or "manager" connections when using the default
313 * Sun provider. To disable all connection pooling, set this property to false.
314 *
315 * @param useConnectionPool whether to pool connections for non-specific users.
316 */
317 public void setUseConnectionPool(boolean useConnectionPool) {
318 this.useConnectionPool = useConnectionPool;
319 }
320
321 public void setUseLdapContext(boolean useLdapContext) {
322 this.useLdapContext = useLdapContext;
323 }
324 }