| Classes in this File | Line Coverage | Branch Coverage | Complexity | ||||||||
| DefaultInitialDirContextFactory |
|
| 2.125;2.125 |
| 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 | 2 | 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 | 55 | private Map extraEnvVars = null; |
| 78 | 55 | protected MessageSourceAccessor messages = AcegiMessageSource.getAccessor(); |
| 79 | ||
| 80 | /** Type of authentication within LDAP; default is simple. */ |
|
| 81 | 55 | 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 | 55 | 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 | 55 | private String managerDn = null; |
| 94 | ||
| 95 | /** The manager user's password. */ |
|
| 96 | 55 | 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 | 55 | 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 | 55 | private boolean useConnectionPool = true; |
| 112 | ||
| 113 | /** Set to true for ldap v3 compatible servers */ |
|
| 114 | 55 | 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 | 55 | public DefaultInitialDirContextFactory(String providerUrl) { |
| 124 | 55 | this.setProviderUrl(providerUrl); |
| 125 | 54 | } |
| 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 | 55 | Assert.hasLength(providerUrl, "An LDAP connection URL must be supplied."); |
| 136 | ||
| 137 | 55 | this.providerUrl = providerUrl; |
| 138 | ||
| 139 | 55 | StringTokenizer st = new StringTokenizer(providerUrl); |
| 140 | ||
| 141 | // Work out rootDn from the first URL and check that the other URLs (if any) match |
|
| 142 | 111 | while (st.hasMoreTokens()) { |
| 143 | 57 | String url = st.nextToken(); |
| 144 | 57 | String urlRootDn = LdapUtils.parseRootDnFromUrl(url); |
| 145 | ||
| 146 | 57 | logger.info(" URL '" + url + "', root DN is '" + urlRootDn + "'"); |
| 147 | ||
| 148 | 57 | if (rootDn == null) { |
| 149 | 55 | rootDn = urlRootDn; |
| 150 | 2 | } else if (!rootDn.equals(urlRootDn)) { |
| 151 | 1 | throw new IllegalArgumentException("Root DNs must be the same when using multiple URLs"); |
| 152 | } |
|
| 153 | 56 | } |
| 154 | ||
| 155 | // This doesn't necessarily hold for embedded servers. |
|
| 156 | //Assert.isTrue(uri.getScheme().equals("ldap"), "Ldap URL must start with 'ldap://'"); |
|
| 157 | 54 | } |
| 158 | ||
| 159 | /** |
|
| 160 | * Get the LDAP url |
|
| 161 | * |
|
| 162 | * @return the url |
|
| 163 | */ |
|
| 164 | private String getProviderUrl() { |
|
| 165 | 48 | return providerUrl; |
| 166 | } |
|
| 167 | ||
| 168 | private InitialDirContext connect(Hashtable env) { |
|
| 169 | 44 | if (logger.isDebugEnabled()) { |
| 170 | 0 | Hashtable envClone = (Hashtable) env.clone(); |
| 171 | ||
| 172 | 0 | if (envClone.containsKey(Context.SECURITY_CREDENTIALS)) { |
| 173 | 0 | envClone.put(Context.SECURITY_CREDENTIALS, "******"); |
| 174 | } |
|
| 175 | ||
| 176 | 0 | logger.debug("Creating InitialDirContext with environment " + envClone); |
| 177 | } |
|
| 178 | ||
| 179 | try { |
|
| 180 | 44 | return useLdapContext ? new InitialLdapContext(env, null) : new InitialDirContext(env); |
| 181 | 5 | } catch (NamingException ne) { |
| 182 | 5 | if ((ne instanceof javax.naming.AuthenticationException) |
| 183 | || (ne instanceof OperationNotSupportedException)) { |
|
| 184 | 4 | throw new BadCredentialsException(messages.getMessage("DefaultIntitalDirContextFactory.badCredentials", |
| 185 | "Bad credentials"), ne); |
|
| 186 | } |
|
| 187 | ||
| 188 | 1 | if (ne instanceof CommunicationException) { |
| 189 | 1 | throw new LdapDataAccessException(messages.getMessage( |
| 190 | "DefaultIntitalDirContextFactory.communicationFailure", "Unable to connect to LDAP server"), ne); |
|
| 191 | } |
|
| 192 | ||
| 193 | 0 | 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 | 48 | Hashtable env = new Hashtable(); |
| 206 | ||
| 207 | 48 | env.put(Context.SECURITY_AUTHENTICATION, authenticationType); |
| 208 | 48 | env.put(Context.INITIAL_CONTEXT_FACTORY, initialContextFactory); |
| 209 | 48 | env.put(Context.PROVIDER_URL, getProviderUrl()); |
| 210 | ||
| 211 | 48 | if (useConnectionPool) { |
| 212 | 47 | env.put(CONNECTION_POOL_KEY, "true"); |
| 213 | } |
|
| 214 | ||
| 215 | 48 | if ((extraEnvVars != null) && (extraEnvVars.size() > 0)) { |
| 216 | 45 | env.putAll(extraEnvVars); |
| 217 | } |
|
| 218 | ||
| 219 | 48 | 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 | 25 | 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 | 38 | if (managerDn != null) { |
| 241 | 36 | return newInitialDirContext(managerDn, managerPassword); |
| 242 | } |
|
| 243 | ||
| 244 | 2 | Hashtable env = getEnvironment(); |
| 245 | 2 | env.put(Context.SECURITY_AUTHENTICATION, AUTH_TYPE_NONE); |
| 246 | ||
| 247 | 2 | return connect(env); |
| 248 | } |
|
| 249 | ||
| 250 | public DirContext newInitialDirContext(String username, String password) { |
|
| 251 | 42 | Hashtable env = getEnvironment(); |
| 252 | ||
| 253 | // Don't pool connections for individual users |
|
| 254 | 42 | if (!username.equals(managerDn)) { |
| 255 | 6 | env.remove(CONNECTION_POOL_KEY); |
| 256 | } |
|
| 257 | ||
| 258 | 42 | env.put(Context.SECURITY_PRINCIPAL, username); |
| 259 | 42 | env.put(Context.SECURITY_CREDENTIALS, password); |
| 260 | ||
| 261 | 42 | return connect(env); |
| 262 | } |
|
| 263 | ||
| 264 | public void setAuthenticationType(String authenticationType) { |
|
| 265 | 1 | Assert.hasLength(authenticationType, "LDAP Authentication type must not be empty or null"); |
| 266 | 1 | this.authenticationType = authenticationType; |
| 267 | 1 | } |
| 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 | 48 | Assert.notNull(extraEnvVars, "Extra environment map cannot be null."); |
| 277 | 48 | this.extraEnvVars = extraEnvVars; |
| 278 | 48 | } |
| 279 | ||
| 280 | public void setInitialContextFactory(String initialContextFactory) { |
|
| 281 | 48 | Assert.hasLength(initialContextFactory, "Initial context factory name cannot be empty or null"); |
| 282 | 48 | this.initialContextFactory = initialContextFactory; |
| 283 | 48 | } |
| 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 | 31 | Assert.hasLength(managerDn, "Manager user name cannot be empty or null."); |
| 294 | 31 | this.managerDn = managerDn; |
| 295 | 31 | } |
| 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 | 30 | Assert.hasLength(managerPassword, "Manager password must not be empty or null."); |
| 304 | 30 | this.managerPassword = managerPassword; |
| 305 | 30 | } |
| 306 | ||
| 307 | public void setMessageSource(MessageSource messageSource) { |
|
| 308 | 12 | this.messages = new MessageSourceAccessor(messageSource); |
| 309 | 12 | } |
| 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 | 1 | this.useConnectionPool = useConnectionPool; |
| 319 | 1 | } |
| 320 | ||
| 321 | public void setUseLdapContext(boolean useLdapContext) { |
|
| 322 | 0 | this.useLdapContext = useLdapContext; |
| 323 | 0 | } |
| 324 | } |