1 /* 2 * Licensed to the Apache Software Foundation (ASF) under one 3 * or more contributor license agreements. See the NOTICE file 4 * distributed with this work for additional information 5 * regarding copyright ownership. The ASF licenses this file 6 * to you under the Apache License, Version 2.0 (the 7 * "License"); you may not use this file except in compliance 8 * with the License. You may obtain a copy of the License at 9 * 10 * http://www.apache.org/licenses/LICENSE-2.0 11 * 12 * Unless required by applicable law or agreed to in writing, 13 * software distributed under the License is distributed on an 14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 * KIND, either express or implied. See the License for the 16 * specific language governing permissions and limitations 17 * under the License. 18 */ 19 package org.apache.shiro.realm.ldap; 20 21 import org.apache.shiro.util.StringUtils; 22 import org.slf4j.Logger; 23 import org.slf4j.LoggerFactory; 24 25 import javax.naming.Context; 26 import javax.naming.NamingException; 27 import javax.naming.ldap.InitialLdapContext; 28 import javax.naming.ldap.LdapContext; 29 import java.util.HashMap; 30 import java.util.Hashtable; 31 import java.util.Map; 32 33 /** 34 * {@link LdapContextFactory} implementation using the default Sun/Oracle JNDI Ldap API, utilizing JNDI 35 * environment properties and an {@link javax.naming.InitialContext}. 36 * <h2>Configuration</h2> 37 * This class basically wraps a default template JNDI environment properties Map. This properties map is the base 38 * configuration template used to acquire JNDI {@link LdapContext} connections at runtime. The 39 * {@link #getLdapContext(Object, Object)} method implementation merges this default template with other properties 40 * accessible at runtime only (for example per-method principals and credentials). The constructed runtime map is the 41 * one used to acquire the {@link LdapContext}. 42 * <p/> 43 * The template can be configured directly via the {@link #getEnvironment()}/{@link #setEnvironment(java.util.Map)} 44 * properties directly if necessary, but it is usually more convenient to use the supporting wrapper get/set methods 45 * for various environment properties. These wrapper methods interact with the environment 46 * template on your behalf, leaving your configuration cleaner and easier to understand. 47 * <p/> 48 * For example, consider the following two identical configurations: 49 * <pre> 50 * [main] 51 * ldapRealm = org.apache.shiro.realm.ldap.JndiLdapRealm 52 * ldapRealm.contextFactory.url = ldap://localhost:389 53 * ldapRealm.contextFactory.authenticationMechanism = DIGEST-MD5 54 * </pre> 55 * and 56 * <pre> 57 * [main] 58 * ldapRealm = org.apache.shiro.realm.ldap.JndiLdapRealm 59 * ldapRealm.contextFactory.environment[java.naming.provider.url] = ldap://localhost:389 60 * ldapRealm.contextFactory.environment[java.naming.security.authentication] = DIGEST-MD5 61 * </pre> 62 * As you can see, the 2nd configuration block is a little more difficult to read and also requires knowledge 63 * of the underlying JNDI Context property keys. The first is easier to read and understand. 64 * <p/> 65 * Note that occasionally it will be necessary to use the latter configuration style to set environment properties 66 * where no corresponding wrapper method exists. In this case, the hybrid approach is still a little easier to read. 67 * For example: 68 * <pre> 69 * [main] 70 * ldapRealm = org.apache.shiro.realm.ldap.JndiLdapRealm 71 * ldapRealm.contextFactory.url = ldap://localhost:389 72 * ldapRealm.contextFactory.authenticationMechanism = DIGEST-MD5 73 * ldapRealm.contextFactory.environment[some.other.obscure.jndi.key] = some value 74 * </pre> 75 * 76 * @since 1.1 77 */ 78 public class JndiLdapContextFactory implements LdapContextFactory { 79 80 /*------------------------------------------- 81 | C O N S T A N T S | 82 ===========================================*/ 83 /** 84 * The Sun LDAP property used to enable connection pooling. This is used in the default implementation 85 * to enable LDAP connection pooling. 86 */ 87 protected static final String SUN_CONNECTION_POOLING_PROPERTY = "com.sun.jndi.ldap.connect.pool"; 88 protected static final String DEFAULT_CONTEXT_FACTORY_CLASS_NAME = "com.sun.jndi.ldap.LdapCtxFactory"; 89 protected static final String SIMPLE_AUTHENTICATION_MECHANISM_NAME = "simple"; 90 protected static final String DEFAULT_REFERRAL = "follow"; 91 92 private static final Logger log = LoggerFactory.getLogger(JndiLdapContextFactory.class); 93 94 /*------------------------------------------- 95 | I N S T A N C E V A R I A B L E S | 96 ============================================*/ 97 private Map<String, Object> environment; 98 private boolean poolingEnabled; 99 private String systemPassword; 100 private String systemUsername; 101 102 /*------------------------------------------- 103 | C O N S T R U C T O R S | 104 ===========================================*/ 105 106 /** 107 * Default no-argument constructor that initializes the backing {@link #getEnvironment() environment template} with 108 * the {@link #setContextFactoryClassName(String) contextFactoryClassName} equal to 109 * {@code com.sun.jndi.ldap.LdapCtxFactory} (the Sun/Oracle default) and the default 110 * {@link #setReferral(String) referral} behavior to {@code follow}. 111 */ 112 public JndiLdapContextFactory() { 113 this.environment = new HashMap<String, Object>(); 114 setContextFactoryClassName(DEFAULT_CONTEXT_FACTORY_CLASS_NAME); 115 setReferral(DEFAULT_REFERRAL); 116 poolingEnabled = true; 117 } 118 119 /*------------------------------------------- 120 | A C C E S S O R S / M O D I F I E R S | 121 ===========================================*/ 122 123 /** 124 * Sets the type of LDAP authentication mechanism to use when connecting to the LDAP server. 125 * This is a wrapper method for setting the JNDI {@link #getEnvironment() environment template}'s 126 * {@link Context#SECURITY_AUTHENTICATION} property. 127 * <p/> 128 * "none" (i.e. anonymous) and "simple" authentications are supported automatically and don't need to be configured 129 * via this property. However, if you require a different mechanism, such as a SASL or External mechanism, you 130 * must configure that explicitly via this property. See the 131 * <a href="http://download-llnw.oracle.com/javase/tutorial/jndi/ldap/auth_mechs.html">JNDI LDAP 132 * Authentication Mechanisms</a> for more information. 133 * 134 * @param authenticationMechanism the type of LDAP authentication to perform. 135 * @see <a href="http://download-llnw.oracle.com/javase/tutorial/jndi/ldap/auth_mechs.html"> 136 * http://download-llnw.oracle.com/javase/tutorial/jndi/ldap/auth_mechs.html</a> 137 */ 138 public void setAuthenticationMechanism(String authenticationMechanism) { 139 setEnvironmentProperty(Context.SECURITY_AUTHENTICATION, authenticationMechanism); 140 } 141 142 /** 143 * Returns the type of LDAP authentication mechanism to use when connecting to the LDAP server. 144 * This is a wrapper method for getting the JNDI {@link #getEnvironment() environment template}'s 145 * {@link Context#SECURITY_AUTHENTICATION} property. 146 * <p/> 147 * If this property remains un-configured (i.e. {@code null} indicating the 148 * {@link #setAuthenticationMechanism(String)} method wasn't used), this indicates that the default JNDI 149 * "none" (anonymous) and "simple" authentications are supported automatically. Any non-null value returned 150 * represents an explicitly configured mechanism (e.g. a SASL or external mechanism). See the 151 * <a href="http://download-llnw.oracle.com/javase/tutorial/jndi/ldap/auth_mechs.html">JNDI LDAP 152 * Authentication Mechanisms</a> for more information. 153 * 154 * @return the type of LDAP authentication mechanism to use when connecting to the LDAP server. 155 * @see <a href="http://download-llnw.oracle.com/javase/tutorial/jndi/ldap/auth_mechs.html"> 156 * http://download-llnw.oracle.com/javase/tutorial/jndi/ldap/auth_mechs.html</a> 157 */ 158 public String getAuthenticationMechanism() { 159 return (String) getEnvironmentProperty(Context.SECURITY_AUTHENTICATION); 160 } 161 162 /** 163 * The name of the ContextFactory class to use. This defaults to the SUN LDAP JNDI implementation 164 * but can be overridden to use custom LDAP factories. 165 * <p/> 166 * This is a wrapper method for setting the JNDI environment's {@link Context#INITIAL_CONTEXT_FACTORY} property. 167 * 168 * @param contextFactoryClassName the context factory that should be used. 169 */ 170 public void setContextFactoryClassName(String contextFactoryClassName) { 171 setEnvironmentProperty(Context.INITIAL_CONTEXT_FACTORY, contextFactoryClassName); 172 } 173 174 /** 175 * Sets the name of the ContextFactory class to use. This defaults to the SUN LDAP JNDI implementation 176 * but can be overridden to use custom LDAP factories. 177 * <p/> 178 * This is a wrapper method for getting the JNDI environment's {@link Context#INITIAL_CONTEXT_FACTORY} property. 179 * 180 * @return the name of the ContextFactory class to use. 181 */ 182 public String getContextFactoryClassName() { 183 return (String) getEnvironmentProperty(Context.INITIAL_CONTEXT_FACTORY); 184 } 185 186 /** 187 * Returns the base JNDI environment template to use when acquiring an LDAP connection (an {@link LdapContext}). 188 * This property is the base configuration template to use for all connections. This template is then 189 * merged with appropriate runtime values as necessary in the 190 * {@link #getLdapContext(Object, Object)} implementation. The merged environment instance is what is used to 191 * acquire the {@link LdapContext} at runtime. 192 * <p/> 193 * Most other get/set methods in this class act as thin proxy wrappers that interact with this property. The 194 * benefit of using them is you have an easier-to-use configuration mechanism compared to setting map properties 195 * based on JNDI context keys. 196 * 197 * @return the base JNDI environment template to use when acquiring an LDAP connection (an {@link LdapContext}) 198 */ 199 public Map getEnvironment() { 200 return this.environment; 201 } 202 203 /** 204 * Sets the base JNDI environment template to use when acquiring LDAP connections. It is typically more common 205 * to use the other get/set methods in this class to set individual environment settings rather than use 206 * this method, but it is available for advanced users that want full control over the base JNDI environment 207 * settings. 208 * <p/> 209 * Note that this template only represents the base/default environment settings. It is then merged with 210 * appropriate runtime values as necessary in the {@link #getLdapContext(Object, Object)} implementation. 211 * The merged environment instance is what is used to acquire the connection ({@link LdapContext}) at runtime. 212 * 213 * @param env the base JNDI environment template to use when acquiring LDAP connections. 214 */ 215 @SuppressWarnings({"unchecked"}) 216 public void setEnvironment(Map env) { 217 this.environment = env; 218 } 219 220 /** 221 * Returns the environment property value bound under the specified key. 222 * 223 * @param name the name of the environment property 224 * @return the property value or {@code null} if the value has not been set. 225 */ 226 private Object getEnvironmentProperty(String name) { 227 return this.environment.get(name); 228 } 229 230 /** 231 * Will apply the value to the environment attribute if and only if the value is not null or empty. If it is 232 * null or empty, the corresponding environment attribute will be removed. 233 * 234 * @param name the environment property key 235 * @param value the environment property value. A null/empty value will trigger removal. 236 */ 237 private void setEnvironmentProperty(String name, String value) { 238 if (StringUtils.hasText(value)) { 239 this.environment.put(name, value); 240 } else { 241 this.environment.remove(name); 242 } 243 } 244 245 /** 246 * Returns whether or not connection pooling should be used when possible and appropriate. This property is NOT 247 * backed by the {@link #getEnvironment() environment template} like most other properties in this class. It 248 * is a flag to indicate that pooling is preferred. The default value is {@code true}. 249 * <p/> 250 * However, pooling will only actually be enabled if this property is {@code true} <em>and</em> the connection 251 * being created is for the {@link #getSystemUsername() systemUsername} user. Connection pooling is not used for 252 * general authentication attempts by application end-users because the probability of re-use for that same 253 * user-specific connection after an authentication attempt is extremely low. 254 * <p/> 255 * If this attribute is {@code true} and it has been determined that the connection is being made with the 256 * {@link #getSystemUsername() systemUsername}, the 257 * {@link #getLdapContext(Object, Object)} implementation will set the Sun/Oracle-specific 258 * {@code com.sun.jndi.ldap.connect.pool} environment property to "{@code true}". This means setting 259 * this property is only likely to work if using the Sun/Oracle default context factory class (i.e. not using 260 * a custom {@link #getContextFactoryClassName() contextFactoryClassName}). 261 * 262 * @return whether or not connection pooling should be used when possible and appropriate 263 */ 264 public boolean isPoolingEnabled() { 265 return poolingEnabled; 266 } 267 268 /** 269 * Sets whether or not connection pooling should be used when possible and appropriate. This property is NOT 270 * a wrapper to the {@link #getEnvironment() environment template} like most other properties in this class. It 271 * is a flag to indicate that pooling is preferred. The default value is {@code true}. 272 * <p/> 273 * However, pooling will only actually be enabled if this property is {@code true} <em>and</em> the connection 274 * being created is for the {@link #getSystemUsername() systemUsername} user. Connection pooling is not used for 275 * general authentication attempts by application end-users because the probability of re-use for that same 276 * user-specific connection after an authentication attempt is extremely low. 277 * <p/> 278 * If this attribute is {@code true} and it has been determined that the connection is being made with the 279 * {@link #getSystemUsername() systemUsername}, the 280 * {@link #getLdapContext(Object, Object)} implementation will set the Sun/Oracle-specific 281 * {@code com.sun.jndi.ldap.connect.pool} environment property to "{@code true}". This means setting 282 * this property is only likely to work if using the Sun/Oracle default context factory class (i.e. not using 283 * a custom {@link #getContextFactoryClassName() contextFactoryClassName}). 284 * 285 * @param poolingEnabled whether or not connection pooling should be used when possible and appropriate 286 */ 287 public void setPoolingEnabled(boolean poolingEnabled) { 288 this.poolingEnabled = poolingEnabled; 289 } 290 291 /** 292 * Sets the LDAP referral behavior when creating a connection. Defaults to {@code follow}. See the Sun/Oracle LDAP 293 * <a href="http://java.sun.com/products/jndi/tutorial/ldap/referral/jndi.html">referral documentation</a> for more. 294 * 295 * @param referral the referral property. 296 * @see <a href="http://java.sun.com/products/jndi/tutorial/ldap/referral/jndi.html">Referrals in JNDI</a> 297 */ 298 public void setReferral(String referral) { 299 setEnvironmentProperty(Context.REFERRAL, referral); 300 } 301 302 /** 303 * Returns the LDAP referral behavior when creating a connection. Defaults to {@code follow}. 304 * See the Sun/Oracle LDAP 305 * <a href="http://java.sun.com/products/jndi/tutorial/ldap/referral/jndi.html">referral documentation</a> for more. 306 * 307 * @return the LDAP referral behavior when creating a connection. 308 * @see <a href="http://java.sun.com/products/jndi/tutorial/ldap/referral/jndi.html">Referrals in JNDI</a> 309 */ 310 public String getReferral() { 311 return (String) getEnvironmentProperty(Context.REFERRAL); 312 } 313 314 /** 315 * The LDAP url to connect to. (e.g. ldap://<ldapDirectoryHostname>:<port>). This must be configured. 316 * 317 * @param url the LDAP url to connect to. (e.g. ldap://<ldapDirectoryHostname>:<port>) 318 */ 319 public void setUrl(String url) { 320 setEnvironmentProperty(Context.PROVIDER_URL, url); 321 } 322 323 /** 324 * Returns the LDAP url to connect to. (e.g. ldap://<ldapDirectoryHostname>:<port>). 325 * This must be configured. 326 * 327 * @return the LDAP url to connect to. (e.g. ldap://<ldapDirectoryHostname>:<port>) 328 */ 329 public String getUrl() { 330 return (String) getEnvironmentProperty(Context.PROVIDER_URL); 331 } 332 333 /** 334 * Sets the password of the {@link #setSystemUsername(String) systemUsername} that will be used when creating an 335 * LDAP connection used for authorization queries. 336 * <p/> 337 * Note that setting this property is not required if the calling LDAP Realm does not perform authorization 338 * checks. 339 * 340 * @param systemPassword the password of the {@link #setSystemUsername(String) systemUsername} that will be used 341 * when creating an LDAP connection used for authorization queries. 342 */ 343 public void setSystemPassword(String systemPassword) { 344 this.systemPassword = systemPassword; 345 } 346 347 /** 348 * Returns the password of the {@link #setSystemUsername(String) systemUsername} that will be used when creating an 349 * LDAP connection used for authorization queries. 350 * <p/> 351 * Note that setting this property is not required if the calling LDAP Realm does not perform authorization 352 * checks. 353 * 354 * @return the password of the {@link #setSystemUsername(String) systemUsername} that will be used when creating an 355 * LDAP connection used for authorization queries. 356 */ 357 public String getSystemPassword() { 358 return this.systemPassword; 359 } 360 361 /** 362 * Sets the system username that will be used when creating an LDAP connection used for authorization queries. 363 * The user must have the ability to query for authorization data for any application user. 364 * <p/> 365 * Note that setting this property is not required if the calling LDAP Realm does not perform authorization 366 * checks. 367 * 368 * @param systemUsername the system username that will be used when creating an LDAP connection used for 369 * authorization queries. 370 */ 371 public void setSystemUsername(String systemUsername) { 372 this.systemUsername = systemUsername; 373 } 374 375 /** 376 * Returns the system username that will be used when creating an LDAP connection used for authorization queries. 377 * The user must have the ability to query for authorization data for any application user. 378 * <p/> 379 * Note that setting this property is not required if the calling LDAP Realm does not perform authorization 380 * checks. 381 * 382 * @return the system username that will be used when creating an LDAP connection used for authorization queries. 383 */ 384 public String getSystemUsername() { 385 return systemUsername; 386 } 387 388 /*-------------------------------------------- 389 | M E T H O D S | 390 ============================================*/ 391 392 /** 393 * This implementation delegates to {@link #getLdapContext(Object, Object)} using the 394 * {@link #getSystemUsername() systemUsername} and {@link #getSystemPassword() systemPassword} properties as 395 * arguments. 396 * 397 * @return the system LdapContext 398 * @throws NamingException if there is a problem connecting to the LDAP directory 399 */ 400 public LdapContext getSystemLdapContext() throws NamingException { 401 return getLdapContext((Object)getSystemUsername(), getSystemPassword()); 402 } 403 404 /** 405 * Deprecated - use {@link #getLdapContext(Object, Object)} instead. This will be removed before Apache Shiro 2.0. 406 * 407 * @param username the username to use when creating the connection. 408 * @param password the password to use when creating the connection. 409 * @return a {@code LdapContext} bound using the given username and password. 410 * @throws javax.naming.NamingException if there is an error creating the context. 411 * @deprecated the {@link #getLdapContext(Object, Object)} method should be used in all cases to ensure more than 412 * String principals and credentials can be used. Shiro no longer calls this method - it will be 413 * removed before the 2.0 release. 414 */ 415 @Deprecated 416 public LdapContext getLdapContext(String username, String password) throws NamingException { 417 return getLdapContext((Object) username, password); 418 } 419 420 /** 421 * Returns {@code true} if LDAP connection pooling should be used when acquiring a connection based on the specified 422 * account principal, {@code false} otherwise. 423 * <p/> 424 * This implementation returns {@code true} only if {@link #isPoolingEnabled()} and the principal equals the 425 * {@link #getSystemUsername()}. The reasoning behind this is that connection pooling is not desirable for 426 * general authentication attempts by application end-users because the probability of re-use for that same 427 * user-specific connection after an authentication attempt is extremely low. 428 * 429 * @param principal the principal under which the connection will be made 430 * @return {@code true} if LDAP connection pooling should be used when acquiring a connection based on the specified 431 * account principal, {@code false} otherwise. 432 */ 433 protected boolean isPoolingConnections(Object principal) { 434 return isPoolingEnabled() && principal != null && principal.equals(getSystemUsername()); 435 } 436 437 /** 438 * This implementation returns an LdapContext based on the configured JNDI/LDAP environment configuration. 439 * The environnmet (Map) used at runtime is created by merging the default/configured 440 * {@link #getEnvironment() environment template} with some runtime values as necessary (e.g. a principal and 441 * credential available at runtime only). 442 * <p/> 443 * After the merged Map instance is created, the LdapContext connection is 444 * {@link #createLdapContext(java.util.Hashtable) created} and returned. 445 * 446 * @param principal the principal to use when acquiring a connection to the LDAP directory 447 * @param credentials the credentials (password, X.509 certificate, etc) to use when acquiring a connection to the 448 * LDAP directory 449 * @return the acquired {@code LdapContext} connection bound using the specified principal and credentials. 450 * @throws NamingException 451 * @throws IllegalStateException 452 */ 453 public LdapContext getLdapContext(Object principal, Object credentials) throws NamingException, 454 IllegalStateException { 455 456 String url = getUrl(); 457 if (url == null) { 458 throw new IllegalStateException("An LDAP URL must be specified of the form ldap://<hostname>:<port>"); 459 } 460 461 //copy the environment template into the runtime instance that will be further edited based on 462 //the method arguments and other class attributes. 463 Hashtable<String, Object> env = new Hashtable<String, Object>(this.environment); 464 465 Object authcMech = getAuthenticationMechanism(); 466 if (authcMech == null && (principal != null || credentials != null)) { 467 //authenticationMechanism has not been set, but either a principal and/or credentials were 468 //supplied, indicating that at least a 'simple' authentication attempt is indeed occurring - the Shiro 469 //end-user just didn't configure it explicitly. So we set it to be 'simple' here as a convenience; 470 //the Sun provider implementation already does this same logic, but by repeating that logic here, we ensure 471 //this convenience exists regardless of provider implementation): 472 env.put(Context.SECURITY_AUTHENTICATION, SIMPLE_AUTHENTICATION_MECHANISM_NAME); 473 } 474 if (principal != null) { 475 env.put(Context.SECURITY_PRINCIPAL, principal); 476 } 477 if (credentials != null) { 478 env.put(Context.SECURITY_CREDENTIALS, credentials); 479 } 480 481 boolean pooling = isPoolingConnections(principal); 482 if (pooling) { 483 env.put(SUN_CONNECTION_POOLING_PROPERTY, "true"); 484 } 485 486 if (log.isDebugEnabled()) { 487 log.debug("Initializing LDAP context using URL [{}] and principal [{}] with pooling {}", 488 new Object[]{url, principal, (pooling ? "enabled" : "disabled")}); 489 } 490 491 return createLdapContext(env); 492 } 493 494 /** 495 * Creates and returns a new {@link javax.naming.ldap.InitialLdapContext} instance. This method exists primarily 496 * to support testing where a mock LdapContext can be returned instead of actually creating a connection, but 497 * subclasses are free to provide a different implementation if necessary. 498 * 499 * @param env the JNDI environment settings used to create the LDAP connection 500 * @return an LdapConnection 501 * @throws NamingException if a problem occurs creating the connection 502 */ 503 protected LdapContext createLdapContext(Hashtable env) throws NamingException { 504 return new InitialLdapContext(env, null); 505 } 506 507 }