/* * Copyright 2004, 2005, 2006 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. */ package org.springframework.security.ldap.userdetails; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Set; import javax.naming.directory.SearchControls; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.ldap.core.ContextSource; import org.springframework.ldap.core.DirContextOperations; import org.springframework.ldap.core.LdapTemplate; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.ldap.SpringSecurityLdapTemplate; import org.springframework.util.Assert; /** * The default strategy for obtaining user role information from the directory. * <p> * It obtains roles by performing a search for "groups" the user is a member of. * <p> * A typical group search scenario would be where each group/role is specified using the * <tt>groupOfNames</tt> (or <tt>groupOfUniqueNames</tt>) LDAP objectClass and the user's * DN is listed in the <tt>member</tt> (or <tt>uniqueMember</tt>) attribute to indicate * that they should be assigned that role. The following LDIF sample has the groups stored * under the DN <tt>ou=groups,dc=springframework,dc=org</tt> and a group called * "developers" with "ben" and "luke" as members: * * <pre> * dn: ou=groups,dc=springframework,dc=org * objectClass: top * objectClass: organizationalUnit * ou: groups * * dn: cn=developers,ou=groups,dc=springframework,dc=org * objectClass: groupOfNames * objectClass: top * cn: developers * description: Spring Security Developers * member: uid=ben,ou=people,dc=springframework,dc=org * member: uid=luke,ou=people,dc=springframework,dc=org * ou: developer * </pre> * <p> * The group search is performed within a DN specified by the <tt>groupSearchBase</tt> * property, which should be relative to the root DN of its <tt>ContextSource</tt>. If the * search base is null, group searching is disabled. The filter used in the search is * defined by the <tt>groupSearchFilter</tt> property, with the filter argument {0} being * the full DN of the user. You can also optionally use the parameter {1}, which will be * substituted with the username. You can also specify which attribute defines the role * name by setting the <tt>groupRoleAttribute</tt> property (the default is "cn"). * <p> * The configuration below shows how the group search might be performed with the above * schema. * * <pre> * <bean id="ldapAuthoritiesPopulator" * class="org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator"> * <constructor-arg ref="contextSource"/> * <constructor-arg value="ou=groups"/> * <property name="groupRoleAttribute" value="ou"/> * <!-- the following properties are shown with their default values --> * <property name="searchSubtree" value="false"/> * <property name="rolePrefix" value="ROLE_"/> * <property name="convertToUpperCase" value="true"/> * </bean> * </pre> * * A search for roles for user "uid=ben,ou=people,dc=springframework,dc=org" would return * the single granted authority "ROLE_DEVELOPER". * <p> * The single-level search is performed by default. Setting the <tt>searchSubTree</tt> * property to true will enable a search of the entire subtree under * <tt>groupSearchBase</tt>. * * @author Luke Taylor * @author Filip Hanik */ public class DefaultLdapAuthoritiesPopulator implements LdapAuthoritiesPopulator { // ~ Static fields/initializers // ===================================================================================== private static final Log logger = LogFactory .getLog(DefaultLdapAuthoritiesPopulator.class); // ~ Instance fields // ================================================================================================ /** * A default role which will be assigned to all authenticated users if set */ private GrantedAuthority defaultRole; /** * Template that will be used for searching */ private final SpringSecurityLdapTemplate ldapTemplate; /** * Controls used to determine whether group searches should be performed over the full * sub-tree from the base DN. Modified by searchSubTree property */ private final SearchControls searchControls = new SearchControls(); /** * The ID of the attribute which contains the role name for a group */ private String groupRoleAttribute = "cn"; /** * The base DN from which the search for group membership should be performed */ private String groupSearchBase; /** * The pattern to be used for the user search. {0} is the user's DN */ private String groupSearchFilter = "(member={0})"; /** * The role prefix that will be prepended to each role name */ private String rolePrefix = "ROLE_"; /** * Should we convert the role name to uppercase */ private boolean convertToUpperCase = true; // ~ Constructors // =================================================================================================== /** * Constructor for group search scenarios. <tt>userRoleAttributes</tt> may still be * set as a property. * * @param contextSource supplies the contexts used to search for user roles. * @param groupSearchBase if this is an empty string the search will be performed from * the root DN of the context factory. If null, no search will be performed. */ public DefaultLdapAuthoritiesPopulator(ContextSource contextSource, String groupSearchBase) { Assert.notNull(contextSource, "contextSource must not be null"); this.ldapTemplate = new SpringSecurityLdapTemplate(contextSource); getLdapTemplate().setSearchControls(getSearchControls()); this.groupSearchBase = groupSearchBase; if (groupSearchBase == null) { logger.info("groupSearchBase is null. No group search will be performed."); } else if (groupSearchBase.length() == 0) { logger.info( "groupSearchBase is empty. Searches will be performed from the context source base"); } } // ~ Methods // ======================================================================================================== /** * This method should be overridden if required to obtain any additional roles for the * given user (on top of those obtained from the standard search implemented by this * class). * * @param user the context representing the user who's roles are required * @return the extra roles which will be merged with those returned by the group * search */ protected Set<GrantedAuthority> getAdditionalRoles(DirContextOperations user, String username) { return null; } /** * Obtains the authorities for the user who's directory entry is represented by the * supplied LdapUserDetails object. * * @param user the user who's authorities are required * @return the set of roles granted to the user. */ @Override public final Collection<GrantedAuthority> getGrantedAuthorities( DirContextOperations user, String username) { String userDn = user.getNameInNamespace(); if (logger.isDebugEnabled()) { logger.debug("Getting authorities for user " + userDn); } Set<GrantedAuthority> roles = getGroupMembershipRoles(userDn, username); Set<GrantedAuthority> extraRoles = getAdditionalRoles(user, username); if (extraRoles != null) { roles.addAll(extraRoles); } if (this.defaultRole != null) { roles.add(this.defaultRole); } List<GrantedAuthority> result = new ArrayList<GrantedAuthority>(roles.size()); result.addAll(roles); return result; } public Set<GrantedAuthority> getGroupMembershipRoles(String userDn, String username) { if (getGroupSearchBase() == null) { return new HashSet<GrantedAuthority>(); } Set<GrantedAuthority> authorities = new HashSet<GrantedAuthority>(); if (logger.isDebugEnabled()) { logger.debug("Searching for roles for user '" + username + "', DN = " + "'" + userDn + "', with filter " + this.groupSearchFilter + " in search base '" + getGroupSearchBase() + "'"); } Set<String> userRoles = getLdapTemplate().searchForSingleAttributeValues( getGroupSearchBase(), this.groupSearchFilter, new String[] { userDn, username }, this.groupRoleAttribute); if (logger.isDebugEnabled()) { logger.debug("Roles from search: " + userRoles); } for (String role : userRoles) { if (this.convertToUpperCase) { role = role.toUpperCase(); } authorities.add(new SimpleGrantedAuthority(this.rolePrefix + role)); } return authorities; } protected ContextSource getContextSource() { return getLdapTemplate().getContextSource(); } protected String getGroupSearchBase() { return this.groupSearchBase; } /** * Convert the role to uppercase */ public void setConvertToUpperCase(boolean convertToUpperCase) { this.convertToUpperCase = convertToUpperCase; } /** * The default role which will be assigned to all users. * * @param defaultRole the role name, including any desired prefix. */ public void setDefaultRole(String defaultRole) { Assert.notNull(defaultRole, "The defaultRole property cannot be set to null"); this.defaultRole = new SimpleGrantedAuthority(defaultRole); } public void setGroupRoleAttribute(String groupRoleAttribute) { Assert.notNull(groupRoleAttribute, "groupRoleAttribute must not be null"); this.groupRoleAttribute = groupRoleAttribute; } public void setGroupSearchFilter(String groupSearchFilter) { Assert.notNull(groupSearchFilter, "groupSearchFilter must not be null"); this.groupSearchFilter = groupSearchFilter; } /** * Sets the prefix which will be prepended to the values loaded from the directory. * Defaults to "ROLE_" for compatibility with <tt>RoleVoter</tt>. */ public void setRolePrefix(String rolePrefix) { Assert.notNull(rolePrefix, "rolePrefix must not be null"); this.rolePrefix = rolePrefix; } /** * If set to true, a subtree scope search will be performed. If false a single-level * search is used. * * @param searchSubtree set to true to enable searching of the entire tree below the * <tt>groupSearchBase</tt>. */ public void setSearchSubtree(boolean searchSubtree) { int searchScope = searchSubtree ? SearchControls.SUBTREE_SCOPE : SearchControls.ONELEVEL_SCOPE; this.searchControls.setSearchScope(searchScope); } /** * Sets the corresponding property on the underlying template, avoiding specific * issues with Active Directory. * * @see LdapTemplate#setIgnoreNameNotFoundException(boolean) */ public void setIgnorePartialResultException(boolean ignore) { getLdapTemplate().setIgnorePartialResultException(ignore); } /** * Returns the current LDAP template. Method available so that classes extending this * can override the template used * @return the LDAP template * @see org.springframework.security.ldap.SpringSecurityLdapTemplate */ protected SpringSecurityLdapTemplate getLdapTemplate() { return this.ldapTemplate; } /** * Returns the attribute name of the LDAP attribute that will be mapped to the role * name Method available so that classes extending this can override * @return the attribute name used for role mapping * @see #setGroupRoleAttribute(String) */ protected final String getGroupRoleAttribute() { return this.groupRoleAttribute; } /** * Returns the search filter configured for this populator Method available so that * classes extending this can override * @return the search filter * @see #setGroupSearchFilter(String) */ protected final String getGroupSearchFilter() { return this.groupSearchFilter; } /** * Returns the role prefix used by this populator Method available so that classes * extending this can override * @return the role prefix * @see #setRolePrefix(String) */ protected final String getRolePrefix() { return this.rolePrefix; } /** * Returns true if role names are converted to uppercase Method available so that * classes extending this can override * @return true if role names are converted to uppercase. * @see #setConvertToUpperCase(boolean) */ protected final boolean isConvertToUpperCase() { return this.convertToUpperCase; } /** * Returns the default role Method available so that classes extending this can * override * @return the default role used * @see #setDefaultRole(String) */ private GrantedAuthority getDefaultRole() { return this.defaultRole; } /** * Returns the search controls Method available so that classes extending this can * override the search controls used * @return the search controls */ private SearchControls getSearchControls() { return this.searchControls; } }