Coverage Report - org.apache.shiro.realm.ldap.JndiLdapRealm
 
Classes in this File Line Coverage Branch Coverage Complexity
JndiLdapRealm
86%
64/74
72%
16/22
2.786
 
 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.authc.AuthenticationException;
 22  
 import org.apache.shiro.authc.AuthenticationInfo;
 23  
 import org.apache.shiro.authc.AuthenticationToken;
 24  
 import org.apache.shiro.authc.SimpleAuthenticationInfo;
 25  
 import org.apache.shiro.authc.credential.AllowAllCredentialsMatcher;
 26  
 import org.apache.shiro.authz.AuthorizationException;
 27  
 import org.apache.shiro.authz.AuthorizationInfo;
 28  
 import org.apache.shiro.ldap.UnsupportedAuthenticationMechanismException;
 29  
 import org.apache.shiro.realm.AuthorizingRealm;
 30  
 import org.apache.shiro.subject.PrincipalCollection;
 31  
 import org.apache.shiro.util.StringUtils;
 32  
 import org.slf4j.Logger;
 33  
 import org.slf4j.LoggerFactory;
 34  
 
 35  
 import javax.naming.AuthenticationNotSupportedException;
 36  
 import javax.naming.NamingException;
 37  
 import javax.naming.ldap.LdapContext;
 38  
 
 39  
 /**
 40  
  * An LDAP {@link org.apache.shiro.realm.Realm Realm} implementation utilizing Sun's/Oracle's
 41  
  * <a href="http://download-llnw.oracle.com/javase/tutorial/jndi/ldap/jndi.html">JNDI API as an LDAP API</a>.  This is
 42  
  * Shiro's default implementation for supporting LDAP, as using the JNDI API has been a common approach for Java LDAP
 43  
  * support for many years.
 44  
  * <p/>
 45  
  * This realm implementation and its backing {@link JndiLdapContextFactory} should cover 99% of all Shiro-related LDAP
 46  
  * authentication and authorization needs.  However, if it does not suit your needs, you might want to look into
 47  
  * creating your own realm using an alternative, perhaps more robust, LDAP communication API, such as the
 48  
  * <a href="http://directory.apache.org/api/">Apache LDAP API</a>.
 49  
  * <h2>Authentication</h2>
 50  
  * During an authentication attempt, if the submitted {@code AuthenticationToken}'s
 51  
  * {@link org.apache.shiro.authc.AuthenticationToken#getPrincipal() principal} is a simple username, but the
 52  
  * LDAP directory expects a complete User Distinguished Name (User DN) to establish a connection, the
 53  
  * {@link #setUserDnTemplate(String) userDnTemplate} property must be configured.  If not configured,
 54  
  * the property will pass the simple username directly as the User DN, which is often incorrect in most LDAP
 55  
  * environments (maybe Microsoft ActiveDirectory being the exception).
 56  
  * <h2>Authorization</h2>
 57  
  * By default, authorization is effectively disabled due to the default
 58  
  * {@link #doGetAuthorizationInfo(org.apache.shiro.subject.PrincipalCollection)} implementation returning {@code null}.
 59  
  * If you wish to perform authorization based on an LDAP schema, you must subclass this one
 60  
  * and override that method to reflect your organization's data model.
 61  
  * <h2>Configuration</h2>
 62  
  * This class primarily provides the {@link #setUserDnTemplate(String) userDnTemplate} property to allow you to specify
 63  
  * the your LDAP server's User DN format.  Most other configuration is performed via the nested
 64  
  * {@link LdapContextFactory contextFactory} property.
 65  
  * <p/>
 66  
  * For example, defining this realm in Shiro .ini:
 67  
  * <pre>
 68  
  * [main]
 69  
  * ldapRealm = org.apache.shiro.realm.ldap.JndiLdapRealm
 70  
  * ldapRealm.userDnTemplate = uid={0},ou=users,dc=mycompany,dc=com
 71  
  * ldapRealm.contextFactory.url = ldap://ldapHost:389
 72  
  * ldapRealm.contextFactory.authenticationMechanism = DIGEST-MD5
 73  
  * ldapRealm.contextFactory.environment[some.obscure.jndi.key] = some value
 74  
  * ...
 75  
  * </pre>
 76  
  * The default {@link #setContextFactory contextFactory} instance is a {@link JndiLdapContextFactory}.  See that
 77  
  * class's JavaDoc for more information on configuring the LDAP connection as well as specifying JNDI environment
 78  
  * properties as necessary.
 79  
  *
 80  
  * @see JndiLdapContextFactory
 81  
  *
 82  
  * @since 1.1
 83  
  */
 84  
 public class JndiLdapRealm extends AuthorizingRealm {
 85  
 
 86  2
     private static final Logger log = LoggerFactory.getLogger(JndiLdapRealm.class);
 87  
 
 88  
     //The zero index currently means nothing, but could be utilized in the future for other substitution techniques.
 89  
     private static final String USERDN_SUBSTITUTION_TOKEN = "{0}";
 90  
 
 91  
     private String userDnPrefix;
 92  
     private String userDnSuffix;
 93  
 
 94  
     /*--------------------------------------------
 95  
     |    I N S T A N C E   V A R I A B L E S    |
 96  
     ============================================*/
 97  
     /**
 98  
      * The LdapContextFactory instance used to acquire {@link javax.naming.ldap.LdapContext LdapContext}'s at runtime
 99  
      * to acquire connections to the LDAP directory to perform authentication attempts and authorizatino queries.
 100  
      */
 101  
     private LdapContextFactory contextFactory;
 102  
 
 103  
     /*--------------------------------------------
 104  
     |         C O N S T R U C T O R S           |
 105  
     ============================================*/
 106  
 
 107  
     /**
 108  
      * Default no-argument constructor that defaults the internal {@link LdapContextFactory} instance to a
 109  
      * {@link JndiLdapContextFactory}.
 110  
      */
 111  26
     public JndiLdapRealm() {
 112  
         //Credentials Matching is not necessary - the LDAP directory will do it automatically:
 113  26
         setCredentialsMatcher(new AllowAllCredentialsMatcher());
 114  
         //Any Object principal and Object credentials may be passed to the LDAP provider, so accept any token:
 115  26
         setAuthenticationTokenClass(AuthenticationToken.class);
 116  26
         this.contextFactory = new JndiLdapContextFactory();
 117  26
     }
 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  
      * Returns the User DN prefix to use when building a runtime User DN value or {@code null} if no
 125  
      * {@link #getUserDnTemplate() userDnTemplate} has been configured.  If configured, this value is the text that
 126  
      * occurs before the {@link #USERDN_SUBSTITUTION_TOKEN} in the {@link #getUserDnTemplate() userDnTemplate} value.
 127  
      *
 128  
      * @return the the User DN prefix to use when building a runtime User DN value or {@code null} if no
 129  
      *         {@link #getUserDnTemplate() userDnTemplate} has been configured.
 130  
      */
 131  
     protected String getUserDnPrefix() {
 132  8
         return userDnPrefix;
 133  
     }
 134  
 
 135  
     /**
 136  
      * Returns the User DN suffix to use when building a runtime User DN value.  or {@code null} if no
 137  
      * {@link #getUserDnTemplate() userDnTemplate} has been configured.  If configured, this value is the text that
 138  
      * occurs after the {@link #USERDN_SUBSTITUTION_TOKEN} in the {@link #getUserDnTemplate() userDnTemplate} value.
 139  
      *
 140  
      * @return the User DN suffix to use when building a runtime User DN value or {@code null} if no
 141  
      *         {@link #getUserDnTemplate() userDnTemplate} has been configured.
 142  
      */
 143  
     protected String getUserDnSuffix() {
 144  8
         return userDnSuffix;
 145  
     }
 146  
 
 147  
     /*--------------------------------------------
 148  
     |               M E T H O D S               |
 149  
     ============================================*/
 150  
 
 151  
     /**
 152  
      * Sets the User Distinguished Name (DN) template to use when creating User DNs at runtime.  A User DN is an LDAP
 153  
      * fully-qualified unique user identifier which is required to establish a connection with the LDAP
 154  
      * directory to authenticate users and query for authorization information.
 155  
      * <h2>Usage</h2>
 156  
      * User DN formats are unique to the LDAP directory's schema, and each environment differs - you will need to
 157  
      * specify the format corresponding to your directory.  You do this by specifying the full User DN as normal, but
 158  
      * but you use a <b>{@code {0}}</b> placeholder token in the string representing the location where the
 159  
      * user's submitted principal (usually a username or uid) will be substituted at runtime.
 160  
      * <p/>
 161  
      * For example,  if your directory
 162  
      * uses an LDAP {@code uid} attribute to represent usernames, the User DN for the {@code jsmith} user may look like
 163  
      * this:
 164  
      * <p/>
 165  
      * <pre>uid=jsmith,ou=users,dc=mycompany,dc=com</pre>
 166  
      * <p/>
 167  
      * in which case you would set this property with the following template value:
 168  
      * <p/>
 169  
      * <pre>uid=<b>{0}</b>,ou=users,dc=mycompany,dc=com</pre>
 170  
      * <p/>
 171  
      * If no template is configured, the raw {@code AuthenticationToken}
 172  
      * {@link AuthenticationToken#getPrincipal() principal} will be used as the LDAP principal.  This is likely
 173  
      * incorrect as most LDAP directories expect a fully-qualified User DN as opposed to the raw uid or username.  So,
 174  
      * ensure you set this property to match your environment!
 175  
      *
 176  
      * @param template the User Distinguished Name template to use for runtime substitution
 177  
      * @throws IllegalArgumentException if the template is null, empty, or does not contain the
 178  
      *                                  {@code {0}} substitution token.
 179  
      * @see LdapContextFactory#getLdapContext(Object,Object)
 180  
      */
 181  
     public void setUserDnTemplate(String template) throws IllegalArgumentException {
 182  16
         if (!StringUtils.hasText(template)) {
 183  4
             String msg = "User DN template cannot be null or empty.";
 184  4
             throw new IllegalArgumentException(msg);
 185  
         }
 186  12
         int index = template.indexOf(USERDN_SUBSTITUTION_TOKEN);
 187  12
         if (index < 0) {
 188  2
             String msg = "User DN template must contain the '" +
 189  
                     USERDN_SUBSTITUTION_TOKEN + "' replacement token to understand where to " +
 190  
                     "insert the runtime authentication principal.";
 191  2
             throw new IllegalArgumentException(msg);
 192  
         }
 193  10
         String prefix = template.substring(0, index);
 194  10
         String suffix = template.substring(prefix.length() + USERDN_SUBSTITUTION_TOKEN.length());
 195  10
         if (log.isDebugEnabled()) {
 196  10
             log.debug("Determined user DN prefix [{}] and suffix [{}]", prefix, suffix);
 197  
         }
 198  10
         this.userDnPrefix = prefix;
 199  10
         this.userDnSuffix = suffix;
 200  10
     }
 201  
 
 202  
     /**
 203  
      * Returns the User Distinguished Name (DN) template to use when creating User DNs at runtime - see the
 204  
      * {@link #setUserDnTemplate(String) setUserDnTemplate} JavaDoc for a full explanation.
 205  
      *
 206  
      * @return the User Distinguished Name (DN) template to use when creating User DNs at runtime.
 207  
      */
 208  
     public String getUserDnTemplate() {
 209  2
         return getUserDn(USERDN_SUBSTITUTION_TOKEN);
 210  
     }
 211  
 
 212  
     /**
 213  
      * Returns the LDAP User Distinguished Name (DN) to use when acquiring an
 214  
      * {@link javax.naming.ldap.LdapContext LdapContext} from the {@link LdapContextFactory}.
 215  
      * <p/>
 216  
      * If the the {@link #getUserDnTemplate() userDnTemplate} property has been set, this implementation will construct
 217  
      * the User DN by substituting the specified {@code principal} into the configured template.  If the
 218  
      * {@link #getUserDnTemplate() userDnTemplate} has not been set, the method argument will be returned directly
 219  
      * (indicating that the submitted authentication token principal <em>is</em> the User DN).
 220  
      *
 221  
      * @param principal the principal to substitute into the configured {@link #getUserDnTemplate() userDnTemplate}.
 222  
      * @return the constructed User DN to use at runtime when acquiring an {@link javax.naming.ldap.LdapContext}.
 223  
      * @throws IllegalArgumentException if the method argument is null or empty
 224  
      * @throws IllegalStateException    if the {@link #getUserDnTemplate userDnTemplate} has not been set.
 225  
      * @see LdapContextFactory#getLdapContext(Object, Object)
 226  
      */
 227  
     protected String getUserDn(String principal) throws IllegalArgumentException, IllegalStateException {
 228  12
         if (!StringUtils.hasText(principal)) {
 229  2
             throw new IllegalArgumentException("User principal cannot be null or empty for User DN construction.");
 230  
         }
 231  10
         String prefix = getUserDnPrefix();
 232  10
         String suffix = getUserDnSuffix();
 233  10
         if (prefix == null && suffix == null) {
 234  2
             log.debug("userDnTemplate property has not been configured, indicating the submitted " +
 235  
                     "AuthenticationToken's principal is the same as the User DN.  Returning the method argument " +
 236  
                     "as is.");
 237  2
             return principal;
 238  
         }
 239  
 
 240  8
         int prefixLength = prefix != null ? prefix.length() : 0;
 241  8
         int suffixLength = suffix != null ? suffix.length() : 0;
 242  8
         StringBuilder sb = new StringBuilder(prefixLength + principal.length() + suffixLength);
 243  8
         if (prefixLength > 0) {
 244  8
             sb.append(prefix);
 245  
         }
 246  8
         sb.append(principal);
 247  8
         if (suffixLength > 0) {
 248  8
             sb.append(suffix);
 249  
         }
 250  8
         return sb.toString();
 251  
     }
 252  
 
 253  
     /**
 254  
      * Sets the LdapContextFactory instance used to acquire connections to the LDAP directory during authentication
 255  
      * attempts and authorization queries.  Unless specified otherwise, the default is a {@link JndiLdapContextFactory}
 256  
      * instance.
 257  
      *
 258  
      * @param contextFactory the LdapContextFactory instance used to acquire connections to the LDAP directory during
 259  
      *                       authentication attempts and authorization queries
 260  
      */
 261  
     @SuppressWarnings({"UnusedDeclaration"})
 262  
     public void setContextFactory(LdapContextFactory contextFactory) {
 263  8
         this.contextFactory = contextFactory;
 264  8
     }
 265  
 
 266  
     /**
 267  
      * Returns the LdapContextFactory instance used to acquire connections to the LDAP directory during authentication
 268  
      * attempts and authorization queries.  Unless specified otherwise, the default is a {@link JndiLdapContextFactory}
 269  
      * instance.
 270  
      *
 271  
      * @return the LdapContextFactory instance used to acquire connections to the LDAP directory during
 272  
      *         authentication attempts and authorization queries
 273  
      */
 274  
     public LdapContextFactory getContextFactory() {
 275  28
         return this.contextFactory;
 276  
     }
 277  
 
 278  
     /*--------------------------------------------
 279  
     |               M E T H O D S                |
 280  
     ============================================*/
 281  
 
 282  
     /**
 283  
      * Delegates to {@link #queryForAuthenticationInfo(org.apache.shiro.authc.AuthenticationToken, LdapContextFactory)},
 284  
      * wrapping any {@link NamingException}s in a Shiro {@link AuthenticationException} to satisfy the parent method
 285  
      * signature.
 286  
      *
 287  
      * @param token the authentication token containing the user's principal and credentials.
 288  
      * @return the {@link AuthenticationInfo} acquired after a successful authentication attempt
 289  
      * @throws AuthenticationException if the authentication attempt fails or if a
 290  
      *                                 {@link NamingException} occurs.
 291  
      */
 292  
     protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
 293  
         AuthenticationInfo info;
 294  
         try {
 295  8
             info = queryForAuthenticationInfo(token, getContextFactory());
 296  0
         } catch (AuthenticationNotSupportedException e) {
 297  0
             String msg = "Unsupported configured authentication mechanism";
 298  0
             throw new UnsupportedAuthenticationMechanismException(msg, e);
 299  2
         } catch (javax.naming.AuthenticationException e) {
 300  2
             throw new AuthenticationException("LDAP authentication failed.", e);
 301  2
         } catch (NamingException e) {
 302  2
             String msg = "LDAP naming error while attempting to authenticate user.";
 303  2
             throw new AuthenticationException(msg, e);
 304  4
         }
 305  
 
 306  4
         return info;
 307  
     }
 308  
 
 309  
 
 310  
     protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
 311  
         AuthorizationInfo info;
 312  
         try {
 313  0
             info = queryForAuthorizationInfo(principals, getContextFactory());
 314  0
         } catch (NamingException e) {
 315  0
             String msg = "LDAP naming error while attempting to retrieve authorization for user [" + principals + "].";
 316  0
             throw new AuthorizationException(msg, e);
 317  0
         }
 318  
 
 319  0
         return info;
 320  
     }
 321  
 
 322  
     /**
 323  
      * Returns the principal to use when creating the LDAP connection for an authentication attempt.
 324  
      * <p/>
 325  
      * This implementation uses a heuristic: it checks to see if the specified token's
 326  
      * {@link AuthenticationToken#getPrincipal() principal} is a {@code String}, and if so,
 327  
      * {@link #getUserDn(String) converts it} from what is
 328  
      * assumed to be a raw uid or username {@code String} into a User DN {@code String}.  Almost all LDAP directories
 329  
      * expect the authentication connection to present a User DN and not an unqualified username or uid.
 330  
      * <p/>
 331  
      * If the token's {@code principal} is not a String, it is assumed to already be in the format supported by the
 332  
      * underlying {@link LdapContextFactory} implementation and the raw principal is returned directly.
 333  
      *
 334  
      * @param token the {@link AuthenticationToken} submitted during the authentication process
 335  
      * @return the User DN or raw principal to use to acquire the LdapContext.
 336  
      * @see LdapContextFactory#getLdapContext(Object, Object)
 337  
      */
 338  
     protected Object getLdapPrincipal(AuthenticationToken token) {
 339  8
         Object principal = token.getPrincipal();
 340  8
         if (principal instanceof String) {
 341  6
             String sPrincipal = (String) principal;
 342  6
             return getUserDn(sPrincipal);
 343  
         }
 344  2
         return principal;
 345  
     }
 346  
 
 347  
     /**
 348  
      * This implementation opens an LDAP connection using the token's
 349  
      * {@link #getLdapPrincipal(org.apache.shiro.authc.AuthenticationToken) discovered principal} and provided
 350  
      * {@link AuthenticationToken#getCredentials() credentials}.  If the connection opens successfully, the
 351  
      * authentication attempt is immediately considered successful and a new
 352  
      * {@link AuthenticationInfo} instance is
 353  
      * {@link #createAuthenticationInfo(org.apache.shiro.authc.AuthenticationToken, Object, Object, javax.naming.ldap.LdapContext) created}
 354  
      * and returned.  If the connection cannot be opened, either because LDAP authentication failed or some other
 355  
      * JNDI problem, an {@link NamingException} will be thrown.
 356  
      *
 357  
      * @param token              the submitted authentication token that triggered the authentication attempt.
 358  
      * @param ldapContextFactory factory used to retrieve LDAP connections.
 359  
      * @return an {@link AuthenticationInfo} instance representing the authenticated user's information.
 360  
      * @throws NamingException if any LDAP errors occur.
 361  
      */
 362  
     protected AuthenticationInfo queryForAuthenticationInfo(AuthenticationToken token,
 363  
                                                             LdapContextFactory ldapContextFactory)
 364  
             throws NamingException {
 365  
 
 366  8
         Object principal = token.getPrincipal();
 367  8
         Object credentials = token.getCredentials();
 368  
 
 369  8
         log.debug("Authenticating user '{}' through LDAP", principal);
 370  
 
 371  8
         principal = getLdapPrincipal(token);
 372  
 
 373  8
         LdapContext ctx = null;
 374  
         try {
 375  8
             ctx = ldapContextFactory.getLdapContext(principal, credentials);
 376  
             //context was opened successfully, which means their credentials were valid.  Return the AuthenticationInfo:
 377  8
             return createAuthenticationInfo(token, principal, credentials, ctx);
 378  
         } finally {
 379  8
             LdapUtils.closeContext(ctx);
 380  
         }
 381  
     }
 382  
 
 383  
     /**
 384  
      * Returns the {@link AuthenticationInfo} resulting from a Subject's successful LDAP authentication attempt.
 385  
      * <p/>
 386  
      * This implementation ignores the {@code ldapPrincipal}, {@code ldapCredentials}, and the opened
 387  
      * {@code ldapContext} arguments and merely returns an {@code AuthenticationInfo} instance mirroring the
 388  
      * submitted token's principal and credentials.  This is acceptable because this method is only ever invoked after
 389  
      * a successful authentication attempt, which means the provided principal and credentials were correct, and can
 390  
      * be used directly to populate the (now verified) {@code AuthenticationInfo}.
 391  
      * <p/>
 392  
      * Subclasses however are free to override this method for more advanced construction logic.
 393  
      *
 394  
      * @param token           the submitted {@code AuthenticationToken} that resulted in a successful authentication
 395  
      * @param ldapPrincipal   the LDAP principal used when creating the LDAP connection.  Unlike the token's
 396  
      *                        {@link AuthenticationToken#getPrincipal() principal}, this value is usually a constructed
 397  
      *                        User DN and not a simple username or uid.  The exact value is depending on the
 398  
      *                        configured
 399  
      *                        <a href="http://download-llnw.oracle.com/javase/tutorial/jndi/ldap/auth_mechs.html">
 400  
      *                        LDAP authentication mechanism</a> in use.
 401  
      * @param ldapCredentials the LDAP credentials used when creating the LDAP connection.
 402  
      * @param ldapContext     the LdapContext created that resulted in a successful authentication.  It can be used
 403  
      *                        further by subclasses for more complex operations.  It does not need to be closed -
 404  
      *                        it will be closed automatically after this method returns.
 405  
      * @return the {@link AuthenticationInfo} resulting from a Subject's successful LDAP authentication attempt.
 406  
      * @throws NamingException if there was any problem using the {@code LdapContext}
 407  
      */
 408  
     @SuppressWarnings({"UnusedDeclaration"})
 409  
     protected AuthenticationInfo createAuthenticationInfo(AuthenticationToken token, Object ldapPrincipal,
 410  
                                                           Object ldapCredentials, LdapContext ldapContext)
 411  
             throws NamingException {
 412  4
         return new SimpleAuthenticationInfo(token.getPrincipal(), token.getCredentials(), getName());
 413  
     }
 414  
 
 415  
 
 416  
     /**
 417  
      * Method that should be implemented by subclasses to build an
 418  
      * {@link AuthorizationInfo} object by querying the LDAP context for the
 419  
      * specified principal.</p>
 420  
      *
 421  
      * @param principals          the principals of the Subject whose AuthenticationInfo should be queried from the LDAP server.
 422  
      * @param ldapContextFactory factory used to retrieve LDAP connections.
 423  
      * @return an {@link AuthorizationInfo} instance containing information retrieved from the LDAP server.
 424  
      * @throws NamingException if any LDAP errors occur during the search.
 425  
      */
 426  
     protected AuthorizationInfo queryForAuthorizationInfo(PrincipalCollection principals,
 427  
                                                           LdapContextFactory ldapContextFactory) throws NamingException {
 428  0
         return null;
 429  
     }
 430  
 }