001: /* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
002: *
003: * Licensed under the Apache License, Version 2.0 (the "License");
004: * you may not use this file except in compliance with the License.
005: * You may obtain a copy of the License at
006: *
007: * http://www.apache.org/licenses/LICENSE-2.0
008: *
009: * Unless required by applicable law or agreed to in writing, software
010: * distributed under the License is distributed on an "AS IS" BASIS,
011: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012: * See the License for the specific language governing permissions and
013: * limitations under the License.
014: */
015:
016: package org.acegisecurity.ldap;
017:
018: import org.acegisecurity.AcegiMessageSource;
019: import org.acegisecurity.BadCredentialsException;
020:
021: import org.apache.commons.logging.Log;
022: import org.apache.commons.logging.LogFactory;
023:
024: import org.springframework.context.MessageSource;
025: import org.springframework.context.MessageSourceAware;
026: import org.springframework.context.support.MessageSourceAccessor;
027:
028: import org.springframework.util.Assert;
029:
030: import java.util.Hashtable;
031: import java.util.Map;
032: import java.util.StringTokenizer;
033:
034: import javax.naming.CommunicationException;
035: import javax.naming.Context;
036: import javax.naming.NamingException;
037: import javax.naming.OperationNotSupportedException;
038: import javax.naming.ldap.InitialLdapContext;
039: import javax.naming.directory.DirContext;
040: import javax.naming.directory.InitialDirContext;
041:
042: /**
043: * Encapsulates the information for connecting to an LDAP server and provides an access point for obtaining
044: * <tt>DirContext</tt> references.
045: * <p>
046: * The directory location is configured using by setting the constructor argument
047: * <tt>providerUrl</tt>. This should be in the form <tt>ldap://monkeymachine.co.uk:389/dc=acegisecurity,dc=org</tt>.
048: * The Sun JNDI provider also supports lists of space-separated URLs, each of which will be tried in turn until a
049: * connection is obtained.
050: * </p>
051: * <p>To obtain an initial context, the client calls the <tt>newInitialDirContext</tt> method. There are two
052: * signatures - one with no arguments and one which allows binding with a specific username and password.
053: * </p>
054: * <p>The no-args version will bind anonymously unless a manager login has been configured using the properties
055: * <tt>managerDn</tt> and <tt>managerPassword</tt>, in which case it will bind as the manager user.</p>
056: * <p>Connection pooling is enabled by default for anonymous or manager connections, but not when binding as a
057: * specific user.</p>
058: *
059: * @author Robert Sanders
060: * @author Luke Taylor
061: * @version $Id: DefaultInitialDirContextFactory.java 1784 2007-02-24 21:00:24Z luke_t $
062: *
063: * @see <a href="http://java.sun.com/products/jndi/tutorial/ldap/connect/pool.html">The Java tutorial's guide to LDAP
064: * connection pooling</a>
065: */
066: public class DefaultInitialDirContextFactory implements
067: InitialDirContextFactory, MessageSourceAware {
068: //~ Static fields/initializers =====================================================================================
069:
070: private static final Log logger = LogFactory
071: .getLog(DefaultInitialDirContextFactory.class);
072: private static final String CONNECTION_POOL_KEY = "com.sun.jndi.ldap.connect.pool";
073: private static final String AUTH_TYPE_NONE = "none";
074:
075: //~ Instance fields ================================================================================================
076:
077: /** Allows extra environment variables to be added at config time. */
078: private Map extraEnvVars = null;
079: protected MessageSourceAccessor messages = AcegiMessageSource
080: .getAccessor();
081:
082: /** Type of authentication within LDAP; default is simple. */
083: private String authenticationType = "simple";
084:
085: /**
086: * The INITIAL_CONTEXT_FACTORY used to create the JNDI Factory. Default is
087: * "com.sun.jndi.ldap.LdapCtxFactory"; you <b>should not</b> need to set this unless you have unusual needs.
088: */
089: private String initialContextFactory = "com.sun.jndi.ldap.LdapCtxFactory";
090:
091: /**
092: * If your LDAP server does not allow anonymous searches then you will need to provide a "manager" user's
093: * DN to log in with.
094: */
095: private String managerDn = null;
096:
097: /** The manager user's password. */
098: private String managerPassword = "manager_password_not_set";
099:
100: /** The LDAP url of the server (and root context) to connect to. */
101: private String providerUrl;
102:
103: /**
104: * The root DN. This is worked out from the url. It is used by client classes when forming a full DN for
105: * bind authentication (for example).
106: */
107: private String rootDn = null;
108:
109: /**
110: * Use the LDAP Connection pool; if true, then the LDAP environment property
111: * "com.sun.jndi.ldap.connect.pool" is added to any other JNDI properties.
112: */
113: private boolean useConnectionPool = true;
114:
115: /** Set to true for ldap v3 compatible servers */
116: private boolean useLdapContext = false;
117:
118: //~ Constructors ===================================================================================================
119:
120: /**
121: * Create and initialize an instance to the LDAP url provided
122: *
123: * @param providerUrl a String of the form <code>ldap://localhost:389/base_dn<code>
124: */
125: public DefaultInitialDirContextFactory(String providerUrl) {
126: this .setProviderUrl(providerUrl);
127: }
128:
129: //~ Methods ========================================================================================================
130:
131: /**
132: * Set the LDAP url
133: *
134: * @param providerUrl a String of the form <code>ldap://localhost:389/base_dn<code>
135: */
136: private void setProviderUrl(String providerUrl) {
137: Assert.hasLength(providerUrl,
138: "An LDAP connection URL must be supplied.");
139:
140: this .providerUrl = providerUrl;
141:
142: StringTokenizer st = new StringTokenizer(providerUrl);
143:
144: // Work out rootDn from the first URL and check that the other URLs (if any) match
145: while (st.hasMoreTokens()) {
146: String url = st.nextToken();
147: String urlRootDn = LdapUtils.parseRootDnFromUrl(url);
148:
149: logger.info(" URL '" + url + "', root DN is '" + urlRootDn
150: + "'");
151:
152: if (rootDn == null) {
153: rootDn = urlRootDn;
154: } else if (!rootDn.equals(urlRootDn)) {
155: throw new IllegalArgumentException(
156: "Root DNs must be the same when using multiple URLs");
157: }
158: }
159:
160: // This doesn't necessarily hold for embedded servers.
161: //Assert.isTrue(uri.getScheme().equals("ldap"), "Ldap URL must start with 'ldap://'");
162: }
163:
164: /**
165: * Get the LDAP url
166: *
167: * @return the url
168: */
169: private String getProviderUrl() {
170: return providerUrl;
171: }
172:
173: private InitialDirContext connect(Hashtable env) {
174: if (logger.isDebugEnabled()) {
175: Hashtable envClone = (Hashtable) env.clone();
176:
177: if (envClone.containsKey(Context.SECURITY_CREDENTIALS)) {
178: envClone.put(Context.SECURITY_CREDENTIALS, "******");
179: }
180:
181: logger.debug("Creating InitialDirContext with environment "
182: + envClone);
183: }
184:
185: try {
186: return useLdapContext ? new InitialLdapContext(env, null)
187: : new InitialDirContext(env);
188: } catch (NamingException ne) {
189: if ((ne instanceof javax.naming.AuthenticationException)
190: || (ne instanceof OperationNotSupportedException)) {
191: throw new BadCredentialsException(
192: messages
193: .getMessage(
194: "DefaultIntitalDirContextFactory.badCredentials",
195: "Bad credentials"), ne);
196: }
197:
198: if (ne instanceof CommunicationException) {
199: throw new LdapDataAccessException(
200: messages
201: .getMessage(
202: "DefaultIntitalDirContextFactory.communicationFailure",
203: "Unable to connect to LDAP server"),
204: ne);
205: }
206:
207: throw new LdapDataAccessException(
208: messages
209: .getMessage(
210: "DefaultIntitalDirContextFactory.unexpectedException",
211: "Failed to obtain InitialDirContext due to unexpected exception"),
212: ne);
213: }
214: }
215:
216: /**
217: * Sets up the environment parameters for creating a new context.
218: *
219: * @return the Hashtable describing the base DirContext that will be created, minus the username/password if any.
220: */
221: protected Hashtable getEnvironment() {
222: Hashtable env = new Hashtable();
223:
224: env.put(Context.SECURITY_AUTHENTICATION, authenticationType);
225: env.put(Context.INITIAL_CONTEXT_FACTORY, initialContextFactory);
226: env.put(Context.PROVIDER_URL, getProviderUrl());
227:
228: if (useConnectionPool) {
229: env.put(CONNECTION_POOL_KEY, "true");
230: }
231:
232: if ((extraEnvVars != null) && (extraEnvVars.size() > 0)) {
233: env.putAll(extraEnvVars);
234: }
235:
236: return env;
237: }
238:
239: /**
240: * Returns the root DN of the configured provider URL. For example, if the URL is
241: * <tt>ldap://monkeymachine.co.uk:389/dc=acegisecurity,dc=org</tt> the value will be
242: * <tt>dc=acegisecurity,dc=org</tt>.
243: *
244: * @return the root DN calculated from the path of the LDAP url.
245: */
246: public String getRootDn() {
247: return rootDn;
248: }
249:
250: /**
251: * Connects anonymously unless a manager user has been specified, in which case it will bind as the
252: * manager.
253: *
254: * @return the resulting context object.
255: */
256: public DirContext newInitialDirContext() {
257: if (managerDn != null) {
258: return newInitialDirContext(managerDn, managerPassword);
259: }
260:
261: Hashtable env = getEnvironment();
262: env.put(Context.SECURITY_AUTHENTICATION, AUTH_TYPE_NONE);
263:
264: return connect(env);
265: }
266:
267: public DirContext newInitialDirContext(String username,
268: String password) {
269: Hashtable env = getEnvironment();
270:
271: // Don't pool connections for individual users
272: if (!username.equals(managerDn)) {
273: env.remove(CONNECTION_POOL_KEY);
274: }
275:
276: env.put(Context.SECURITY_PRINCIPAL, username);
277: env.put(Context.SECURITY_CREDENTIALS, password);
278:
279: return connect(env);
280: }
281:
282: public void setAuthenticationType(String authenticationType) {
283: Assert.hasLength(authenticationType,
284: "LDAP Authentication type must not be empty or null");
285: this .authenticationType = authenticationType;
286: }
287:
288: /**
289: * Sets any custom environment variables which will be added to the those returned
290: * by the <tt>getEnvironment</tt> method.
291: *
292: * @param extraEnvVars extra environment variables to be added at config time.
293: */
294: public void setExtraEnvVars(Map extraEnvVars) {
295: Assert.notNull(extraEnvVars,
296: "Extra environment map cannot be null.");
297: this .extraEnvVars = extraEnvVars;
298: }
299:
300: public void setInitialContextFactory(String initialContextFactory) {
301: Assert.hasLength(initialContextFactory,
302: "Initial context factory name cannot be empty or null");
303: this .initialContextFactory = initialContextFactory;
304: }
305:
306: /**
307: * Sets the directory user to authenticate as when obtaining a context using the
308: * <tt>newInitialDirContext()</tt> method.
309: * If no name is supplied then the context will be obtained anonymously.
310: *
311: * @param managerDn The name of the "manager" user for default authentication.
312: */
313: public void setManagerDn(String managerDn) {
314: Assert.hasLength(managerDn,
315: "Manager user name cannot be empty or null.");
316: this .managerDn = managerDn;
317: }
318:
319: /**
320: * Sets the password which will be used in combination with the manager DN.
321: *
322: * @param managerPassword The "manager" user's password.
323: */
324: public void setManagerPassword(String managerPassword) {
325: Assert.hasLength(managerPassword,
326: "Manager password must not be empty or null.");
327: this .managerPassword = managerPassword;
328: }
329:
330: public void setMessageSource(MessageSource messageSource) {
331: this .messages = new MessageSourceAccessor(messageSource);
332: }
333:
334: /**
335: * Connection pooling is enabled by default for anonymous or "manager" connections when using the default
336: * Sun provider. To disable all connection pooling, set this property to false.
337: *
338: * @param useConnectionPool whether to pool connections for non-specific users.
339: */
340: public void setUseConnectionPool(boolean useConnectionPool) {
341: this .useConnectionPool = useConnectionPool;
342: }
343:
344: public void setUseLdapContext(boolean useLdapContext) {
345: this.useLdapContext = useLdapContext;
346: }
347: }
|