You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@shiro.apache.org by lh...@apache.org on 2010/08/08 04:30:28 UTC

svn commit: r983338 - in /incubator/shiro/trunk: core/src/main/java/org/apache/shiro/realm/ldap/ core/src/test/java/org/apache/shiro/realm/ldap/ samples/spring-client/

Author: lhazlewood
Date: Sun Aug  8 02:30:28 2010
New Revision: 983338

URL: http://svn.apache.org/viewvc?rev=983338&view=rev
Log:
SHIRO-127: re-work of LDAP support.  First initial commit - introduced two new classes, JndiLdapRealm and JndiLdapContextFactory that effectively supercede the now-deprecated AbstractLdapRealm and DefaultJndiContextFactory.  Test coverage for the new classes reaches 90% and 100% respectively.  We should be able to get to 100% once Authorization is flushed out.  Discussion to ensue on the list before this issue should be closed...

Added:
    incubator/shiro/trunk/core/src/main/java/org/apache/shiro/realm/ldap/JndiLdapContextFactory.java
    incubator/shiro/trunk/core/src/main/java/org/apache/shiro/realm/ldap/JndiLdapRealm.java
    incubator/shiro/trunk/core/src/test/java/org/apache/shiro/realm/ldap/
    incubator/shiro/trunk/core/src/test/java/org/apache/shiro/realm/ldap/JndiLdapContextFactoryTest.java
    incubator/shiro/trunk/core/src/test/java/org/apache/shiro/realm/ldap/JndiLdapRealmTest.java
Modified:
    incubator/shiro/trunk/core/src/main/java/org/apache/shiro/realm/ldap/DefaultLdapContextFactory.java
    incubator/shiro/trunk/core/src/main/java/org/apache/shiro/realm/ldap/LdapContextFactory.java
    incubator/shiro/trunk/core/src/main/java/org/apache/shiro/realm/ldap/LdapUtils.java
    incubator/shiro/trunk/samples/spring-client/   (props changed)

Modified: incubator/shiro/trunk/core/src/main/java/org/apache/shiro/realm/ldap/DefaultLdapContextFactory.java
URL: http://svn.apache.org/viewvc/incubator/shiro/trunk/core/src/main/java/org/apache/shiro/realm/ldap/DefaultLdapContextFactory.java?rev=983338&r1=983337&r2=983338&view=diff
==============================================================================
--- incubator/shiro/trunk/core/src/main/java/org/apache/shiro/realm/ldap/DefaultLdapContextFactory.java (original)
+++ incubator/shiro/trunk/core/src/main/java/org/apache/shiro/realm/ldap/DefaultLdapContextFactory.java Sun Aug  8 02:30:28 2010
@@ -31,15 +31,18 @@ import org.slf4j.LoggerFactory;
 /**
  * <p>Default implementation of {@link LdapContextFactory} that can be configured or extended to
  * customize the way {@link javax.naming.ldap.LdapContext} objects are retrieved.</p>
- *
+ * <p/>
  * <p>This implementation of {@link LdapContextFactory} is used by the {@link AbstractLdapRealm} if a
  * factory is not explictly configured.</p>
- *
+ * <p/>
  * <p>Connection pooling is enabled by default on this factory, but can be disabled using the
  * {@link #usePooling} property.</p>
  *
  * @since 0.2
+ * @deprecated replaced by the {@link JndiLdapContextFactory} implementation.  This implementation will be removed
+ * prior to Shiro 2.0
  */
