diff --git a/sandbox/src/main/java/org/acegisecurity/providers/dao/ldap/LdapPasswordAuthenticationDao.java b/sandbox/src/main/java/org/acegisecurity/providers/dao/ldap/LdapPasswordAuthenticationDao.java index e092b3e056..a2dee1b30f 100644 --- a/sandbox/src/main/java/org/acegisecurity/providers/dao/ldap/LdapPasswordAuthenticationDao.java +++ b/sandbox/src/main/java/org/acegisecurity/providers/dao/ldap/LdapPasswordAuthenticationDao.java @@ -1,17 +1,17 @@ /* Copyright 2004, 2005 Acegi Technology Pty Limited - * - * Licensed 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. - */ +* +* Licensed 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 net.sf.acegisecurity.providers.dao.ldap; @@ -28,6 +28,7 @@ import org.apache.commons.logging.LogFactory; import org.springframework.dao.DataAccessException; import org.springframework.dao.DataAccessResourceFailureException; +import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collection; import java.util.Hashtable; @@ -44,365 +45,857 @@ import javax.naming.directory.BasicAttribute; import javax.naming.directory.BasicAttributes; import javax.naming.directory.DirContext; import javax.naming.directory.InitialDirContext; +import javax.naming.directory.SearchControls; import javax.naming.directory.SearchResult; /** - * This is an example PasswordAuthenticationDao implementation - * using LDAP service for user authentication. - * -*

Example use:
-* <bean id="ldapDaoImpl" class="net.sf.acegisecurity.providers.dao.ldap.LdapPasswordAuthenticationDao">
-* <property name="host"><value>sydney.ipov.info</value></property>
-* <property name="rootContext"><value>dc=ipov,dc=info</value></property>
-* <property name="userContext"><alue>ou=Users</value></property>
-* <property name="userAttribute"><value>uid</value></property>
-* </bean>
-* ...
-* <bean id="authenticationProvider" class="net.sf.acegisecurity.providers.dao.PasswordDaoAuthenticationProvider">
-* <property name="passwordAuthenticationDao"><ref local="ldapDaoImpl"/></property>
-* </bean>
+*

+* LdapPasswordAuthenticationDao allows you to authenticate user's against LDAP Directories via JNDI. +* LDAP administrators have a wide variety of options available to them when configuring a server, +* so the LdapPasswordAuthenticationDao has a wide variety of ways that it can be configured.

+* +* +*

+* Currently LdapPasswordAuthenticationDao authenticates a username/password pair by +* 'logging in to' the LDAP server via a JNDI bind() operation. +* There is some flexibility in that multiple userContexts can be set; the +* LdapPasswordAuthenticationDao will attempt to bind() against each until either a bind() +* operation succeeds or all userContexts have been tried.

+* +*

+* LdapPasswordAuthenticationDao offers 3 modes for determining the roles assigned to a user +* (these can be used in combination).

+* +* +*

+* If the both the userRolesAttributes method and the roleContexts search method are used, +* and if both return results, then the final list of roles will be determined by combining the two results. *

- * - * @author Karel Miarka - * @author Daniel Miller - * @author Robert Sanders - */ +*

+* One final operation is performed before returning the list of GrantedAuthority +* objects associated with the user: if the the upperCaseRoleNames property is set to +* true the user's role names are capitalized; then the values of the rolePrefix and roleSuffix +* are used to wrap any role names. +*

+*

+* At this point a few examples will probably help clear up the confusion +* that the abstract description above may have created. +* Unless otherwise noted, all examples will use the following base set of assumptions: +* An LDAP server reachable at the url ldap://ldap.mycompany.com:389/ +* and a rootContext of dc=mycompany,dc=com. The following would be you AuthenticationProvider: +*

+*     <bean id="authenticationProvider" class="net.sf.acegisecurity.providers.dao.PasswordDaoAuthenticationProvider">
+*         <property name="passwordAuthenticationDao"><ref local="ldapDaoImpl"/></property>
+*     </bean>
+* 
+*

+* +*

+* First example: your users are stored under the rootContext as cn=USERNAME,ou=Users; +* user objects have the attribute memberOf which contains the names of any roles they +* have been granted. You would use the following bean configuration: +*

+*     <bean id="ldapDaoImpl" class="net.sf.acegisecurity.providers.dao.ldap.LdapPasswordAuthenticationDao">
+*         <property name="url"><value>ldap://ldap.mycompany.com:389/</value></property>
+*         <property name="rootContext"><value>dc=mycompany,dc=com</value></property>
+*         <property name="userContext"><alue>cn={0},ou=Users,dc=mycompany,dc=com</value></property>
+*         <property name="userRolesAttribute"><value>memberOf</value></property>
+*     </bean>
+* 
+*

+* +*

