View Javadoc

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   * &lt;bean id="ldapAuthoritiesPopulator"
71   *       class="org.springframework.security.providers.ldap.populator.DefaultLdapAuthoritiesPopulator">
72   *   &lt;constructor-arg ref="contextSource"/>
73   *   &lt;constructor-arg value="ou=groups"/>
74   *   &lt;property name="groupRoleAttribute" value="ou"/>
75   * &lt;!-- the following properties are shown with their default values -->
76   *   &lt;property name="searchSubTree" value="false"/>
77   *   &lt;property name="rolePrefix" value="ROLE_"/>
78   *   &lt;property name="convertToUpperCase" value="true"/>
79   * &lt;/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 }