+@Deprecated
 public class DefaultLdapContextFactory implements LdapContextFactory {
 
     //TODO - complete JavaDoc
@@ -111,7 +114,9 @@ public class DefaultLdapContextFactory i
      * (e.g. OU=OrganizationName,DC=MyDomain,DC=local )
      *
      * @param searchBase the search base.
+     * @deprecated this attribute existed, but was never used in Shiro 1.x.  It will be removed prior to Shiro 2.0.
      */
+    @Deprecated
     public void setSearchBase(String searchBase) {
         this.searchBase = searchBase;
     }
@@ -193,38 +198,49 @@ public class DefaultLdapContextFactory i
     /*--------------------------------------------
     |               M E T H O D S               |
     ============================================*/
-
     public LdapContext getSystemLdapContext() throws NamingException {
         return getLdapContext(systemUsername, systemPassword);
     }
 
+    /**
+     * Deprecated - use {@link #getLdapContext(Object, Object)} instead.  This will be removed before Apache Shiro 2.0.
+     *
+     * @param username the username to use when creating the connection.
+     * @param password the password to use when creating the connection.
+     * @return a {@code LdapContext} bound using the given username and password.
+     * @throws javax.naming.NamingException if there is an error creating the context.
+     * @deprecated the {@link #getLdapContext(Object, Object)} method should be used in all cases to ensure more than
+     *             String principals and credentials can be used.  Shiro no longer calls this method - it will be
+     *             removed before the 2.0 release.
+     */
+    @Deprecated
     public LdapContext getLdapContext(String username, String password) throws NamingException {
-        if (searchBase == null) {
-            throw new IllegalStateException("A search base must be specified.");
+        if (username != null && principalSuffix != null) {
+            username += principalSuffix;
         }
+        return getLdapContext((Object) username, password);
+    }
+
+    public LdapContext getLdapContext(Object principal, Object credentials) throws NamingException {
         if (url == null) {
             throw new IllegalStateException("An LDAP URL must be specified of the form ldap://<hostname>:<port>");
         }
 
-        if (username != null && principalSuffix != null) {
-            username += principalSuffix;
-        }
-
-        Hashtable<String, String> env = new Hashtable<String, String>();
+        Hashtable<String, Object> env = new Hashtable<String, Object>();
 
         env.put(Context.SECURITY_AUTHENTICATION, authentication);
-        if (username != null) {
-            env.put(Context.SECURITY_PRINCIPAL, username);
+        if (principal != null) {
+            env.put(Context.SECURITY_PRINCIPAL, principal);
         }
-        if (password != null) {
-            env.put(Context.SECURITY_CREDENTIALS, password);
+        if (credentials!= null) {
+            env.put(Context.SECURITY_CREDENTIALS, credentials);
         }
         env.put(Context.INITIAL_CONTEXT_FACTORY, contextFactoryClassName);
         env.put(Context.PROVIDER_URL, url);
         env.put(Context.REFERRAL, referral);
 
         // Only pool connections for system contexts
-        if (usePooling && username != null && username.equals(systemUsername)) {
+        if (usePooling && principal != null && principal.equals(systemUsername)) {
             // Enable connection pooling
             env.put(SUN_CONNECTION_POOLING_PROPERTY, "true");
         }

Added: incubator/shiro/trunk/core/src/main/java/org/apache/shiro/realm/ldap/JndiLdapContextFactory.java
URL: http://svn.apache.org/viewvc/incubator/shiro/trunk/core/src/main/java/org/apache/shiro/realm/ldap/JndiLdapContextFactory.java?rev=983338&view=auto
==============================================================================
--- incubator/shiro/trunk/core/src/main/java/org/apache/shiro/realm/ldap/JndiLdapContextFactory.java (added)
+++ incubator/shiro/trunk/core/src/main/java/org/apache/shiro/realm/ldap/JndiLdapContextFactory.java Sun Aug  8 02:30:28 2010
@@ -0,0 +1,507 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.shiro.realm.ldap;
+
+import org.apache.shiro.util.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.naming.Context;
+import javax.naming.NamingException;
+import javax.naming.ldap.InitialLdapContext;
+import javax.naming.ldap.LdapContext;
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.Map;
+
+/**
+ * {@link LdapContextFactory} implementation using the default Sun/Oracle JNDI Ldap API, utilizing JNDI
+ * environment properties and an {@link javax.naming.InitialContext}.
+ * <h2>Configuration</h2>
+ * This class basically wraps a default template JNDI environment properties Map.  This properties map is the base
+ * configuration template used to acquire JNDI {@link LdapContext} connections at runtime.  The
+ * {@link #getLdapContext(Object, Object)} method implementation merges this default template with other properties
+ * accessible at runtime only (for example per-method principals and credentials).  The constructed runtime map is the
+ * one used to acquire the {@link LdapContext}.
+ * <p/>
+ * The template can be configured directly via the {@link #getEnvironment()}/{@link #setEnvironment(java.util.Map)}
+ * properties directly if necessary, but it is usually more convenient to use the supporting wrapper get/set methods
+ * for various environment properties.  These wrapper methods interact with the environment
+ * template on your behalf, leaving your configuration cleaner and easier to understand.
+ * <p/>
+ * For example, consider the following two identical configurations:
+ * <pre>
+ * [main]
+ * ldapRealm = org.apache.shiro.realm.ldap.JndiLdapRealm
+ * ldapRealm.contextFactory.url = ldap://localhost:389
+ * ldapRealm.contextFactory.authenticationMechanism = DIGEST-MD5
+ * </pre>
+ * and
+ * <pre>
+ * [main]
+ * ldapRealm = org.apache.shiro.realm.ldap.JndiLdapRealm
+ * ldapRealm.contextFactory.environment[java.naming.provider.url] = ldap://localhost:389
+ * ldapRealm.contextFactory.environment[java.naming.security.authentication] = DIGEST-MD5
+ * </pre>
+ * As you can see, the 2nd configuration block is a little more difficult to read and also requires knowledge
+ * of the underlying JNDI Context property keys.  The first is easier to read and understand.
+ * <p/>
+ * Note that occasionally it will be necessary to use the latter configuration style to set environment properties
+ * where no corresponding wrapper method exists.  In this case, the hybrid approach is still a little easier to read.
+ * For example:
+ * <pre>
+ * [main]
+ * ldapRealm = org.apache.shiro.realm.ldap.JndiLdapRealm
+ * ldapRealm.contextFactory.url = ldap://localhost:389
+ * ldapRealm.contextFactory.authenticationMechanism = DIGEST-MD5
+ * ldapRealm.contextFactory.environment[some.other.obscure.jndi.key] = some value
+ * </pre>
+ *
+ * @since 1.1
+ */
+public class JndiLdapContextFactory implements LdapContextFactory {
+
+    /*-------------------------------------------
+     |             C O N S T A N T S            |
+     ===========================================*/
+    /**
+     * The Sun LDAP property used to enable connection pooling.  This is used in the default implementation
+     * to enable LDAP connection pooling.
+     */
+    protected static final String SUN_CONNECTION_POOLING_PROPERTY = "com.sun.jndi.ldap.connect.pool";
+    protected static final String DEFAULT_CONTEXT_FACTORY_CLASS_NAME = "com.sun.jndi.ldap.LdapCtxFactory";
+    protected static final String SIMPLE_AUTHENTICATION_MECHANISM_NAME = "simple";
+    protected static final String DEFAULT_REFERRAL = "follow";
+
+    private static final Logger log = LoggerFactory.getLogger(JndiLdapContextFactory.class);
+
+    /*-------------------------------------------
+     |    I N S T A N C E   V A R I A B L E S   |
+     ============================================*/
+    private Map<String, Object> environment;
+    private boolean poolingEnabled;
+    private String systemPassword;
+    private String systemUsername;
+
+    /*-------------------------------------------
+     |         C O N S T R U C T O R S          |
+     ===========================================*/
+
+    /**
+     * Default no-argument constructor that initializes the backing {@link #getEnvironment() environment template} with
+     * the {@link #setContextFactoryClassName(String) contextFactoryClassName} equal to
+     * {@code com.sun.jndi.ldap.LdapCtxFactory} (the Sun/Oracle default) and the default
+     * {@link #setReferral(String) referral} behavior to {@code follow}.
+     */
+    public JndiLdapContextFactory() {
+        this.environment = new HashMap<String, Object>();
+        setContextFactoryClassName(DEFAULT_CONTEXT_FACTORY_CLASS_NAME);
+        setReferral(DEFAULT_REFERRAL);
+        poolingEnabled = true;
+    }
+
+    /*-------------------------------------------
+     |  A C C E S S O R S / M O D I F I E R S   |
+     ===========================================*/
+
+    /**
+     * Sets the type of LDAP authentication mechanism to use when connecting to the LDAP server.
+     * This is a wrapper method for setting the JNDI {@link #getEnvironment() environment template}'s
+     * {@link Context#SECURITY_AUTHENTICATION} property.
+     * <p/>
+     * "none" (i.e. anonymous) and "simple" authentications are supported automatically and don't need to be configured
+     * via this property.  However, if you require a different mechanism, such as a SASL or External mechanism, you
+     * must configure that explicitly via this property.  See the
+     * <a href="http://download-llnw.oracle.com/javase/tutorial/jndi/ldap/auth_mechs.html">JNDI LDAP
+     * Authentication Mechanisms</a> for more information.
+     *
+     * @param authenticationMechanism the type of LDAP authentication to perform.
+     * @see <a href="http://download-llnw.oracle.com/javase/tutorial/jndi/ldap/auth_mechs.html">
+     *      http://download-llnw.oracle.com/javase/tutorial/jndi/ldap/auth_mechs.html</a>
+     */
+    public void setAuthenticationMechanism(String authenticationMechanism) {
+        setEnvironmentProperty(Context.SECURITY_AUTHENTICATION, authenticationMechanism);
+    }
+
+    /**
+     * Returns the type of LDAP authentication mechanism to use when connecting to the LDAP server.
+     * This is a wrapper method for getting the JNDI {@link #getEnvironment() environment template}'s
+     * {@link Context#SECURITY_AUTHENTICATION} property.
+     * <p/>
+     * If this property remains un-configured (i.e. {@code null} indicating the
+     * {@link #setAuthenticationMechanism(String)} method wasn't used), this indicates that the default JNDI
+     * "none" (anonymous) and "simple" authentications are supported automatically.  Any non-null value returned
+     * represents an explicitly configured mechanism (e.g. a SASL or external mechanism). See the
+     * <a href="http://download-llnw.oracle.com/javase/tutorial/jndi/ldap/auth_mechs.html">JNDI LDAP
+     * Authentication Mechanisms</a> for more information.
+     *
+     * @return the type of LDAP authentication mechanism to use when connecting to the LDAP server.
+     * @see <a href="http://download-llnw.oracle.com/javase/tutorial/jndi/ldap/auth_mechs.html">
+     *      http://download-llnw.oracle.com/javase/tutorial/jndi/ldap/auth_mechs.html</a>
+     */
+    public String getAuthenticationMechanism() {
+        return (String) getEnvironmentProperty(Context.SECURITY_AUTHENTICATION);
+    }
+
+    /**
+     * The name of the ContextFactory class to use. This defaults to the SUN LDAP JNDI implementation
+     * but can be overridden to use custom LDAP factories.
+     * <p/>
+     * This is a wrapper method for setting the JNDI environment's {@link Context#INITIAL_CONTEXT_FACTORY} property.
+     *
+     * @param contextFactoryClassName the context factory that should be used.
+     */
+    public void setContextFactoryClassName(String contextFactoryClassName) {
+        setEnvironmentProperty(Context.INITIAL_CONTEXT_FACTORY, contextFactoryClassName);
+    }
+
+    /**
+     * Sets the name of the ContextFactory class to use. This defaults to the SUN LDAP JNDI implementation
+     * but can be overridden to use custom LDAP factories.
+     * <p/>
+     * This is a wrapper method for getting the JNDI environment's {@link Context#INITIAL_CONTEXT_FACTORY} property.
+     *
+     * @return the name of the ContextFactory class to use.
+     */
+    public String getContextFactoryClassName() {
+        return (String) getEnvironmentProperty(Context.INITIAL_CONTEXT_FACTORY);
+    }
+
+    /**
+     * Returns the base JNDI environment template to use when acquiring an LDAP connection (an {@link LdapContext}).
+     * This property is the base configuration template to use for all connections.  This template is then
+     * merged with appropriate runtime values as necessary in the
+     * {@link #getLdapContext(Object, Object)} implementation.  The merged environment instance is what is used to
+     * acquire the {@link LdapContext} at runtime.
+     * <p/>
+     * Most other get/set methods in this class act as thin proxy wrappers that interact with this property.  The
+     * benefit of using them is you have an easier-to-use configuration mechanism compared to setting map properties
+     * based on JNDI context keys.
+     *
+     * @return the base JNDI environment template to use when acquiring an LDAP connection (an {@link LdapContext})
+     */
+    public Map getEnvironment() {
+        return this.environment;
+    }
+
+    /**
+     * Sets the base JNDI environment template to use when acquiring LDAP connections.  It is typically more common
+     * to use the other get/set methods in this class to set individual environment settings rather than use
+     * this method, but it is available for advanced users that want full control over the base JNDI environment
+     * settings.
+     * <p/>
+     * Note that this template only represents the base/default environment settings.  It is then merged with
+     * appropriate runtime values as necessary in the {@link #getLdapContext(Object, Object)} implementation.
+     * The merged environment instance is what is used to acquire the connection ({@link LdapContext}) at runtime.
+     *
+     * @param env the base JNDI environment template to use when acquiring LDAP connections.
+     */
+    @SuppressWarnings({"unchecked"})
+    public void setEnvironment(Map env) {
+        this.environment = env;
+    }
+
+    /**
+     * Returns the environment property value bound under the specified key.
+     *
+     * @param name the name of the environment property
+     * @return the property value or {@code null} if the value has not been set.
+     */
+    private Object getEnvironmentProperty(String name) {
+        return this.environment.get(name);
+    }
+
+    /**
+     * Will apply the value to the environment attribute if and only if the value is not null or empty.  If it is
+     * null or empty, the corresponding environment attribute will be removed.
+     *
+     * @param name  the environment property key
+     * @param value the environment property value.  A null/empty value will trigger removal.
+     */
+    private void setEnvironmentProperty(String name, String value) {
+        if (StringUtils.hasText(value)) {
+            this.environment.put(name, value);
+        } else {
+            this.environment.remove(name);
+        }
+    }
+
+    /**
+     * Returns whether or not connection pooling should be used when possible and appropriate.  This property is NOT
+     * backed by the {@link #getEnvironment() environment template} like most other properties in this class.  It
+     * is a flag to indicate that pooling is preferred.  The default value is {@code true}.
+     * <p/>
+     * However, pooling will only actually be enabled if this property is {@code true} <em>and</em> the connection
+     * being created is for the {@link #getSystemUsername() systemUsername} user.  Connection pooling is not used for
+     * general authentication attempts by application end-users because the probability of re-use for that same
+     * user-specific connection after an authentication attempt is extremely low.
+     * <p/>
+     * If this attribute is {@code true} and it has been determined that the connection is being made with the
+     * {@link #getSystemUsername() systemUsername}, the
+     * {@link #getLdapContext(Object, Object)} implementation will set the Sun/Oracle-specific
+     * {@code com.sun.jndi.ldap.connect.pool} environment property to &quot;{@code true}&quot;.  This means setting
+     * this property is only likely to work if using the Sun/Oracle default context factory class (i.e. not using
+     * a custom {@link #getContextFactoryClassName() contextFactoryClassName}).
+     *
+     * @return whether or not connection pooling should be used when possible and appropriate
+     */
+    public boolean isPoolingEnabled() {
+        return poolingEnabled;
+    }
+
+    /**
+     * Sets whether or not connection pooling should be used when possible and appropriate.  This property is NOT
+     * a wrapper to the {@link #getEnvironment() environment template} like most other properties in this class.  It
+     * is a flag to indicate that pooling is preferred.  The default value is {@code true}.
+     * <p/>
+     * However, pooling will only actually be enabled if this property is {@code true} <em>and</em> the connection
+     * being created is for the {@link #getSystemUsername() systemUsername} user.  Connection pooling is not used for
+     * general authentication attempts by application end-users because the probability of re-use for that same
+     * user-specific connection after an authentication attempt is extremely low.
+     * <p/>
+     * If this attribute is {@code true} and it has been determined that the connection is being made with the
+     * {@link #getSystemUsername() systemUsername}, the
+     * {@link #getLdapContext(Object, Object)} implementation will set the Sun/Oracle-specific
+     * {@code com.sun.jndi.ldap.connect.pool} environment property to &quot;{@code true}&quot;.  This means setting
+     * this property is only likely to work if using the Sun/Oracle default context factory class (i.e. not using
+     * a custom {@link #getContextFactoryClassName() contextFactoryClassName}).
+     *
+     * @param poolingEnabled whether or not connection pooling should be used when possible and appropriate
+     */
+    public void setPoolingEnabled(boolean poolingEnabled) {
+        this.poolingEnabled = poolingEnabled;
+    }
+
+    /**
+     * Sets the LDAP referral behavior when creating a connection.  Defaults to {@code follow}.  See the Sun/Oracle LDAP
+     * <a href="http://java.sun.com/products/jndi/tutorial/ldap/referral/jndi.html">referral documentation</a> for more.
+     *
+     * @param referral the referral property.
+     * @see <a href="http://java.sun.com/products/jndi/tutorial/ldap/referral/jndi.html">Referrals in JNDI</a>
+     */
+    public void setReferral(String referral) {
+        setEnvironmentProperty(Context.REFERRAL, referral);
+    }
+
+    /**
+     * Returns the LDAP referral behavior when creating a connection.  Defaults to {@code follow}.
+     * See the Sun/Oracle LDAP
+     * <a href="http://java.sun.com/products/jndi/tutorial/ldap/referral/jndi.html">referral documentation</a> for more.
+     *
+     * @return the LDAP referral behavior when creating a connection.
+     * @see <a href="http://java.sun.com/products/jndi/tutorial/ldap/referral/jndi.html">Referrals in JNDI</a>
+     */
+    public String getReferral() {
+        return (String) getEnvironmentProperty(Context.REFERRAL);
+    }
+
+    /**
+     * The LDAP url to connect to. (e.g. ldap://&lt;ldapDirectoryHostname&gt;:&lt;port&gt;).  This must be configured.
+     *
+     * @param url the LDAP url to connect to. (e.g. ldap://&lt;ldapDirectoryHostname&gt;:&lt;port&gt;)
+     */
+    public void setUrl(String url) {
+        setEnvironmentProperty(Context.PROVIDER_URL, url);
+    }
+
+    /**
+     * Returns the LDAP url to connect to. (e.g. ldap://&lt;ldapDirectoryHostname&gt;:&lt;port&gt;).
+     * This must be configured.
+     *
+     * @return the LDAP url to connect to. (e.g. ldap://&lt;ldapDirectoryHostname&gt;:&lt;port&gt;)
+     */
+    public String getUrl() {
+        return (String) getEnvironmentProperty(Context.PROVIDER_URL);
+    }
+
+    /**
+     * Sets the password of the {@link #setSystemUsername(String) systemUsername} that will be used when creating an
+     * LDAP connection used for authorization queries.
+     * <p/>
+     * Note that setting this property is not required if the calling LDAP Realm does not perform authorization
+     * checks.
+     *
+     * @param systemPassword the password of the {@link #setSystemUsername(String) systemUsername} that will be used
+     *                       when creating an LDAP connection used for authorization queries.
+     */
+    public void setSystemPassword(String systemPassword) {
+        this.systemPassword = systemPassword;
+    }
+
+    /**
+     * Returns the password of the {@link #setSystemUsername(String) systemUsername} that will be used when creating an
+     * LDAP connection used for authorization queries.
+     * <p/>
+     * Note that setting this property is not required if the calling LDAP Realm does not perform authorization
+     * checks.
+     *
+     * @return the password of the {@link #setSystemUsername(String) systemUsername} that will be used when creating an
+     *         LDAP connection used for authorization queries.
+     */
+    public String getSystemPassword() {
+        return this.systemPassword;
+    }
+
+    /**
+     * Sets the system username that will be used when creating an LDAP connection used for authorization queries.
+     * The user must have the ability to query for authorization data for any application user.
+     * <p/>
+     * Note that setting this property is not required if the calling LDAP Realm does not perform authorization
+     * checks.
+     *
+     * @param systemUsername the system username that will be used when creating an LDAP connection used for
+     *                       authorization queries.
+     */
+    public void setSystemUsername(String systemUsername) {
+        this.systemUsername = systemUsername;
+    }
+
+    /**
+     * Returns the system username that will be used when creating an LDAP connection used for authorization queries.
+     * The user must have the ability to query for authorization data for any application user.
+     * <p/>
+     * Note that setting this property is not required if the calling LDAP Realm does not perform authorization
+     * checks.
+     *
+     * @return the system username that will be used when creating an LDAP connection used for authorization queries.
+     */
+    public String getSystemUsername() {
+        return systemUsername;
+    }
+
+    /*--------------------------------------------
+    |               M E T H O D S               |
+    ============================================*/
+
+    /**
+     * This implementation delegates to {@link #getLdapContext(Object, Object)} using the
+     * {@link #getSystemUsername() systemUsername} and {@link #getSystemPassword() systemPassword} properties as
+     * arguments.
+     *
+     * @return the system LdapContext
+     * @throws NamingException if there is a problem connecting to the LDAP directory
+     */
+    public LdapContext getSystemLdapContext() throws NamingException {
+        return getLdapContext((Object)getSystemUsername(), getSystemPassword());
+    }
+
+    /**
+     * Deprecated - use {@link #getLdapContext(Object, Object)} instead.  This will be removed before Apache Shiro 2.0.
+     *
+     * @param username the username to use when creating the connection.
+     * @param password the password to use when creating the connection.
+     * @return a {@code LdapContext} bound using the given username and password.
+     * @throws javax.naming.NamingException if there is an error creating the context.
+     * @deprecated the {@link #getLdapContext(Object, Object)} method should be used in all cases to ensure more than
+     *             String principals and credentials can be used.  Shiro no longer calls this method - it will be
+     *             removed before the 2.0 release.
+     */
+    @Deprecated
+    public LdapContext getLdapContext(String username, String password) throws NamingException {
+        return getLdapContext((Object) username, password);
+    }
+
+    /**
+     * Returns {@code true} if LDAP connection pooling should be used when acquiring a connection based on the specified
+     * account principal, {@code false} otherwise.
+     * <p/>
+     * This implementation returns {@code true} only if {@link #isPoolingEnabled()} and the principal equals the
+     * {@link #getSystemUsername()}.  The reasoning behind this is that connection pooling is not desirable for
+     * general authentication attempts by application end-users because the probability of re-use for that same
+     * user-specific connection after an authentication attempt is extremely low.
+     *
+     * @param principal the principal under which the connection will be made
+     * @return {@code true} if LDAP connection pooling should be used when acquiring a connection based on the specified
+     *         account principal, {@code false} otherwise.
+     */
+    protected boolean isPoolingConnections(Object principal) {
+        return isPoolingEnabled() && principal != null && principal.equals(getSystemUsername());
+    }
+
+    /**
+     * This implementation returns an LdapContext based on the configured JNDI/LDAP environment configuration.
+     * The environmet (Map) used at runtime is created by merging the default/configured
+     * {@link #getEnvironment() environment template} with some runtime values as necessary (e.g. a principal and
+     * credential available at runtime only).
+     * <p/>
+     * After the merged Map instance is created, the LdapContext connection is
+     * {@link #createLdapContext(java.util.Hashtable) created} and returned.
+     *
+     * @param principal   the principal to use when acquiring a connection to the LDAP directory
+     * @param credentials the credentials (password, X.509 certificate, etc) to use when acquiring a connection to the
+     *                    LDAP directory
+     * @return the acquired {@code LdapContext} connection bound using the specified principal and credentials.
+     * @throws NamingException
+     * @throws IllegalStateException
+     */
+    public LdapContext getLdapContext(Object principal, Object credentials) throws NamingException,
+            IllegalStateException {
+
+        String url = getUrl();
+        if (url == null) {
+            throw new IllegalStateException("An LDAP URL must be specified of the form ldap://<hostname>:<port>");
+        }
+
+        //copy the environment template into the runtime instance that will be further edited based on
+        //the method arguments and other class attributes.
+        Hashtable<String, Object> env = new Hashtable<String, Object>(this.environment);
+
+        Object authcMech = getAuthenticationMechanism();
+        if (authcMech == null && (principal != null || credentials != null)) {
+            //No authenticationMechanism has not been set, but either a principal and/or credentials were
+            //supplied, indicating that at least a 'simple' authentication attempt is indeed occurring - the Shiro
+            //end-user just didn't configure it explicitly.  So we set it to be 'simple' here as a convenience;
+            //the Sun provider implementation already does this same logic, but by repeating that logic here, we ensure
+            //this convenience exists regardless of provider implementation):
+            env.put(Context.SECURITY_AUTHENTICATION, SIMPLE_AUTHENTICATION_MECHANISM_NAME);
+        }
+        if (principal != null) {
+            env.put(Context.SECURITY_PRINCIPAL, principal);
+        }
+        if (credentials != null) {
+            env.put(Context.SECURITY_CREDENTIALS, credentials);
+        }
+
+        boolean pooling = isPoolingConnections(principal);
+        if (pooling) {
+            env.put(SUN_CONNECTION_POOLING_PROPERTY, "true");
+        }
+
+        if (log.isDebugEnabled()) {
+            log.debug("Initializing LDAP context using URL [{}] and principal [{}] with pooling {}",
+                    new Object[]{url, principal, (pooling ? "enabled" : "disabled")});
+        }
+
+        return createLdapContext(env);
+    }
+
+    /**
+     * Creates and returns a new {@link javax.naming.ldap.InitialLdapContext} instance.  This method exists primarily
+     * to support testing where a mock LdapContext can be returned instead of actually creating a connection, but
+     * subclasses are free to provide a different implementation if necessary.
+     *
+     * @param env the JNDI environment settings used to create the LDAP connection
+     * @return an LdapConnection
+     * @throws NamingException if a problem occurs creating the connection
+     */
+    protected LdapContext createLdapContext(Hashtable env) throws NamingException {
+        return new InitialLdapContext(env, null);
+    }
+
+}

Added: incubator/shiro/trunk/core/src/main/java/org/apache/shiro/realm/ldap/JndiLdapRealm.java
URL: http://svn.apache.org/viewvc/incubator/shiro/trunk/core/src/main/java/org/apache/shiro/realm/ldap/JndiLdapRealm.java?rev=983338&view=auto
==============================================================================
--- incubator/shiro/trunk/core/src/main/java/org/apache/shiro/realm/ldap/JndiLdapRealm.java (added)
+++ incubator/shiro/trunk/core/src/main/java/org/apache/shiro/realm/ldap/JndiLdapRealm.java Sun Aug  8 02:30:28 2010
@@ -0,0 +1,400 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.shiro.realm.ldap;
+
+import org.apache.shiro.authc.AuthenticationException;
+import org.apache.shiro.authc.AuthenticationInfo;
+import org.apache.shiro.authc.AuthenticationToken;
+import org.apache.shiro.authc.SimpleAuthenticationInfo;
+import org.apache.shiro.authc.credential.AllowAllCredentialsMatcher;
+import org.apache.shiro.authz.AuthorizationException;
+import org.apache.shiro.authz.AuthorizationInfo;
+import org.apache.shiro.realm.AuthorizingRealm;
+import org.apache.shiro.subject.PrincipalCollection;
+import org.apache.shiro.util.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.naming.NamingException;
+import javax.naming.ldap.LdapContext;
+
+/**
+ * An LDAP {@link org.apache.shiro.realm.Realm Realm} implementation utilizing Sun's/Oracle's
+ * <a href="http://download-llnw.oracle.com/javase/tutorial/jndi/ldap/jndi.html">JNDI API as an LDAP API</a>.  This is
+ * Shiro's default implementation for supporting LDAP, as using the JNDI API has been a common approach for Java LDAP
+ * support for many years.
+ * <p/>
+ * This realm implementation and its backing {@link JndiLdapContextFactory} should cover 99% of all Shiro-related LDAP
+ * authentication and authorization needs.  However, if it does not suit your needs, you might want to look into
+ * creating your own realm using an alternative, perhaps more robust, LDAP communication API, such as the
+ * <a href="http://directory.apache.org/api/">Apache LDAP API</a>.
+ * <h2>Authentication</h2>
+ * During an authentication attempt, if the submitted {@code AuthenticationToken}'s
+ * {@link org.apache.shiro.authc.AuthenticationToken#getPrincipal() principal} is a simple username, but the
+ * LDAP directory expects a complete User Distinguished Name (User DN) to establish a connection, the
+ * {@link #setUserDnTemplate(String) userDnTemplate} property must be configured.  If not configured,
+ * the property will pass the simple username directly as the User DN, which is often incorrect in most LDAP
+ * environments (Microsoft ActiveDirectory being the exception).
+ *
+ * @since 1.1
+ */
+public class JndiLdapRealm extends AuthorizingRealm {
+
+    private static final Logger log = LoggerFactory.getLogger(JndiLdapRealm.class);
+
+    //The zero index currently means nothing, but could be utilized in the future for other substitution techniques.
+    private static final String USERDN_SUBSTITUTION_TOKEN = "{0}";
+
+    private String userDnPrefix;
+    private String userDnSuffix;
+
+    /*--------------------------------------------
+    |    I N S T A N C E   V A R I A B L E S    |
+    ============================================*/
+    /**
+     * The LdapContextFactory instance used to acquire {@link javax.naming.ldap.LdapContext LdapContext}'s at runtime
+     * to acquire connections to the LDAP directory to perform authentication attempts and authorizatino queries.
+     */
+    private LdapContextFactory contextFactory;
+
+    /*--------------------------------------------
+    |         C O N S T R U C T O R S           |
+    ============================================*/
+
+    /**
+     * Default no-argument constructor that defaults the internal {@link LdapContextFactory} instance to a
+     * {@link JndiLdapContextFactory}.
+     */
+    public JndiLdapRealm() {
+        //Credentials Matching is not necessary - the LDAP directory will do it automatically:
+        setCredentialsMatcher(new AllowAllCredentialsMatcher());
+        //Any Object principal and Object credentials may be passed to the LDAP provider, so accept any token:
+        setAuthenticationTokenClass(AuthenticationToken.class);
+        this.contextFactory = new JndiLdapContextFactory();
+    }
+
+    /*--------------------------------------------
+    |  A C C E S S O R S / M O D I F I E R S    |
+    ============================================*/
+
+    /**
+     * Returns the User DN prefix to use when building a runtime User DN value or {@code null} if no
+     * {@link #getUserDnTemplate() userDnTemplate} has been configured.  If configured, this value is the text that
+     * occurs before the {@link #USERDN_SUBSTITUTION_TOKEN} in the {@link #getUserDnTemplate() userDnTemplate} value.
+     *
+     * @return the the User DN prefix to use when building a runtime User DN value or {@code null} if no
+     *         {@link #getUserDnTemplate() userDnTemplate} has been configured.
+     */
+    protected String getUserDnPrefix() {
+        return userDnPrefix;
+    }
+
+    /**
+     * Returns the User DN suffix to use when building a runtime User DN value.  or {@code null} if no
+     * {@link #getUserDnTemplate() userDnTemplate} has been configured.  If configured, this value is the text that
+     * occurs after the {@link #USERDN_SUBSTITUTION_TOKEN} in the {@link #getUserDnTemplate() userDnTemplate} value.
+     *
+     * @return the User DN suffix to use when building a runtime User DN value or {@code null} if no
+     *         {@link #getUserDnTemplate() userDnTemplate} has been configured.
+     */
+    protected String getUserDnSuffix() {
+        return userDnSuffix;
+    }
+
+    /*--------------------------------------------
+    |               M E T H O D S               |
+    ============================================*/
+
+    /**
+     * Sets the User Distinguished Name (DN) template to use when creating User DNs at runtime.  A User DN is an LDAP
+     * fully-qualified unique user identifier which is required to establish a connection with the LDAP
+     * directory to authenticate users and query for authorization information.
+     * <h2>Usage</h2>
+     * User DN formats are unique to the LDAP directory's schema, and each environment differs - you will need to
+     * specify the format corresponding to your directory.  You do this by specifying the full User DN as normal, but
+     * but you use a <b>{@code {0}}</b> placeholder token in the string representing the location where the
+     * user's submitted principal (usually a username or uid) will be substituted at runtime.
+     * <p/>
+     * For example,  if your directory
+     * uses an LDAP {@code uid} attribute to represent usernames, the User DN for the {@code jsmith} user may look like
+     * this:
+     * <p/>
+     * <pre>uid=jsmith,ou=users,dc=mycompany,dc=com</pre>
+     * <p/>
+     * in which case you would set this property with the following template value:
+     * <p/>
+     * <pre>uid=<b>{0}</b>,ou=users,dc=mycompany,dc=com</pre>
+     * <p/>
+     * If no template is configured, the raw {@code AuthenticationToken}
+     * {@link AuthenticationToken#getPrincipal() principal} will be used as the LDAP principal.  This is likely
+     * incorrect as most LDAP directories expect a fully-qualified User DN as opposed to the raw uid or username.  So,
+     * ensure you set this property to match your environment!
+     *
+     * @param template the User Distinguished Name template to use for runtime substitution
+     * @throws IllegalArgumentException if the template is null, empty, or does not contain the
+     *                                  {@code {0}} substitution token.
+     * @see LdapContextFactory#getLdapContext(Object,Object)
+     */
+    public void setUserDnTemplate(String template) throws IllegalArgumentException {
+        if (!StringUtils.hasText(template)) {
+            String msg = "User DN template cannot be null or empty.";
+            throw new IllegalArgumentException(msg);
+        }
+        int index = template.indexOf(USERDN_SUBSTITUTION_TOKEN);
+        if (index < 0) {
+            String msg = "User DN template must contain the '" +
+                    USERDN_SUBSTITUTION_TOKEN + "' replacement token to understand where to " +
+                    "insert the runtime authentication principal.";
+            throw new IllegalArgumentException(msg);
+        }
+        String prefix = template.substring(0, index);
+        String suffix = template.substring(prefix.length() + USERDN_SUBSTITUTION_TOKEN.length());
+        if (log.isDebugEnabled()) {
+            log.debug("Determined user DN prefix [{}] and suffix [{}]", prefix, suffix);
+        }
+        this.userDnPrefix = prefix;
+        this.userDnSuffix = suffix;
+    }
+
+    /**
+     * Returns the User Distinguished Name (DN) template to use when creating User DNs at runtime - see the
+     * {@link #setUserDnTemplate(String) setUserDnTemplate} JavaDoc for a full explanation.
+     *
+     * @return the User Distinguished Name (DN) template to use when creating User DNs at runtime.
+     */
+    public String getUserDnTemplate() {
+        return getUserDn(USERDN_SUBSTITUTION_TOKEN);
+    }
+
+    /**
+     * Returns the LDAP User Distinguished Name (DN) to use when acquiring an
+     * {@link javax.naming.ldap.LdapContext LdapContext} from the {@link LdapContextFactory}.
+     * <p/>
+     * If the the {@link #getUserDnTemplate() userDnTemplate} property has been set, this implementation will construct
+     * the User DN by substituting the specified {@code principal} into the configured template.  If the
+     * {@link #getUserDnTemplate() userDnTemplate} has not been set, the method argument will be returned directly
+     * (indicating that the submitted authentication token principal <em>is</em> the User DN).
+     *
+     * @param principal the principal to substitute into the configured {@link #getUserDnTemplate() userDnTemplate}.
+     * @return the constructed User DN to use at runtime when acquiring an {@link javax.naming.ldap.LdapContext}.
+     * @throws IllegalArgumentException if the method argument is null or empty
+     * @throws IllegalStateException    if the {@link #getUserDnTemplate userDnTemplate} has not been set.
+     * @see LdapContextFactory#getLdapContext(Object, Object)
+     */
+    protected String getUserDn(String principal) throws IllegalArgumentException, IllegalStateException {
+        if (!StringUtils.hasText(principal)) {
+            throw new IllegalArgumentException("User principal cannot be null or empty for User DN construction.");
+        }
+        String prefix = getUserDnPrefix();
+        String suffix = getUserDnSuffix();
+        if (prefix == null && suffix == null) {
+            log.debug("userDnTemplate property has not been configured, indicating the submitted " +
+                    "AuthenticationToken's principal is the same as the User DN.  Returning the method argument " +
+                    "as is.");
+            return principal;
+        }
+
+        int prefixLength = prefix != null ? prefix.length() : 0;
+        int suffixLength = suffix != null ? suffix.length() : 0;
+        StringBuilder sb = new StringBuilder(prefixLength + principal.length() + suffixLength);
+        if (prefixLength > 0) {
+            sb.append(prefix);
+        }
+        sb.append(principal);
+        if (suffixLength > 0) {
+            sb.append(suffix);
+        }
+        return sb.toString();
+    }
+
+    /**
+     * Sets the LdapContextFactory instance used to acquire connections to the LDAP directory during authentication
+     * attempts and authorization queries.  Unless specified otherwise, the default is a {@link JndiLdapContextFactory}
+     * instance.
+     *
+     * @param contextFactory the LdapContextFactory instance used to acquire connections to the LDAP directory during
+     *                       authentication attempts and authorization queries
+     */
+    @SuppressWarnings({"UnusedDeclaration"})
+    public void setContextFactory(LdapContextFactory contextFactory) {
+        this.contextFactory = contextFactory;
+    }
+
+    /**
+     * Returns the LdapContextFactory instance used to acquire connections to the LDAP directory during authentication
+     * attempts and authorization queries.  Unless specified otherwise, the default is a {@link JndiLdapContextFactory}
+     * instance.
+     *
+     * @return the LdapContextFactory instance used to acquire connections to the LDAP directory during
+     *         authentication attempts and authorization queries
+     */
+    public LdapContextFactory getContextFactory() {
+        return this.contextFactory;
+    }
+
+    /*--------------------------------------------
+    |               M E T H O D S                |
+    ============================================*/
+
+    /**
+     * Delegates to {@link #queryForAuthenticationInfo(org.apache.shiro.authc.AuthenticationToken, LdapContextFactory)},
+     * wrapping any {@link NamingException}s in a Shiro {@link AuthenticationException} to satisfy the parent method
+     * signature.
+     *
+     * @param token the authentication token containing the user's principal and credentials.
+     * @return the {@link AuthenticationInfo} acquired after a successful authentication attempt
+     * @throws AuthenticationException if the authentication attempt fails or if a
+     *                                 {@link NamingException} occurs.
+     */
+    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
+        AuthenticationInfo info;
+        try {
+            info = queryForAuthenticationInfo(token, getContextFactory());
+        } catch (javax.naming.AuthenticationException e) {
+            throw new AuthenticationException("LDAP authentication failed.", e);
+        } catch (NamingException e) {
+            String msg = "LDAP naming error while attempting to authenticate user.";
+            throw new AuthenticationException(msg, e);
+        }
+
+        return info;
+    }
+
+
+    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
+        AuthorizationInfo info;
+        try {
+            info = queryForAuthorizationInfo(principals, getContextFactory());
+        } catch (NamingException e) {
+            String msg = "LDAP naming error while attempting to retrieve authorization for user [" + principals + "].";
+            throw new AuthorizationException(msg, e);
+        }
+
+        return info;
+    }
+
+    /**
+     * Returns the principal to use when creating the LDAP connection for an authentication attempt.
+     * <p/>
+     * This implementation uses a heuristic: it checks to see if the specified token's
+     * {@link AuthenticationToken#getPrincipal() principal} is a {@code String}, and if so,
+     * {@link #getUserDn(String) converts it} from what is
+     * assumed to be a raw uid or username {@code String} into a User DN {@code String}.  Almost all LDAP directories
+     * expect the authentication connection to present a User DN and not an unqualified username or uid.
+     * <p/>
+     * If the token's {@code principal} is not a String, it is assumed to already be in the format supported by the
+     * underlying {@link LdapContextFactory} implementation and the raw principal is returned directly.
+     *
+     * @param token the {@link AuthenticationToken} submitted during the authentication process
+     * @return the User DN or raw principal to use to acquire the LdapContext.
+     * @see LdapContextFactory#getLdapContext(Object, Object)
+     */
+    protected Object getLdapPrincipal(AuthenticationToken token) {
+        Object principal = token.getPrincipal();
+        if (principal instanceof String) {
+            String sPrincipal = (String) principal;
+            return getUserDn(sPrincipal);
+        }
+        return principal;
+    }
+
+    /**
+     * This implementation opens an LDAP connection using the token's
+     * {@link #getLdapPrincipal(org.apache.shiro.authc.AuthenticationToken) discovered principal} and provided
+     * {@link AuthenticationToken#getCredentials() credentials}.  If the connection opens successfully, the
+     * authentication attempt is immediately considered successful and a new
+     * {@link AuthenticationInfo} instance is
+     * {@link #createAuthenticationInfo(org.apache.shiro.authc.AuthenticationToken, Object, Object, javax.naming.ldap.LdapContext) created}
+     * and returned.  If the connection cannot be opened, either because LDAP authentication failed or some other
+     * JNDI problem, an {@link NamingException} will be thrown.
+     *
+     * @param token              the submitted authentication token that triggered the authentication attempt.
+     * @param ldapContextFactory factory used to retrieve LDAP connections.
+     * @return an {@link AuthenticationInfo} instance representing the authenticated user's information.
+     * @throws NamingException if any LDAP errors occur.
+     */
+    protected AuthenticationInfo queryForAuthenticationInfo(AuthenticationToken token,
+                                                            LdapContextFactory ldapContextFactory)
+            throws NamingException {
+
+        Object principal = token.getPrincipal();
+        Object credentials = token.getCredentials();
+
+        log.debug("Authenticating user '{}' through LDAP", principal);
+
+        principal = getLdapPrincipal(token);
+
+        LdapContext ctx = null;
+        try {
+            ctx = ldapContextFactory.getLdapContext(principal, credentials);
+            //context was opened successfully, which means their credentials were valid.  Return the AuthenticationInfo:
+            return createAuthenticationInfo(token, principal, credentials, ctx);
+        } finally {
+            LdapUtils.closeContext(ctx);
+        }
+    }
+
+    /**
+     * Returns the {@link AuthenticationInfo} resulting from a Subject's successful LDAP authentication attempt.
+     * <p/>
+     * This implementation ignores the {@code ldapPrincipal}, {@code ldapCredentials}, and the opened
+     * {@code ldapContext} arguments and merely returns an {@code AuthenticationInfo} instance mirroring the
+     * submitted token's principal and credentials.  This is acceptable because this method is only ever invoked after
+     * a successful authentication attempt, which means the provided principal and credentials were correct, and can
+     * be used directly to populate the (now verified) {@code AuthenticationInfo}.
+     * <p/>
+     * Subclasses however are free to override this method for more advanced construction logic.
+     *
+     * @param token           the submitted {@code AuthenticationToken} that resulted in a successful authentication
+     * @param ldapPrincipal   the LDAP principal used when creating the LDAP connection.  Unlike the token's
+     *                        {@link AuthenticationToken#getPrincipal() principal}, this value is usually a constructed
+     *                        User DN and not a simple username or uid.  The exact value is depending on the
+     *                        configured
+     *                        <a href="http://download-llnw.oracle.com/javase/tutorial/jndi/ldap/auth_mechs.html">
+     *                        LDAP authentication mechanism</a> in use.
+     * @param ldapCredentials the LDAP credentials used when creating the LDAP connection.
+     * @param ldapContext     the LdapContext created that resulted in a successful authentication.  It can be used
+     *                        further by subclasses for more complex operations.  It does not need to be closed -
+     *                        it will be closed automatically after this method returns.
+     * @return the {@link AuthenticationInfo} resulting from a Subject's successful LDAP authentication attempt.
+     * @throws NamingException if there was any problem using the {@code LdapContext}
+     */
+    @SuppressWarnings({"UnusedDeclaration"})
+    protected AuthenticationInfo createAuthenticationInfo(AuthenticationToken token, Object ldapPrincipal,
+                                                          Object ldapCredentials, LdapContext ldapContext)
+            throws NamingException {
+        return new SimpleAuthenticationInfo(token.getPrincipal(), token.getCredentials(), getName());
+    }
+
+
+    /**
+     * Method that should be implemented by subclasses to build an
+     * {@link AuthorizationInfo} object by querying the LDAP context for the
+     * specified principal.</p>
+     *
+     * @param principal          the principal of the Subject whose AuthenticationInfo should be queried from the LDAP server.
+     * @param ldapContextFactory factory used to retrieve LDAP connections.
+     * @return an {@link AuthorizationInfo} instance containing information retrieved from the LDAP server.
+     * @throws NamingException if any LDAP errors occur during the search.
+     */
+    protected AuthorizationInfo queryForAuthorizationInfo(PrincipalCollection principal,
+                                                          LdapContextFactory ldapContextFactory) throws NamingException {
+        return null;
+    }
+}

Modified: incubator/shiro/trunk/core/src/main/java/org/apache/shiro/realm/ldap/LdapContextFactory.java
URL: http://svn.apache.org/viewvc/incubator/shiro/trunk/core/src/main/java/org/apache/shiro/realm/ldap/LdapContextFactory.java?rev=983338&r1=983337&r2=983338&view=diff
==============================================================================
--- incubator/shiro/trunk/core/src/main/java/org/apache/shiro/realm/ldap/LdapContextFactory.java (original)
+++ incubator/shiro/trunk/core/src/main/java/org/apache/shiro/realm/ldap/LdapContextFactory.java Sun Aug  8 02:30:28 2010
@@ -22,33 +22,56 @@ import javax.naming.NamingException;
 import javax.naming.ldap.LdapContext;
 
 /**
- * Interface that encapsulates the creation of <tt>LdapContext</tt> objects that are used by subclasses
- * of {@link AbstractLdapRealm} to query for <tt>AuthenticationInfo</tt> security data (roles, permissions, etc) of particular
- * Subjects (users).
+ * Interface that encapsulates the creation of {@code LdapContext} objects that are used by {@link JndiLdapRealm}s to
+ * perform authentication attempts and query for authorization data.
  *
  * @since 0.2
  */
 public interface LdapContextFactory {
 
     /**
-     * Creates (or retrieves from a pool) a <tt>LdapContext</tt> connection bound using the system account, or anonymously
-     * if no system account is configured.
+     * Creates (or retrieves from a pool) a {@code LdapContext} connection bound using the system account, or
+     * anonymously if no system account is configured.
      *
-     * @return a <tt>LdapContext</tt> bound by the system account, or bound anonymously if no system account
+     * @return a {@code LdapContext} bound by the system account, or bound anonymously if no system account
      *         is configured.
      * @throws javax.naming.NamingException if there is an error creating the context.
      */
     LdapContext getSystemLdapContext() throws NamingException;
 
     /**
-     * Creates (or retrieves from a pool) a <tt>LdapContext</tt> connection bound using the username and password
+     * Creates (or retrieves from a pool) a {@code LdapContext} connection bound using the username and password
      * specified.
      *
      * @param username the username to use when creating the connection.
      * @param password the password to use when creating the connection.
-     * @return a <tt>LdapContext</tt> bound using the given username and password.
+     * @return a {@code LdapContext} bound using the given username and password.
      * @throws javax.naming.NamingException if there is an error creating the context.
+     * @deprecated the {@link #getLdapContext(Object, Object)} method should be used in all cases to ensure more than
+     * String principals and credentials can be used.
      */
+    @Deprecated
     LdapContext getLdapContext(String username, String password) throws NamingException;
 
+    /**
+     * Creates (or retrieves from a pool) an {@code LdapContext} connection bound using the specified principal and
+     * credentials.  The format of the principal and credentials are whatever is supported by the underlying
+     * LDAP {@link javax.naming.spi.InitialContextFactory InitialContextFactory} implementation.  The default Sun
+     * (now Oracle) implementation supports
+     * <a href="http://download-llnw.oracle.com/javase/tutorial/jndi/ldap/auth_mechs.html">anonymous, simple, and
+     * SASL-based mechanisms</a>.
+     * <p/>
+     * This method was added in Shiro 1.1 to address the fact that principals and credentials can be more than just
+     * {@code String} user DNs and passwords for connecting to LDAP.  For example, the credentials can be an
+     * {@code X.509} certificate.
+     *
+     * @param principal   the principal to use when acquiring a connection to the LDAP directory
+     * @param credentials the credentials (password, X.509 certificate, etc) to use when acquiring a connection to the
+     *                    LDAP directory
+     * @return the acquired {@code LdapContext} connection bound using the specified principal and credentials.
+     * @throws NamingException if unable to acquire a connection.
+     * @since 1.1
+     */
+    LdapContext getLdapContext(Object principal, Object credentials) throws NamingException;
+    
 }

Modified: incubator/shiro/trunk/core/src/main/java/org/apache/shiro/realm/ldap/LdapUtils.java
URL: http://svn.apache.org/viewvc/incubator/shiro/trunk/core/src/main/java/org/apache/shiro/realm/ldap/LdapUtils.java?rev=983338&r1=983337&r2=983338&view=diff
==============================================================================
--- incubator/shiro/trunk/core/src/main/java/org/apache/shiro/realm/ldap/LdapUtils.java (original)
+++ incubator/shiro/trunk/core/src/main/java/org/apache/shiro/realm/ldap/LdapUtils.java Sun Aug  8 02:30:28 2010
@@ -35,16 +35,12 @@ import org.slf4j.LoggerFactory;
  *
  * @since 0.2
  */
-public class LdapUtils {
-
-    /** Private internal log instance. */
-    private static final Logger log = LoggerFactory.getLogger(LdapUtils.class);
+public final class LdapUtils {
 
     /**
-     * Private constructor to prevent instantiation
+     * Private internal log instance.
      */
-    private LdapUtils() {
-    }
+    private static final Logger log = LoggerFactory.getLogger(LdapUtils.class);
 
     /**
      * Closes an LDAP context, logging any errors, but not throwing
@@ -58,13 +54,10 @@ public class LdapUtils {
                 ctx.close();
             }
         } catch (NamingException e) {
-            if (log.isErrorEnabled()) {
-                log.error("Exception while closing LDAP context. ", e);
-            }
+            log.error("Exception while closing LDAP context. ", e);
         }
     }
 
-
     /**
      * Helper method used to retrieve all attribute values from a particular context attribute.
      *
@@ -74,12 +67,31 @@ public class LdapUtils {
      */
     public static Collection<String> getAllAttributeValues(Attribute attr) throws NamingException {
         Set<String> values = new HashSet<String>();
-        for (NamingEnumeration e = attr.getAll(); e.hasMore();) {
-            String value = (String) e.next();
-            values.add(value);
+        NamingEnumeration ne = null;
+        try {
+            ne = attr.getAll();
+            while (ne.hasMore()) {
+                String value = (String) ne.next();
+                values.add(value);
+            }
+        } finally {
+            closeEnumeration(ne);
         }
+
         return values;
     }
 
+    //added based on SHIRO-127, per Emmanuel's comment [1]
+    // [1] https://issues.apache.org/jira/browse/SHIRO-127?focusedCommentId=12891380&page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel#action_12891380
+
+    public static void closeEnumeration(NamingEnumeration ne) {
+        try {
+            if (ne != null) {
+                ne.close();
+            }
+        } catch (NamingException e) {
+            log.error("Exception while closing NamingEnumeration: ", e);
+        }
+    }
 
 }

Added: incubator/shiro/trunk/core/src/test/java/org/apache/shiro/realm/ldap/JndiLdapContextFactoryTest.java
URL: http://svn.apache.org/viewvc/incubator/shiro/trunk/core/src/test/java/org/apache/shiro/realm/ldap/JndiLdapContextFactoryTest.java?rev=983338&view=auto
==============================================================================
--- incubator/shiro/trunk/core/src/test/java/org/apache/shiro/realm/ldap/JndiLdapContextFactoryTest.java (added)
+++ incubator/shiro/trunk/core/src/test/java/org/apache/shiro/realm/ldap/JndiLdapContextFactoryTest.java Sun Aug  8 02:30:28 2010
@@ -0,0 +1,187 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.shiro.realm.ldap;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import javax.naming.Context;
+import javax.naming.NamingException;
+import javax.naming.ldap.LdapContext;
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.Map;
+import java.util.UUID;
+
+import static junit.framework.Assert.*;
+import static org.easymock.EasyMock.*;
+
+/**
+ * Tests for the {@link JndiLdapContextFactory} class.
+ *
+ * @since 1.1
+ */
+public class JndiLdapContextFactoryTest {
+
+    private JndiLdapContextFactory factory;
+
+    @Before
+    public void setUp() {
+        factory = new JndiLdapContextFactory() {
+            //Fake a JNDI environment for the tests:
+            @Override
+            protected LdapContext createLdapContext(Hashtable env) throws NamingException {
+                return createNiceMock(LdapContext.class);
+            }
+        };
+    }
+
+    /**
+     * This is the only test that does not fake the JNDI environment.  It is provided for 100% test coverage.
+     *
+     * @throws NamingException thrown because the host is always broken.
+     */
+    @Test(expected = NamingException.class)
+    public void testGetLdapContext() throws NamingException {
+        factory = new JndiLdapContextFactory();
+        //garbage URL to test that the context is being created, but fails:
+        String brokenHost = UUID.randomUUID().toString();
+        factory.setUrl("ldap://" + brokenHost + ":389");
+        factory.getLdapContext((Object) "foo", "bar");
+    }
+
+    @Test
+    public void testAuthenticationMechanism() {
+        String mech = "MD5-DIGEST";
+        factory.setAuthenticationMechanism(mech);
+        assertEquals(mech, factory.getAuthenticationMechanism());
+    }
+
+    @Test
+    public void testReferral() {
+        String referral = "throw";
+        factory.setReferral(referral);
+        assertEquals(referral, factory.getReferral());
+    }
+
+    @Test
+    public void testGetContextFactoryClassName() {
+        assertEquals(JndiLdapContextFactory.DEFAULT_CONTEXT_FACTORY_CLASS_NAME, factory.getContextFactoryClassName());
+    }
+
+    @Test
+    public void testSetEnvironmentPropertyNull() {
+        factory.setAuthenticationMechanism("MD5-DIGEST");
+        factory.setAuthenticationMechanism(null);
+        assertNull(factory.getAuthenticationMechanism());
+    }
+
+    @Test
+    public void testCustomEnvironment() {
+        Map<String, String> map = new HashMap<String, String>();
+        map.put("foo", "bar");
+        factory.setEnvironment(map);
+        assertEquals("bar", factory.getEnvironment().get("foo"));
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testGetLdapContextWithoutUrl() throws NamingException {
+        factory.getLdapContext((Object) "foo", "bar");
+    }
+
+    @Test
+    public void testGetLdapContextDefault() throws NamingException {
+        factory = new JndiLdapContextFactory() {
+            @Override
+            protected LdapContext createLdapContext(Hashtable env) throws NamingException {
+                assertEquals("ldap://localhost:389", env.get(Context.PROVIDER_URL));
+                assertEquals("foo", env.get(Context.SECURITY_PRINCIPAL));
+                assertEquals("bar", env.get(Context.SECURITY_CREDENTIALS));
+                assertEquals("simple", env.get(Context.SECURITY_AUTHENTICATION));
+                assertNull(env.get(SUN_CONNECTION_POOLING_PROPERTY));
+                return createNiceMock(LdapContext.class);
+            }
+        };
+
+        factory.setUrl("ldap://localhost:389");
+        factory.getLdapContext((Object) "foo", "bar");
+    }
+
+    @SuppressWarnings({"deprecation"})
+    @Test
+    public void testGetLdapContextStringArguments() throws NamingException {
+        factory = new JndiLdapContextFactory() {
+            @Override
+            protected LdapContext createLdapContext(Hashtable env) throws NamingException {
+                assertEquals("ldap://localhost:389", env.get(Context.PROVIDER_URL));
+                assertEquals("foo", env.get(Context.SECURITY_PRINCIPAL));
+                assertEquals("bar", env.get(Context.SECURITY_CREDENTIALS));
+                assertEquals("simple", env.get(Context.SECURITY_AUTHENTICATION));
+                assertNull(env.get(SUN_CONNECTION_POOLING_PROPERTY));
+                return createNiceMock(LdapContext.class);
+            }
+        };
+
+        factory.setUrl("ldap://localhost:389");
+        factory.getLdapContext("foo", "bar");
+    }
+
+    @Test
+    public void testGetSystemLdapContext() throws NamingException {
+        factory = new JndiLdapContextFactory() {
+            @Override
+            protected LdapContext createLdapContext(Hashtable env) throws NamingException {
+                assertEquals("ldap://localhost:389", env.get(Context.PROVIDER_URL));
+                assertEquals("foo", env.get(Context.SECURITY_PRINCIPAL));
+                assertEquals("bar", env.get(Context.SECURITY_CREDENTIALS));
+                assertEquals("simple", env.get(Context.SECURITY_AUTHENTICATION));
+                assertNotNull(env.get(SUN_CONNECTION_POOLING_PROPERTY));
+                return createNiceMock(LdapContext.class);
+            }
+        };
+
+        factory.setSystemUsername("foo");
+        factory.setSystemPassword("bar");
+        factory.setUrl("ldap://localhost:389");
+        factory.getSystemLdapContext();
+    }
+
+    @Test
+    public void testGetSystemLdapContextPoolingDisabled() throws NamingException {
+        factory = new JndiLdapContextFactory() {
+            @Override
+            protected LdapContext createLdapContext(Hashtable env) throws NamingException {
+                assertEquals("ldap://localhost:389", env.get(Context.PROVIDER_URL));
+                assertEquals("foo", env.get(Context.SECURITY_PRINCIPAL));
+                assertEquals("bar", env.get(Context.SECURITY_CREDENTIALS));
+                assertEquals("simple", env.get(Context.SECURITY_AUTHENTICATION));
+                assertNull(env.get(SUN_CONNECTION_POOLING_PROPERTY));
+                return createNiceMock(LdapContext.class);
+            }
+        };
+
+        factory.setSystemUsername("foo");
+        factory.setSystemPassword("bar");
+        factory.setPoolingEnabled(false);
+        factory.setUrl("ldap://localhost:389");
+        factory.getSystemLdapContext();
+    }
+
+
+}

Added: incubator/shiro/trunk/core/src/test/java/org/apache/shiro/realm/ldap/JndiLdapRealmTest.java
URL: http://svn.apache.org/viewvc/incubator/shiro/trunk/core/src/test/java/org/apache/shiro/realm/ldap/JndiLdapRealmTest.java?rev=983338&view=auto
==============================================================================
--- incubator/shiro/trunk/core/src/test/java/org/apache/shiro/realm/ldap/JndiLdapRealmTest.java (added)
+++ incubator/shiro/trunk/core/src/test/java/org/apache/shiro/realm/ldap/JndiLdapRealmTest.java Sun Aug  8 02:30:28 2010
@@ -0,0 +1,174 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.shiro.realm.ldap;
+
+import org.apache.shiro.authc.AuthenticationException;
+import org.apache.shiro.authc.AuthenticationToken;
+import org.apache.shiro.authc.UsernamePasswordToken;
+import org.apache.shiro.authc.credential.AllowAllCredentialsMatcher;
+import org.junit.Before;
+import org.junit.Test;
+
+import javax.naming.NamingException;
+import javax.naming.ldap.LdapContext;
+
+import java.util.UUID;
+
+import static org.easymock.EasyMock.*;
+import static org.junit.Assert.*;
+
+/**
+ * Tests for the {@link JndiLdapRealm} class.
+ *
+ * @since 1.1
+ */
+@SuppressWarnings({"ThrowableInstanceNeverThrown"})
+public class JndiLdapRealmTest {
+
+    private JndiLdapRealm realm;
+
+    @Before
+    public void setUp() {
+        realm = new JndiLdapRealm();
+    }
+
+    @Test
+    public void testDefaultInstance() {
+        assertTrue(realm.getCredentialsMatcher() instanceof AllowAllCredentialsMatcher);
+        assertEquals(AuthenticationToken.class, realm.getAuthenticationTokenClass());
+        assertTrue(realm.getContextFactory() instanceof JndiLdapContextFactory);
+    }
+
+    @Test(expected=IllegalArgumentException.class)
+    public void testSetUserDnTemplateNull() {
+        realm.setUserDnTemplate(null);
+    }
+    
+    @Test(expected=IllegalArgumentException.class)
+    public void testSetUserDnTemplateEmpty() {
+        realm.setUserDnTemplate("  ");
+    }
+
+    @Test(expected=IllegalArgumentException.class)
+    public void testSetUserDnTemplateWithoutToken() {
+        realm.setUserDnTemplate("uid=,ou=users,dc=mycompany,dc=com");
+    }
+
+    @Test
+    public void testUserDnTemplate() {
+        String template = "uid={0},ou=users,dc=mycompany,dc=com";
+        realm.setUserDnTemplate(template);
+        assertEquals(template, realm.getUserDnTemplate());
+    }
+
+    @Test
+    public void testUserDnTemplateSubstitution() throws NamingException {
+        realm.setUserDnTemplate("uid={0},ou=users,dc=mycompany,dc=com");
+        LdapContextFactory factory = createMock(LdapContextFactory.class);
+        realm.setContextFactory(factory);
+
+        Object expectedPrincipal = "uid=jsmith,ou=users,dc=mycompany,dc=com";
+
+        expect(factory.getLdapContext(eq(expectedPrincipal), isA(Object.class))).andReturn(createNiceMock(LdapContext.class));
+        replay(factory);
+
+        realm.getAuthenticationInfo(new UsernamePasswordToken("jsmith", "secret") );
+        verify(factory);
+    }
+
+    @Test(expected= AuthenticationException.class)
+    public void testGetAuthenticationInfoNamingAuthenticationException() throws NamingException {
+        realm.setUserDnTemplate("uid={0},ou=users,dc=mycompany,dc=com");
+        LdapContextFactory factory = createMock(LdapContextFactory.class);
+        realm.setContextFactory(factory);
+
+        expect(factory.getLdapContext(isA(Object.class), isA(Object.class)))
+                .andThrow(new javax.naming.AuthenticationException("LDAP Authentication failed."));
+        replay(factory);
+
+        realm.getAuthenticationInfo(new UsernamePasswordToken("jsmith", "secret") );
+    }
+
+    @Test(expected= AuthenticationException.class)
+    public void testGetAuthenticationInfoNamingException() throws NamingException {
+        realm.setUserDnTemplate("uid={0},ou=users,dc=mycompany,dc=com");
+        LdapContextFactory factory = createMock(LdapContextFactory.class);
+        realm.setContextFactory(factory);
+
+        expect(factory.getLdapContext(isA(Object.class), isA(Object.class)))
+                .andThrow(new NamingException("Communication error."));
+        replay(factory);
+
+        realm.getAuthenticationInfo(new UsernamePasswordToken("jsmith", "secret") );
+    }
+
+    /**
+     * This test simulates that if a non-String principal (i.e. not a username) is passed as the LDAP principal, that
+     * it is not altered into a User DN and is passed as-is.  This will allow principals to be things like X.509
+     * certificates as well instead of only strings.
+     *
+     * @throws NamingException not thrown
+     */
+    @Test
+    public void testGetAuthenticationInfoNonSimpleToken() throws NamingException {
+        realm.setUserDnTemplate("uid={0},ou=users,dc=mycompany,dc=com");
+        LdapContextFactory factory = createMock(LdapContextFactory.class);
+        realm.setContextFactory(factory);
+
+        final UUID userId = UUID.randomUUID();
+
+        //ensure the userId is passed as-is:
+        expect(factory.getLdapContext(eq(userId), isA(Object.class))).andReturn(createNiceMock(LdapContext.class));
+        replay(factory);
+
+        realm.getAuthenticationInfo(new AuthenticationToken() {
+            public Object getPrincipal() {
+                return userId;
+            }
+
+            public Object getCredentials() {
+                return "secret";
+            }
+        });
+        verify(factory);
+    }
+
+    @Test(expected=IllegalArgumentException.class)
+    public void testGetUserDnNullArgument() {
+        realm.getUserDn(null);
+    }
+
+    @Test
+    public void testGetUserDnWithOutPrefixAndSuffix() {
+        realm = new JndiLdapRealm() {
+            @Override
+            protected String getUserDnPrefix() {
+                return null;
+            }
+
+            @Override
+            protected String getUserDnSuffix() {
+                return null;
+            }
+        };
+        String principal = "foo";
+        String userDn = realm.getUserDn(principal);
+        assertEquals(principal, userDn);
+    }
+}

Propchange: incubator/shiro/trunk/samples/spring-client/
------------------------------------------------------------------------------
--- svn:ignore (original)
+++ svn:ignore Sun Aug  8 02:30:28 2010
@@ -3,3 +3,5 @@
 .project
 .settings
 target
+
+*.iml