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 "{@code true}". 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 "{@code true}". 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://<ldapDirectoryHostname>:<port>). This must be configured.
+ *
+ * @param url the LDAP url to connect to. (e.g. ldap://<ldapDirectoryHostname>:<port>)
+ */
+ public void setUrl(String url) {
+ setEnvironmentProperty(Context.PROVIDER_URL, url);
+ }
+
+ /**
+ * Returns the LDAP url to connect to. (e.g. ldap://<ldapDirectoryHostname>:<port>).
+ * This must be configured.
+ *
+ * @return the LDAP url to connect to. (e.g. ldap://<ldapDirectoryHostname>:<port>)
+ */
+ 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