1 /* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited 2 * 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 16 package org.springframework.security.ldap.populator; 17 18 import org.springframework.security.GrantedAuthority; 19 import org.springframework.security.GrantedAuthorityImpl; 20 import org.springframework.security.ldap.SpringSecurityLdapTemplate; 21 import org.springframework.security.ldap.LdapAuthoritiesPopulator; 22 import org.springframework.ldap.core.ContextSource; 23 import org.springframework.ldap.core.DirContextOperations; 24 import org.springframework.util.Assert; 25 26 import org.apache.commons.logging.Log; 27 import org.apache.commons.logging.LogFactory; 28 29 import javax.naming.directory.SearchControls; 30 import java.util.HashSet; 31 import java.util.Iterator; 32 import java.util.Set; 33 34 35 /** 36 * The default strategy for obtaining user role information from the directory. 37 * <p> 38 * It obtains roles by performing a search for "groups" the user is a member of. 39 * <p> 40 * A typical group search scenario would be where each group/role is specified using the <tt>groupOfNames</tt> 41 * (or <tt>groupOfUniqueNames</tt>) LDAP objectClass and the user's DN is listed in the <tt>member</tt> (or 42 * <tt>uniqueMember</tt>) attribute to indicate that they should be assigned that role. The following LDIF sample has 43 * the groups stored under the DN <tt>ou=groups,dc=springframework,dc=org</tt> and a group called "developers" with 44 * "ben" and "luke" as members: 45 * <pre> 46 * dn: ou=groups,dc=springframework,dc=org 47 * objectClass: top 48 * objectClass: organizationalUnit 49 * ou: groups 50 * 51 * dn: cn=developers,ou=groups,dc=springframework,dc=org 52 * objectClass: groupOfNames 53 * objectClass: top 54 * cn: developers 55 * description: Spring Security Developers 56 * member: uid=ben,ou=people,dc=springframework,dc=org 57 * member: uid=luke,ou=people,dc=springframework,dc=org 58 * ou: developer 59 * </pre> 60 * <p> 61 * The group search is performed within a DN specified by the <tt>groupSearchBase</tt> property, which should 62 * be relative to the root DN of its <tt>InitialDirContextFactory</tt>. If the search base is null, group searching is 63 * disabled. The filter used in the search is defined by the <tt>groupSearchFilter</tt> property, with the filter 64 * argument {0} being the full DN of the user. You can also optionally use the parameter {1}, which will be substituted 65 * with the username. You can also specify which attribute defines the role name by setting 66 * the <tt>groupRoleAttribute</tt> property (the default is "cn"). 67 * <p> 68 * The configuration below shows how the group search might be performed with the above schema. 69 * <pre> 70 * <bean id="ldapAuthoritiesPopulator" 71 * class="org.springframework.security.providers.ldap.populator.DefaultLdapAuthoritiesPopulator"> 72 * <constructor-arg ref="contextSource"/> 73 * <constructor-arg value="ou=groups"/> 74 * <property name="groupRoleAttribute" value="ou"/> 75 * <!-- the following properties are shown with their default values --> 76 * <property name="searchSubTree" value="false"/> 77 * <property name="rolePrefix" value="ROLE_"/> 78 * <property name="convertToUpperCase" value="true"/> 79 * </bean> 80 * </pre> 81 * A search for roles for user "uid=ben,ou=people,dc=springframework,dc=org" would return the single granted authority 82 * "ROLE_DEVELOPER". 83 * <p> 84 * The single-level search is performed by default. Setting the <tt>searchSubTree</tt> property to true will enable 85 * a search of the entire subtree under <tt>groupSearchBase</tt>. 86 * 87 * @author Luke Taylor 88 * @version $Id: DefaultLdapAuthoritiesPopulator.java 3260 2008-08-26 12:38:02Z luke_t $ 89 */ 90 public class DefaultLdapAuthoritiesPopulator implements LdapAuthoritiesPopulator { 91 //~ Static fields/initializers ===================================================================================== 92 93 private static final Log logger = LogFactory.getLog(DefaultLdapAuthoritiesPopulator.class); 94 95 //~ Instance fields ================================================================================================ 96 97 /** 98 * A default role which will be assigned to all authenticated users if set 99 */ 100 private GrantedAuthority defaultRole; 101 102 private SpringSecurityLdapTemplate ldapTemplate; 103 104 /** 105 * Controls used to determine whether group searches should be performed over the full sub-tree from the 106 * base DN. Modified by searchSubTree property 107 */ 108 private SearchControls searchControls = new SearchControls(); 109 110 /** 111 * The ID of the attribute which contains the role name for a group 112 */ 113 private String groupRoleAttribute = "cn"; 114 115 /** 116 * The base DN from which the search for group membership should be performed 117 */ 118 private String groupSearchBase; 119 120 /** 121 * The pattern to be used for the user search. {0} is the user's DN 122 */ 123 private String groupSearchFilter = "(member={0})"; 124 125 /** 126 * Attributes of the User's LDAP Object that contain role name information. 127 */ 128 129 // private String[] userRoleAttributes = null; 130 private String rolePrefix = "ROLE_"; 131 private boolean convertToUpperCase = true; 132 133 //~ Constructors =================================================================================================== 134 135 /** 136 * Constructor for group search scenarios. <tt>userRoleAttributes</tt> may still be 137 * set as a property. 138 * 139 * @param contextSource supplies the contexts used to search for user roles. 140 * @param groupSearchBase if this is an empty string the search will be performed from the root DN of the 141 * context factory. 142 */ 143 public DefaultLdapAuthoritiesPopulator(ContextSource contextSource, String groupSearchBase) { 144 Assert.notNull(contextSource, "contextSource must not be null"); 145 ldapTemplate = new SpringSecurityLdapTemplate(contextSource); 146 ldapTemplate.setSearchControls(searchControls); 147 setGroupSearchBase(groupSearchBase); 148 } 149 150 //~ Methods ======================================================================================================== 151 152 /** 153 * This method should be overridden if required to obtain any additional 154 * roles for the given user (on top of those obtained from the standard 155 * search implemented by this class). 156 * 157 * @param user the context representing the user who's roles are required 158 * @return the extra roles which will be merged with those returned by the group search 159 */ 160 161 protected Set getAdditionalRoles(DirContextOperations user, String username) { 162 return null; 163 } 164 165 /** 166 * Obtains the authorities for the user who's directory entry is represented by 167 * the supplied LdapUserDetails object. 168 * 169 * @param user the user who's authorities are required 170 * @return the set of roles granted to the user. 171 */ 172 public final GrantedAuthority[] getGrantedAuthorities(DirContextOperations user, String username) { 173 String userDn = user.getNameInNamespace(); 174 175 if (logger.isDebugEnabled()) { 176 logger.debug("Getting authorities for user " + userDn); 177 } 178 179 Set roles = getGroupMembershipRoles(userDn, username); 180 181 Set extraRoles = getAdditionalRoles(user, username); 182 183 if (extraRoles != null) { 184 roles.addAll(extraRoles); 185 } 186 187 if (defaultRole != null) { 188 roles.add(defaultRole); 189 } 190 191 return (GrantedAuthority[]) roles.toArray(new GrantedAuthority[roles.size()]); 192 } 193 194 public Set getGroupMembershipRoles(String userDn, String username) { 195 Set authorities = new HashSet(); 196 197 if (getGroupSearchBase() == null) { 198 return authorities; 199 } 200 201 if (logger.isDebugEnabled()) { 202 logger.debug("Searching for roles for user '" + username + "', DN = " + "'" + userDn + "', with filter " 203 + groupSearchFilter + " in search base '" + getGroupSearchBase() + "'"); 204 } 205 206 Set userRoles = ldapTemplate.searchForSingleAttributeValues(getGroupSearchBase(), groupSearchFilter, 207 new String[]{userDn, username}, groupRoleAttribute); 208 209 if (logger.isDebugEnabled()) { 210 logger.debug("Roles from search: " + userRoles); 211 } 212 213 Iterator it = userRoles.iterator(); 214 215 while (it.hasNext()) { 216 String role = (String) it.next(); 217 218 if (convertToUpperCase) { 219 role = role.toUpperCase(); 220 } 221 222 authorities.add(new GrantedAuthorityImpl(rolePrefix + role)); 223 } 224 225 return authorities; 226 } 227 228 protected ContextSource getContextSource() { 229 return ldapTemplate.getContextSource(); 230 } 231 232 /** 233 * Set the group search base (name to search under) 234 * 235 * @param groupSearchBase if this is an empty string the search will be performed from the root DN of the context 236 * factory. 237 */ 238 private void setGroupSearchBase(String groupSearchBase) { 239 Assert.notNull(groupSearchBase, "The groupSearchBase (name to search under), must not be null."); 240 this.groupSearchBase = groupSearchBase; 241 if (groupSearchBase.length() == 0) { 242 logger.info("groupSearchBase is empty. Searches will be performed from the context source base"); 243 } 244 } 245 246 protected String getGroupSearchBase() { 247 return groupSearchBase; 248 } 249 250 public void setConvertToUpperCase(boolean convertToUpperCase) { 251 this.convertToUpperCase = convertToUpperCase; 252 } 253 254 /** 255 * The default role which will be assigned to all users. 256 * 257 * @param defaultRole the role name, including any desired prefix. 258 */ 259 public void setDefaultRole(String defaultRole) { 260 Assert.notNull(defaultRole, "The defaultRole property cannot be set to null"); 261 this.defaultRole = new GrantedAuthorityImpl(defaultRole); 262 } 263 264 public void setGroupRoleAttribute(String groupRoleAttribute) { 265 Assert.notNull(groupRoleAttribute, "groupRoleAttribute must not be null"); 266 this.groupRoleAttribute = groupRoleAttribute; 267 } 268 269 public void setGroupSearchFilter(String groupSearchFilter) { 270 Assert.notNull(groupSearchFilter, "groupSearchFilter must not be null"); 271 this.groupSearchFilter = groupSearchFilter; 272 } 273 274 /** 275 * Sets the prefix which will be prepended to the values loaded from the directory. 276 * Defaults to "ROLE_" for compatibility with <tt>RoleVoter/tt>. 277 */ 278 public void setRolePrefix(String rolePrefix) { 279 Assert.notNull(rolePrefix, "rolePrefix must not be null"); 280 this.rolePrefix = rolePrefix; 281 } 282 283 /** 284 * If set to true, a subtree scope search will be performed. If false a single-level search is used. 285 * 286 * @param searchSubtree set to true to enable searching of the entire tree below the <tt>groupSearchBase</tt>. 287 */ 288 public void setSearchSubtree(boolean searchSubtree) { 289 int searchScope = searchSubtree ? SearchControls.SUBTREE_SCOPE : SearchControls.ONELEVEL_SCOPE; 290 searchControls.setSearchScope(searchScope); 291 } 292 }