+* Second example: users are stored under the rootContext as uid=USERNAME,ou=Users; +* user object have no role information. Groups (aka roles) are stored as objects +* under the context ou=Groups and have an attribute memberUid which contains the +* full distinguished name of the user. You would use the following bean configuration: +*

+*     <bean id="ldapDaoImpl" class="net.sf.acegisecurity.providers.dao.ldap.LdapPasswordAuthenticationDao">
+*         <property name="url"><value>ldap://ldap.mycompany.com:389/</value></property> 
+*         <property name="rootContext"><value>dc=mycompany,dc=com</value></property> 
+*         <!-- here {0} is the username -->
+*         <property name="userContext"><value>uid={0},ou=Users,dc=mycompany,dc=com</value></property>
+*         <property name="roleContext"><value>ou=Groups</value></property> 
+*         <!-- here {0} is the distinguished name (which would be uid=USERNAME,ou=Users,dc=mycompany,cd=com
+*           and {1} is the username. -->
+*         <property name="roleAttributesSearchFilter"><value>(memberUid={0})</value></property> 
+*         <property name="roleNameAttribute"><value>memberUid</value></property> 
+*     </bean>
+* 
+*

+* +*

+* Third example: under the rootContext your users are stored as uid=USERNAME,ou=Users. +* You don't care about the roles stored in the LDAP, all you want to know is if the user +* can login via LDAP. You would use the following bean configuration: +*

+*     <bean id="ldapDaoImpl" class="net.sf.acegisecurity.providers.dao.ldap.LdapPasswordAuthenticationDao">
+*         <property name="url"><value>ldap://ldap.mycompany.com:389/</value></property> 
+*         <property name="rootContext"><value>dc=mycompany,dc=com</value></property> 
+*         <property name="userContext"><alue>cn={0},ou=Users,dc=mycompany,dc=com</value></property> 
+*         <property name="defaultRolename"><value>USER</value></property> 
+*     </bean>
+* 
+*

+* +*

+* Forth example (something more complex): under the rootContext your users are stored in to seperate subContexts. +* Your internal users are under uid=USERNAME,ou=Users; you also have client logins stored +* under the context uid=USERNAME,ou=Clients. For internal users role information is stored +* under the context ou=Groups and have an attribute memberUid which contains the +* full distinguished name of the user. For clients, role information is stored as an attribute +* memberOf as part of their user object. You could split the definitions up into two separate +* LdapPasswordAuthenticationDao beans, but you could also use: +*

+*     <bean id="ldapDaoImpl" class="net.sf.acegisecurity.providers.dao.ldap.LdapPasswordAuthenticationDao">
+*         <property name="url"><value>ldap://ldap.mycompany.com:389/</value></property> 
+*         <property name="rootContext"><value>dc=mycompany,dc=com</value></property> 
+*         <!-- here {0} is the username -->
+*         <property name="userContexts">
+*           <list>
+*             <value>uid={0},ou=Users,dc=mycompany,dc=com</value>
+*             <value>uid={0},ou=Clients,dc=mycompany,dc=com</value>
+*           </list>
+*         </property>
+*         <property name="userRolesAttribute"><value>memberOf</value></property> 
+*         <property name="roleContext"><value>ou=Groups</value></property> 
+*         <!-- here {0} is the distinguished name (which would be uid=USERNAME,ou=Users,dc=mycompany,cd=com
+*           and {1} is the username. -->
+*         <property name="roleAttributesSearchFilter"><value>(memberUid={0})</value></property> 
+*         <property name="roleNameAttributes"><value>memberUid</value></property> 
+*     </bean>
+* 
+*

+* +* @author Karel Miarka +* @author Daniel Miller +* @author Robert Sanders +*/ public class LdapPasswordAuthenticationDao implements PasswordAuthenticationDao { - //~ Static fields/initializers ============================================= + + /** InnerClass used to keep context variable together. */ + private class UserContext { + public DirContext dirContext; + public String userPrincipal; + + /** + * Get the attribute(s) to match when searching for the user object. This + * implementation returns a "distinguishedName" attribute with the value + * returned by getUserPrincipal(username). A subclass may + * customize this behavior by overriding getUserPrincipal + * and/or getUsernameAttributes. + * + * @param username + * DOCUMENT ME! + * + * @return DOCUMENT ME! + */ + public Attributes getUsernameAttributes() { + Attributes matchAttrs = new BasicAttributes(true); // ignore case + matchAttrs.put(new BasicAttribute("distinguishedName", userPrincipal)); + return matchAttrs; + } + } + - public static final String BAD_CREDENTIALS_EXCEPTION_MESSAGE = "Invalid username, password or context"; - private static final transient Log log = LogFactory.getLog(LdapPasswordAuthenticationDao.class); + public static final String BAD_CREDENTIALS_EXCEPTION_MESSAGE = "Invalid username, password or context"; - //~ Instance fields ======================================================== + private static final transient Log log = LogFactory + .getLog(LdapPasswordAuthenticationDao.class); - private String host; + /** Type of authentication within LDAP; default is simple. */ + private String authenticationType = "simple"; - /** The INITIAL_CONTEXT_FACTORY for use with JNDI. */ - private String initialContextFactory = "com.sun.jndi.ldap.LdapCtxFactory"; - private String rootContext; - private String userAttribute = "CN"; // ??? is this the right code?? - private String userContext = "CN=Users"; - private String[] rolesAttributes = {"memberOf"}; - private int port = 389; + /** If set to a non-null value, and a user can be bound to a LDAP Conext, + * but no role information is found then this role is automatically added. + * If null (the default) then a BadCredentialsException is thrown + * + *

For example; if you have an LDAP directory with no role information + * stored, you might simply want to give any user who can login a role of "USER".

+ */ + private String defaultRole = null; - //~ Methods ================================================================ + /** The INITIAL_CONTEXT_FACTORY used to create the JNDI Factory. + * Default is "com.sun.jndi.ldap.LdapCtxFactory"; you should not + * need to set this unless you have unusual needs. + **/ + private String initialContextFactory = "com.sun.jndi.ldap.LdapCtxFactory"; + + /** Internal variable, concatenation */ + private String providerUrl; - /** - * Set hostname or IP address of the host running LDAP server. - * - * @param hostname DOCUMENT ME! - */ - public void setHost(String hostname) { - this.host = hostname; - } + /** Used to build LDAP Search Filter for finding roles (in the roleContexts) + * pointing to a user. Uses MessageFormat like tokens; {0} is the + * user's DistiguishedName, {1} is the user's username. + * For more information on syntax see + * javax.naming.directory.DirContext.search(), or RFC 2254. + * + *

Example: if each group has an attribute 'memberUid' with values being + * the usernames of the user's in that group, then the value of this property + * would be (memberUid={1})

+ **/ + private String roleAttributesSearchFilter; - /** - * DOCUMENT ME! - * - * @return Returns the host. - */ - public String getHost() { - return host; - } + /** Contexts to search for role's (which point to the user id). + *

Example, if you have a Groups object containing Groups of users then + * the expression: ou=Groups,dc=mycompany,dc=com might be used; + * alternatively, if rootContext="dc=mycompany,dc=com" then simply use "ou=Groups" here. + **/ + private String[] roleContexts; + + /** Attribute(s) of any role object returned from the roleContexts to use as role-names. + * Warning: if you do role lookups using the roleContexts and + * roleAttributesSearchFilter then you need to set roleNameAttributes or ALL attributes + * will be returned. + * + **/ + private String[] roleNameAttributes; + + /** Prefix to be associated with any roles found for a user, + * defaults to an empty string. + * Older versions of this class used "ROLE_" for this value. */ + private String rolePrefix = ""; + + /** Suffix to be associated with any roles found for a user, + * defaults to an empty string. */ + private String roleSuffix = ""; - /** - * DOCUMENT ME! - * - * @param initialContextFactory The initialContextFactory to set. - */ - public void setInitialContextFactory(String initialContextFactory) { - this.initialContextFactory = initialContextFactory; - } + /** Root context of the LDAP Connection, if any is needed. + *

Example: dc=mycompany,dc=com

+ *

Note: It is usually preferable to add this data as part of the + * userContexts and/or roleContexts attributes.

+ **/ + private String rootContext = ""; + + /** If true then all role name values returned from the directory + * will be converted to uppercase. + */ + private boolean upperCaseRoleNames = false; + + /** + * LDAP URL (without the port) of the LDAP server to connect to; example + * ldap://dir.mycompany.com:389/ (port 389 is the standard LDAP port). + */ + private String url; + + /** One or more LDAP Contexts which contain user account information, use the + * MessageFormat key {0} to denote location where the user's username should + * be inserted into the expression to create a DistiguishedName. + *

Example:

cn={0},ou=Users,dc=mycompnay,dc=com

+ *

Alternatively, if you had set rootContext="dc=mycompany,dc=com" then + * the first example would be rewritten as cn={0},ou=Users.

+ **/ + private MessageFormat[] userContexts; - /** - * DOCUMENT ME! - * - * @return Returns the initialContextFactory. - */ - public String getInitialContextFactory() { - return initialContextFactory; - } + /** Name(s) of the attribute(s) for a user account object + * contaning role names assigned to the user. Leave unset if there are none. + * Consult your LDAP server administrator to determine these value(s). + * + **/ + private String[] userRolesAttributes; + + /** + * + * @param results Result of searching on of the roleContexts for matches against the current user. + * @param roles List of roles the user has already been assigned. + * @throws NamingException + */ + protected void addAnyRolesFound(NamingEnumeration results, Collection roles) throws NamingException { + while (results.hasMore()) { + SearchResult result = (SearchResult)results.next(); + Attributes attrs = result.getAttributes(); + if (attrs == null) { + continue; + } + // Here we loop over the attributes returned in the SearchResult + // TODO replace with Utility method call: + NamingEnumeration e = attrs.getAll(); + while (e.hasMore()) { + Attribute a = (Attribute)e.next(); + for (int i = 0; i < a.size(); i++) { + roles.add( (String)a.get(i) ); + } + } + } + } - /** - * Set the port on which is running the LDAP server.
Default value: 389 - * - * @param port DOCUMENT ME! - */ - public void setPort(int port) { - this.port = port; - } + /** + * @return Returns the defaultRole. + */ + public String getDefaultRole() { + return defaultRole; + } - /** - * DOCUMENT ME! - * - * @return Returns the port. - */ - public int getPort() { - return port; - } + /** + * Get an array GrantedAuthorities given the list of roles + * obtained from the LDAP context. Delegates to + * getGrantedAuthority(String ldapRole). This function may + * be overridden in a subclass. + * + * @param ldapRoles + * DOCUMENT ME! + * + * @return DOCUMENT ME! + */ + protected GrantedAuthority[] getGrantedAuthorities(String[] ldapRoles) { + GrantedAuthority[] grantedAuthorities = new GrantedAuthority[ldapRoles.length]; - public String getProviderURL() { - StringBuffer providerUrl = new StringBuffer(); - providerUrl.append("ldap://"); - providerUrl.append(this.host); - providerUrl.append(":"); - providerUrl.append(this.port); - providerUrl.append("/"); - providerUrl.append(this.rootContext); + for (int i = 0; i < ldapRoles.length; i++) { + grantedAuthorities[i] = getGrantedAuthority(ldapRoles[i]); + } - return providerUrl.toString(); - } + return grantedAuthorities; + } - /** - * Set the name of user object's attribute(s) which contains the list of - * user's role names. The role is converted to upper case and a "ROLE_" - * prefix is added when GrantedAuthority is created. Default - * value: { "memberOf" }. - * - * @param rolesAttributes DOCUMENT ME! - */ - public void setRolesAttributes(String[] rolesAttributes) { - this.rolesAttributes = rolesAttributes; - } + /** + * Get a GrantedAuthority given a role obtained from the LDAP + * context. If found in the LDAP role, the following characters are + * converted to underscore: ',' (comma), '=' (equals), ' ' (space) This + * function may be overridden in a subclass. + * + * @param ldapRole + * DOCUMENT ME! + * + * @return DOCUMENT ME! + */ + protected GrantedAuthority getGrantedAuthority(String ldapRole) { + String roleName = rolePrefix + ldapRole.toUpperCase() + roleSuffix; + if (upperCaseRoleNames) { + roleName = roleName.toUpperCase(); + } + GrantedAuthority ga = new GrantedAuthorityImpl(roleName); - /** - * Set the root context to which you attempt to log in.
- * For example: DC=yourdomain,DC=com - * - * @param rootContext DOCUMENT ME! - */ - public void setRootContext(String rootContext) { - this.rootContext = rootContext; - } + if (log.isDebugEnabled()) { + log.debug("GrantedAuthority: " + ga); + } - /** - * DOCUMENT ME! - * - * @param userAttribute The userAttribute to set. - */ - public void setUserAttribute(String userAttribute) { - this.userAttribute = userAttribute; - } + return ga; + } + + /** + * @return The InitialContextFactory for creating the root JNDI context; defaults to "com.sun.jndi.ldap.LdapCtxFactory" + */ + public String getInitialContextFactory() { + return initialContextFactory; + } + + // ~ Methods + // ================================================================ + + /** + * Given a password, construct the Hashtable of JNDI values for a bind attempt. + */ + protected Hashtable getJdniEnvironment(String password) { + Hashtable env = new Hashtable(11); + env.put(Context.INITIAL_CONTEXT_FACTORY, initialContextFactory); + env.put(Context.PROVIDER_URL, getProviderURL()); + env.put(Context.SECURITY_AUTHENTICATION, authenticationType); + env.put(Context.SECURITY_CREDENTIALS, password); + return env; + } + + /** + * @return The full "Provuder" URL for the LDAP source; it should look + * something like: ldap://www.mycompany.com:389/ + */ + public synchronized String getProviderURL() { + if (null == this.providerUrl) { + StringBuffer providerUrl = new StringBuffer( this.url ); + if (!this.url.endsWith("/")) { + providerUrl.append("/"); + } + providerUrl.append(this.rootContext); + this.providerUrl = providerUrl.toString(); + } + return this.providerUrl; + } - /** - * DOCUMENT ME! - * - * @return Returns the userAttribute. - */ - public String getUserAttribute() { - return userAttribute; - } + /** + * @return Returns the roleUserAttributes. + */ + public String getRoleAttributesSearchFilter() { + return roleAttributesSearchFilter; + } - /** - * Set the context in which all users reside relative to the root context.
- * Defalut value: "CN=Users" - * - * @param userContext DOCUMENT ME! - */ - public void setUserContext(String userContext) { - this.userContext = userContext; - } + + /** + * @return Array of MessageFormat String's for Contexts that store role information for users. + */ + public String[] getRoleContexts() { + return roleContexts; + } + + /** + * @return Returns the roleNameAttributes. + */ + public String[] getRoleNameAttributes() { + return roleNameAttributes; + } + + /** + * @return Returns the rolePrefix. + */ + public String getRolePrefix() { + return rolePrefix; + } + - public UserDetails loadUserByUsernameAndPassword(String username, - String password) throws DataAccessException, BadCredentialsException { - if ((password == null) || (password.length() == 0)) { - throw new BadCredentialsException("Empty password"); - } + protected Collection getRolesFromRoleSearch(UserContext userContext, String username, String[] roleAttributes) { + if ((null == roleContexts) || (roleContexts.length == 0)) { + return null; + } + String[] searchFilterVars = new String[] {userContext.userPrincipal, username}; + + SearchControls controls = new SearchControls(); + controls.setSearchScope(SearchControls.SUBTREE_SCOPE); + controls.setReturningAttributes(roleAttributes); + + List roles = new ArrayList(); + for (int i = 0; i < roleContexts.length; i++) { + try { + NamingEnumeration results = userContext.dirContext.search( + roleContexts[i], roleAttributesSearchFilter, searchFilterVars, controls); + addAnyRolesFound(results, roles); + } catch (NamingException e) { + if (log.isInfoEnabled()) { + log.info("Unable to find user-role match in context = " + roleContexts[i], e); + } + } + } + return roles; + } - Hashtable env = new Hashtable(11); + /** + * Looksup any roleAttributes associated with the user's DN within the DirContext. + * + * @param userContext + * UserContext Object containing DirContext in which to operate, and the user's DistinguishedName. + * @param roleAttributes + * Names of all attributes to search for role name information. + * @return Collection of roles granted to the user within the JNDI Context. + * @throws NamingException + */ + protected Collection getRolesFromUserContext(UserContext userContext, String[] roleAttributes) + throws NamingException { + List roles = new ArrayList(); + if (roleAttributes != null) { + if (log.isDebugEnabled()) { + StringBuffer rolesString = new StringBuffer(); + + for (int i = 0; i < roleAttributes.length; i++) { + rolesString.append(", "); + rolesString.append(roleAttributes[i]); + } + + log.debug("Searching user context '" + userContext.userPrincipal + "' for roles " + + "attributes: " + rolesString.substring(1)); + } + Attributes attrs = userContext.dirContext.getAttributes(userContext.userPrincipal, roleAttributes); + NamingEnumeration roleEnum = attrs.getAll(); + while (roleEnum.hasMore()) { + Attribute roleAttr = (Attribute)roleEnum.next(); + for (int i = 0; i < roleAttr.size(); i++) { + roles.add( roleAttr.get(i) ); + } + } + } + return roles; + } + + /** + * @return Returns the roleSuffix. + */ + public String getRoleSuffix() { + return roleSuffix; + } - env.put(Context.INITIAL_CONTEXT_FACTORY, - "com.sun.jndi.ldap.LdapCtxFactory"); - env.put(Context.PROVIDER_URL, getProviderURL()); - env.put(Context.SECURITY_AUTHENTICATION, "simple"); - env.put(Context.SECURITY_PRINCIPAL, getUserPrincipal(username)); - env.put(Context.SECURITY_CREDENTIALS, password); + /** + * @return Returns the rootContext which to connect to; + * typically it could look something like: dc=mycompany,dc=com. + */ + public String getRootContext() { + return rootContext; + } + + /** + * @return The LDAP URL to conntect to; example: ldap://ldap.mycompany.com:389/ + */ + public String getURL() { + return url; + } + + /** Attempts to bind to the userContexts; returning on the first successful bind; + * or failing with a BadCredentialsException. + * @param username + * @param password + * @return UserContext, an innerclass holding the DirContext, and the user's LDAP Principal String. + * @throws NamingException + * @throws BadCredentialsException + */ + protected UserContext getUserContext(String username, String password) throws NamingException, BadCredentialsException { + Hashtable env = getJdniEnvironment(password); + UserContext userContext = new UserContext(); + for (int i = 0; i < userContexts.length; i++) { + env.remove(Context.SECURITY_PRINCIPAL); + userContext.userPrincipal = userContexts[i].format(new String[]{username}); + env.put(Context.SECURITY_PRINCIPAL, userContext.userPrincipal); + try { + userContext.dirContext = new InitialDirContext(env); + if (userContext.dirContext != null) { + return userContext; + } + } catch (AuthenticationException ax) { + if (log.isInfoEnabled()) { + log.info("Authentication exception for user.", ax); + } + } + } + throw new BadCredentialsException(BAD_CREDENTIALS_EXCEPTION_MESSAGE); + } + - try { - if (log.isDebugEnabled()) { - log.debug("Connecting to " + getProviderURL() + " as " - + getUserPrincipal(username)); - } + /** + * @return Returns the userContexts. + */ + public String[] getUserContexts() { + String[] formats = new String[userContexts.length]; + for (int i = 0; i < userContexts.length; i++) { + formats[i] = userContexts[i].toPattern(); + } + return formats; + } - DirContext ctx = new InitialDirContext(env); + /** + * @return Returns the userRolesAttributes. + */ + public String[] getUserRolesAttributes() { + return userRolesAttributes; + } - String[] attrIDs = getRolesAttributeNames(); - Collection roles = getRolesFromContext(ctx, userContext, username, - attrIDs); - ctx.close(); + /** + * @FIXME When using a search (see getRolesFromContext()) I don't think this + * extra check is needed; JNDI should be responible for returning + * only the attributes requested (or maybe I don't understand JNDI + * well enough). + * + * @param Name/Id + * of the JNDI Attribute. + * + * @return Return true if the given name is a role attribute. + */ + protected boolean isRoleAttribute(String name) { + log.info("Checking rolename: " + name); + if (name != null) { + for (int i = 0; i < userRolesAttributes.length; i++) { + if (name.equals(userRolesAttributes[i])) { + return true; + } + } + } + return false; + } + + /** + * @return Returns the upperCaseRoleNames. + */ + public boolean isUpperCaseRoleNames() { + return upperCaseRoleNames; + } + + - if (roles.isEmpty()) { - throw new BadCredentialsException("The user has no granted " - + "authorities or the rolesAttribute is invalid"); - } + public UserDetails loadUserByUsernameAndPassword(String username, + String password) throws DataAccessException, + BadCredentialsException { + if ((password == null) || (password.length() == 0)) { + throw new BadCredentialsException("Empty password"); + } - String[] ldapRoles = (String[]) roles.toArray(new String[] {}); + try { + if (log.isDebugEnabled()) { + log.debug("Connecting to " + getProviderURL() + " as " + username); + } - return new User(username, password, true, true, true, - getGrantedAuthorities(ldapRoles)); - } catch (AuthenticationException ex) { - throw new BadCredentialsException(BAD_CREDENTIALS_EXCEPTION_MESSAGE, - ex); - } catch (CommunicationException ex) { - throw new DataAccessResourceFailureException(ex.getRootCause() - .getMessage(), ex); - } catch (NamingException ex) { - throw new DataAccessResourceFailureException(ex.getMessage(), ex); - } - } + UserContext userContext = getUserContext(username, password); - /** - * Get an array GrantedAuthorities given the list of roles - * obtained from the LDAP context. Delegates to - * getGrantedAuthority(String ldapRole). This function may be - * overridden in a subclass. - * - * @param ldapRoles DOCUMENT ME! - * - * @return DOCUMENT ME! - */ - protected GrantedAuthority[] getGrantedAuthorities(String[] ldapRoles) { - GrantedAuthority[] grantedAuthorities = new GrantedAuthority[ldapRoles.length]; + Collection roles = getRolesFromUserContext(userContext, getUserRolesAttributes()); + Collection roles2 = getRolesFromRoleSearch(userContext, username, getRoleNameAttributes()); + if (null != roles2) { + roles.addAll(roles2); + } + + userContext.dirContext.close(); - for (int i = 0; i < ldapRoles.length; i++) { - grantedAuthorities[i] = getGrantedAuthority(ldapRoles[i]); - } + + if (roles.isEmpty()) { + if (null == defaultRole) { + throw new BadCredentialsException("The user has no granted " + + "authorities or the rolesAttribute is invalid"); + } else { + roles.add(defaultRole); + } + } - return grantedAuthorities; - } + String[] ldapRoles = (String[]) roles.toArray(new String[] {}); + + return new User(username, password, true, true, true, + getGrantedAuthorities(ldapRoles)); + } catch (AuthenticationException ex) { + throw new BadCredentialsException( + BAD_CREDENTIALS_EXCEPTION_MESSAGE, ex); + } catch (CommunicationException ex) { + throw new DataAccessResourceFailureException(ex.getRootCause() + .getMessage(), ex); + } catch (NamingException ex) { + throw new DataAccessResourceFailureException(ex.getMessage(), ex); + } + } + + /** If set to a non-null value, and a user can be bound to a LDAP Conext, + * but no role information is found then this role is automatically added. + * If null (the default) then a BadCredentialsException is thrown + * + *

For example; if you have an LDAP directory with no role information + * stored, you might simply want to give any user who can login a role of "USER".

+ * + * @param defaultRole The defaultRole to set. + */ + public void setDefaultRole(String defaultRole) { + this.defaultRole = defaultRole; + } - /** - * Get a GrantedAuthority given a role obtained from the LDAP - * context. If found in the LDAP role, the following characters are - * converted to underscore: ',' (comma), '=' (equals), ' ' (space) This - * function may be overridden in a subclass. - * - * @param ldapRole DOCUMENT ME! - * - * @return DOCUMENT ME! - */ - protected GrantedAuthority getGrantedAuthority(String ldapRole) { - GrantedAuthority ga = new GrantedAuthorityImpl("ROLE_" - + ldapRole.toUpperCase()); + /** The INITIAL_CONTEXT_FACTORY used to create the JNDI Factory. + * Default is "com.sun.jndi.ldap.LdapCtxFactory"; you should not + * need to set this unless you have unusual needs. + * + * @param initialContextFactory The InitialContextFactory for creating the root JNDI context; + * defaults to "com.sun.jndi.ldap.LdapCtxFactory" + */ + public void setInitialContextFactory(String initialContextFactory) { + this.initialContextFactory = initialContextFactory; + } - if (log.isDebugEnabled()) { - log.debug("GrantedAuthority: " + ga); - } + /** Name(s) of the attribute(s) for a user account object + * contaning role names assigned to the user. Leave unset if there are none. + * Consult your LDAP server administrator to determine these value(s). + * + * @param roleUserAttributes + * The roleUserAttributes to set. + */ + public void setRoleAttributesSearchFilter(String roleAttributesSearchArgs) { + this.roleAttributesSearchFilter = roleAttributesSearchArgs; + } + + /** Shortcut for setRoleContexts( new String[]{roleContext} ); */ + public void setRoleContext(String roleContext) { + setRoleContexts( new String[]{roleContext} ); + } - return ga; - } + /** Contexts to search for role's (which point to the user id). + *

Example, if you have a Groups object containing Groups of users then + * the expression: ou=Groups,dc=mycompany,dc=com might be used; + * alternatively, if rootContext="dc=mycompany,dc=com" then simply use "ou=Groups" here. + * + * @param roleContexts Array of MessageFormat String's for Contexts that store role information for users. + */ + public void setRoleContexts(String[] roleContexts) { + this.roleContexts = roleContexts; + } + + /** Used to build LDAP Search Filter for finding roles (in the roleContexts) + * pointing to a user. Uses MessageFormat like tokens; {0} is the + * user's DistiguishedName, {1} is the user's username. + * For more information on syntax see + * javax.naming.directory.DirContext.search(), or RFC 2254. + * + *

Example: if each group has an attribute 'memberUid' with values being + * the usernames of the user's in that group, then the value of this property + * would be (memberUid={1})

+ * + * @param roleNameAttributes The roleNameAttributes to set. + */ + public void setRoleNameAttribute(String roleNameAttribute) { + setRoleNameAttributes( new String[] {roleNameAttribute} ); + } + + /** Attribute(s) of any role object returned from the roleContexts to use as role-names. + * Warning: if you do role lookups using the roleContexts and + * roleAttributesSearchFilter then you need to set roleNameAttributes or ALL attributes + * will be returned. + * + * @param roleNameAttributes The roleNameAttributes to set. + */ + public void setRoleNameAttributes(String[] roleNameAttributes) { + this.roleNameAttributes = roleNameAttributes; + } + + /** Prefix to be associated with any roles found for a user, + * defaults to an empty string. + * Older versions of this class used "ROLE_" for this value. + * + * @param rolePrefix The rolePrefix to set. + */ + public void setRolePrefix(String rolePrefix) { + this.rolePrefix = rolePrefix; + } + + /** Suffix to be associated with any roles found for a user, + * defaults to an empty string. + * + * @param roleSuffix The roleSuffix to set. + */ + public void setRoleSuffix(String roleSuffix) { + this.roleSuffix = roleSuffix; + } + + /** Root context of the LDAP Connection, if any is needed. + *

Example: dc=mycompany,dc=com

+ *

Note: It is usually preferable to add this data as part of the + * userContexts and/or roleContexts attributes.

+ * + * @param rootContext The rootContext which to connect to; + * typically it could look something like: dc=mycompany,dc=com. + */ + public void setRootContext(String rootContext) { + this.rootContext = rootContext; + } + + /** If true then all role name values returned from the directory + * will be converted to uppercase. + * + * @param upperCaseRoleNames The upperCaseRoleNames to set. + */ + public void setUpperCaseRoleNames(boolean upperCaseRoleNames) { + this.upperCaseRoleNames = upperCaseRoleNames; + } - /** - * DOCUMENT ME! - * - * @param name DOCUMENT ME! - * - * @return Return true if the given name is a role attribute. - */ - protected boolean isRoleAttribute(String name) { - if (name != null) { - for (int i = 0; i < rolesAttributes.length; i++) { - if (name.equals(rolesAttributes[i])) { - return true; - } - } - } + /** + * @param host The LDAP URL to conntect to; example: ldap://ldap.mycompany.com:389/ + */ + public void setURL(String url) { + this.url = url; + } + + /** Shortcut for setUserContexts( new String[]{userContext} ); */ + public void setUserContext(String userContext) { + setUserContexts( new String[]{userContext} ); + } - return false; - } + /** One or more LDAP Contexts which contain user account information, use the + * MessageFormat key {0} to denote location where the user's username should + * be inserted into the expression to create a DistiguishedName. + *

Example:

cn={0},ou=Users,dc=mycompnay,dc=com

+ *

Alternatively, if you had set rootContext="dc=mycompany,dc=com" then + * the first example would be rewritten as cn={0},ou=Users.

+ * + * @param userContexts + * The userContexts to set. + */ + public void setUserContexts(String[] userContexts) { + this.userContexts = new MessageFormat[userContexts.length]; + for (int i = 0; i < userContexts.length; i++) { + this.userContexts[i] = new MessageFormat(userContexts[i]); + } + } + + /** Shortcut for setUserRolesAttributes(new String[]{userRolesAttribute}); */ + public void setUserRolesAttribute(String userRolesAttribute) { + this.userRolesAttributes = new String[]{userRolesAttribute}; + } - /** - * Get the attributes to that contain role information. This function may - * be overridden in a subclass. - * - * @return DOCUMENT ME! - */ - protected String[] getRolesAttributeNames() { - return rolesAttributes; - } + /** Attribute(s) of any role object returned from the roleContexts to use as role-names. + * Warning: if you do role lookups using the roleContexts and + * roleAttributesSearchFilter then you need to set roleNameAttributes or ALL attributes + * will be returned. + * + * @param userRolesAttributes + * The userRolesAttributes to set. + */ + public void setUserRolesAttributes(String[] userRolesAttributes) { + this.userRolesAttributes = userRolesAttributes; + } - protected Collection getRolesFromContext(DirContext ctx, - String userContext, String username, String[] roleAttributes) - throws NamingException { - List roles = new ArrayList(); - - if (log.isDebugEnabled()) { - String rolesString = ""; - - for (int i = 0; i < roleAttributes.length; i++) { - rolesString += (", " + roleAttributes[i]); - } - - log.debug("Searching user context '" + userContext + "' for roles " - + "attributes: " + rolesString.substring(1)); - } - - NamingEnumeration answer = ctx.search(userContext, - getUsernameAttributes(username), roleAttributes); - - while (answer.hasMore()) { - SearchResult sr = (SearchResult) answer.next(); - NamingEnumeration attrs = sr.getAttributes().getAll(); - - while (attrs.hasMore()) { - Attribute attr = (Attribute) attrs.next(); - - if (isRoleAttribute(attr.getID())) { - NamingEnumeration rolesAttr = attr.getAll(); - - while (rolesAttr.hasMore()) { - String role = (String) rolesAttr.next(); - roles.add(role); - - if (log.isDebugEnabled()) { - log.debug("Role read: " + attr.getID() + "=" + role); - } - } - } - } - } - - return roles; - } - - /** - * Get the Context.SECURITY_PRINCIPAL for the given username - * string. This implementation returns the userBase for JNDI / LDAP - * lookup. - * - * @param username DOCUMENT ME! - * - * @return DOCUMENT ME! - */ - protected String getUserPrincipal(String username) { - StringBuffer principal = new StringBuffer(userAttribute); - principal.append("="); - principal.append(username); - principal.append(","); - principal.append(this.userContext); - principal.append(","); - principal.append(this.rootContext); - - return principal.toString(); - } - - /** - * Get the attribute(s) to match when searching for the user object. This - * implementation returns a "distinguishedName" attribute with the value - * returned by getUserPrincipal(username). A subclass may - * customize this behavior by overriding getUserPrincipal - * and/or getUsernameAttributes. - * - * @param username DOCUMENT ME! - * - * @return DOCUMENT ME! - */ - protected Attributes getUsernameAttributes(String username) { - Attributes matchAttrs = new BasicAttributes(true); // ignore case - matchAttrs.put(new BasicAttribute("distinguishedName", - getUserPrincipal(username))); - - return matchAttrs; - } }