View Javadoc
1   /*
2    * Licensed to The Apereo Foundation under one or more contributor license
3    * agreements. See the NOTICE file distributed with this work for additional
4    * information regarding copyright ownership.
5    *
6    *
7    * The Apereo Foundation licenses this file to you under the Educational
8    * Community License, Version 2.0 (the "License"); you may not use this file
9    * except in compliance with the License. You may obtain a copy of the License
10   * at:
11   *
12   *   http://opensource.org/licenses/ecl2.txt
13   *
14   * Unless required by applicable law or agreed to in writing, software
15   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
16   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
17   * License for the specific language governing permissions and limitations under
18   * the License.
19   *
20   */
21  package org.opencastproject.userdirectory.ldap;
22  
23  import org.opencastproject.security.api.Organization;
24  import org.opencastproject.security.api.Role;
25  import org.opencastproject.security.api.SecurityService;
26  import org.opencastproject.userdirectory.JpaGroupRoleProvider;
27  
28  import org.apache.commons.lang3.StringUtils;
29  import org.slf4j.Logger;
30  import org.slf4j.LoggerFactory;
31  import org.springframework.ldap.core.DirContextOperations;
32  import org.springframework.security.core.GrantedAuthority;
33  import org.springframework.security.core.authority.SimpleGrantedAuthority;
34  import org.springframework.security.ldap.userdetails.LdapAuthoritiesPopulator;
35  
36  import java.util.Arrays;
37  import java.util.Collection;
38  import java.util.Collections;
39  import java.util.HashMap;
40  import java.util.HashSet;
41  import java.util.List;
42  import java.util.Map;
43  import java.util.Set;
44  
45  /** Map a series of LDAP attributes to user authorities in Opencast */
46  public class OpencastLdapAuthoritiesPopulator implements LdapAuthoritiesPopulator {
47  
48    public static final String ROLE_CLEAN_REGEXP = "[\\s_]+";
49    public static final String ROLE_CLEAN_REPLACEMENT = "_";
50  
51    private Set<String> attributeNames;
52    private String[] additionalAuthorities;
53    private String prefix = "";
54    private Set<String> excludedPrefixes = new HashSet<>();
55    private String groupCheckPrefix = null;
56    private boolean applyAttributesAsRoles = true;
57    private boolean applyAttributesAsGroups = true;
58    private Map<String, String[]> ldapAssignmentRoleMap = new HashMap<>();
59    private Map<String, String[]> ldapAssignmentGroupMap = new HashMap<>();
60    private boolean uppercase = true;
61    private Organization organization;
62    private SecurityService securityService;
63    private JpaGroupRoleProvider groupRoleProvider;
64    private static final Logger logger = LoggerFactory.getLogger(OpencastLdapAuthoritiesPopulator.class);
65  
66    /**
67     * Activate component
68     *
69     * @param applyAttributesAsRoles
70     *          Specifies, whether the ldap attributes should be added as a role.
71     * @param applyAttributesAsGroups
72     *          Specifies, whether the ldap attributes should be added as a group.
73     *          applyAttributesAsRoles needs to be enabled.
74     * @param ldapAssignmentRoleMap
75     *          Maps the ldap assignments to additional roles.
76     *          Key and value are expected to be uppercase if the bool uppercase is set.
77     * @param ldapAssignmentGroupMap
78     *          Maps the ldap assignments to additional groups.
79     *          Key and value are expected to be uppercase if the bool uppercase is set.
80     */
81    public OpencastLdapAuthoritiesPopulator(
82        String attributeNames,
83        String prefix,
84        String[] aExcludedPrefixes,
85        String groupCheckPrefix,
86        boolean applyAttributesAsRoles,
87        boolean applyAttributesAsGroups,
88        Map<String, String[]> ldapAssignmentRoleMap,
89        Map<String, String[]> ldapAssignmentGroupMap,
90        boolean uppercase,
91        Organization organization,
92        SecurityService securityService,
93        JpaGroupRoleProvider groupRoleProvider,
94        String... additionalAuthorities
95    ) {
96  
97      logger.debug("Creating new instance");
98  
99      if (attributeNames == null) {
100       throw new IllegalArgumentException("The attribute list cannot be null");
101     }
102 
103     if (securityService == null) {
104       throw new IllegalArgumentException("The security service cannot be null");
105     }
106     this.securityService = securityService;
107 
108     if (organization == null) {
109       throw new IllegalArgumentException("The organization cannot be null");
110     }
111     this.organization = organization;
112 
113     this.attributeNames = new HashSet<>();
114     for (String attributeName : attributeNames.split(",")) {
115       String temp = attributeName.trim();
116       if (!temp.isEmpty()) {
117         this.attributeNames.add(temp);
118       }
119     }
120     if (this.attributeNames.size() == 0) {
121       throw new IllegalArgumentException("At least one valid attribute must be provided");
122     }
123 
124     if (logger.isDebugEnabled()) {
125       logger.debug("Roles will be read from the LDAP attributes:");
126       for (String attribute : this.attributeNames) {
127         logger.debug("\t* {}", attribute);
128       }
129     }
130 
131     if (groupRoleProvider == null) {
132       logger.info("Provided GroupRoleProvider was null. Group roles will therefore not be expanded");
133     }
134     this.groupRoleProvider = groupRoleProvider;
135 
136     this.uppercase = uppercase;
137     if (uppercase) {
138       logger.debug("Roles will be converted to uppercase");
139     } else {
140       logger.debug("Roles will NOT be converted to uppercase");
141     }
142 
143     this.prefix = roleCleanUpperCase(prefix, uppercase);
144     logger.debug("Role prefix set to: {}", this.prefix);
145 
146     if (aExcludedPrefixes != null) {
147       for (String origExcludedPrefix : aExcludedPrefixes) {
148         String excludedPrefix;
149         if (uppercase) {
150           excludedPrefix = StringUtils.trimToEmpty(origExcludedPrefix).toUpperCase();
151         } else {
152           excludedPrefix = StringUtils.trimToEmpty(origExcludedPrefix);
153         }
154         if (!excludedPrefix.isEmpty()) {
155           excludedPrefixes.add(excludedPrefix);
156         }
157       }
158     }
159 
160     if (groupCheckPrefix == null) {
161       throw new IllegalArgumentException("The parameter groupCheckPrefix cannot be null");
162     }
163     this.groupCheckPrefix = groupCheckPrefix;
164     if (uppercase) {
165       this.groupCheckPrefix = this.groupCheckPrefix.toUpperCase();
166     }
167 
168     this.applyAttributesAsRoles = applyAttributesAsRoles;
169     this.applyAttributesAsGroups = applyAttributesAsGroups;
170 
171     if (ldapAssignmentRoleMap != null) {
172       this.ldapAssignmentRoleMap = ldapAssignmentRoleMap;
173     }
174 
175     if (ldapAssignmentGroupMap != null) {
176       this.ldapAssignmentGroupMap = ldapAssignmentGroupMap;
177     }
178 
179     if (additionalAuthorities == null) {
180       this.additionalAuthorities = new String[0];
181     } else {
182       this.additionalAuthorities = Arrays.stream(additionalAuthorities)
183               .map(x -> roleCleanUpperCase(x, uppercase))
184               .toArray(String[]::new);
185     }
186 
187     if (logger.isDebugEnabled()) {
188       StringBuilder additionalAuthoritiesAsStr = new StringBuilder();
189       for (String role : this.additionalAuthorities) {
190         additionalAuthoritiesAsStr.append(String.format("\n\t* %s", role));
191       }
192       logger.debug("Authenticated users will receive the following extra roles:{}", additionalAuthoritiesAsStr);
193     }
194   }
195 
196   @Override
197   public Collection<? extends GrantedAuthority> getGrantedAuthorities(DirContextOperations userData, String username) {
198 
199     logger.debug("user attributes for user {}:\n\t{}", username, userData.getAttributes());
200 
201     Set<GrantedAuthority> authorities = new HashSet<>();
202     for (String attributeName : attributeNames) {
203       logger.debug("Looking for attribute name '{}'", attributeName);
204       try {
205         String[] attributeValues = userData.getStringAttributes(attributeName);
206         // Should the attribute not be defined, the returned array is null
207         if (attributeValues != null) {
208           for (String attributeValue : attributeValues) {
209             // The attribute value may be a single authority (a single role) or a list of roles
210             String[] splitValue =  attributeValue.split(",");
211             if (applyAttributesAsRoles) {
212               String[] roles = splitValue;
213               addAuthorities(authorities, roles, false, true);
214               if (applyAttributesAsGroups) {
215                 // ignore attributes which aren't groups according to groupCheckPrefix
216                 String[] groups = Arrays.stream(splitValue)
217                         .filter(x -> {
218                           String filter = roleCleanUpperCase(x, uppercase);
219                           return filter.startsWith(groupCheckPrefix);
220                         })
221                         .toArray(String[]::new);
222                 addAuthorities(authorities, groups, true, true);
223               }
224             }
225 
226             // map attribute values to roles
227             String[] mappedRoles = Arrays.stream(splitValue)
228                      .map(x -> roleCleanUpperCase(x, uppercase))
229                      .map(x -> ldapAssignmentRoleMap.get(x))
230                      .filter(x -> x != null)
231                      .flatMap(x -> Arrays.stream(x))
232                      .toArray(String[]::new);
233             addAuthorities(authorities, mappedRoles, false, false);
234             // map attribute values to groups
235             String[] mappedGroups = Arrays.stream(splitValue)
236                     .map(x -> roleCleanUpperCase(x, uppercase))
237                     .map(x -> ldapAssignmentGroupMap.get(x))
238                     .filter(x -> x != null)
239                     .flatMap(x -> Arrays.stream(x))
240                     .toArray(String[]::new);
241             addAuthorities(authorities, mappedGroups, true, false);
242           }
243         } else {
244           logger.debug("Could not find any attribute named '{}' in user '{}'", attributeName, userData.getDn());
245         }
246       } catch (ClassCastException e) {
247         logger.error(
248             "Specified attribute containing user roles ('{}') was not of expected type String",
249             attributeName, e);
250       }
251     }
252 
253     // Add the list of additional roles
254     addAuthorities(authorities, additionalAuthorities, false, false);
255     addAuthorities(authorities, Arrays.stream(additionalAuthorities)
256         .filter(x -> x.startsWith(groupCheckPrefix))
257         .toArray(String[]::new),
258         true, false
259     );
260 
261     if (logger.isDebugEnabled()) {
262       StringBuilder authorityListAsString = new StringBuilder();
263       for (GrantedAuthority authority : authorities) {
264         authorityListAsString.append(String.format("\n\t%s", authority));
265       }
266       logger.debug("Returning user {} with authorities:{}", username, authorityListAsString);
267     }
268 
269     // Update the user in the security service if it matches the user whose authorities are being returned
270 
271     return authorities;
272   }
273 
274   /**
275    * Return the attributes names this object will search for
276    *
277    * @return a {@link Collection} containing such attribute names
278    */
279   public Collection<String> getAttributeNames() {
280     return new HashSet<>(attributeNames);
281   }
282 
283   /**
284    * Get the role prefix being used by this object. Please note that such prefix can be empty.
285    *
286    * @return the role prefix in use.
287    */
288   public String getRolePrefix() {
289     return prefix;
290   }
291 
292   /**
293    * Get the exclude prefixes being used by this object.
294    *
295    * @return the role prefix in use.
296    */
297   public String[] getExcludePrefixes() {
298     return excludedPrefixes.toArray(new String[0]);
299   }
300 
301   /**
302    * Get the property that defines whether or not the role names should be converted to uppercase.
303    *
304    * @return {@code true} if this class converts the role names to uppercase. {@code false} otherwise.
305    */
306   public boolean getConvertToUpperCase() {
307     return uppercase;
308   }
309 
310   /**
311    * Get the extra roles to be added to any user returned by this authorities populator
312    *
313    * @return A {@link Collection} of {@link String}s representing the additional roles
314    */
315   public String[] getAdditionalAuthorities() {
316     return additionalAuthorities.clone();
317   }
318 
319   /**
320    * Cleans the spaces and unnecessary underscores out of the provided Role and converts it to uppercase if needed
321    *
322    * @param rawRole
323    *          the raw Role, which should be cleaned and converted
324    * @param toUpperCase
325    *          set if the Role should be converted to uppercase
326    */
327   private String roleCleanUpperCase(String rawRole, boolean toUpperCase) {
328     if (toUpperCase) {
329       return StringUtils.trimToEmpty(rawRole).replaceAll(ROLE_CLEAN_REGEXP, ROLE_CLEAN_REPLACEMENT)
330               .toUpperCase();
331     }
332     else {
333       return StringUtils.trimToEmpty(rawRole).replaceAll(ROLE_CLEAN_REGEXP, ROLE_CLEAN_REPLACEMENT);
334     }
335   }
336 
337   /**
338    * Add the specified authorities to the provided set
339    *
340    * @param authorities
341    *          a set containing the authorities
342    * @param values
343    *          the values to add to the set
344    * @param addAsGroup
345    *          if enabled, roles and groups are added to the authorities
346    * @param addPrefix
347    *          if enabled, the set prefix is added to the authority, if no excludePrefix applies
348    */
349   private void addAuthorities(Set<GrantedAuthority> authorities, final String[] values,
350                   final boolean addAsGroup, final boolean addPrefix) {
351 
352     if (values != null) {
353       Organization org = securityService.getOrganization();
354       if (!organization.equals(org)) {
355         throw new SecurityException(String.format(
356             "Current request belongs to the organization \"%s\". Expected \"%s\"",
357             org.getId(), organization.getId()));
358       }
359 
360       for (String value : values) {
361         /*
362          * Please note the prefix logic for roles:
363          *
364          * - Roles that start with any of the "exclude prefixes" are left intact
365          * - In any other case, the "role prefix" is prepended to the roles read from LDAP
366          *
367          * This only applies to the prefix addition. The conversion to uppercase is independent from these
368          * considerations
369          */
370         String authority = roleCleanUpperCase(value, uppercase);
371 
372         // Ignore the empty parts
373         if (!authority.isEmpty()) {
374           // Check if this role is a group role and assign the groups appropriately
375           List<Role> groupRoles;
376           if (groupRoleProvider != null && addAsGroup) {
377             groupRoles = groupRoleProvider.getRolesForGroup(authority);
378           } else {
379             groupRoles = Collections.emptyList();
380           }
381 
382           // Try to add the prefix if appropriate
383           String prefix = this.prefix;
384 
385           if (addPrefix) {
386             if (!prefix.isEmpty()) {
387               boolean hasExcludePrefix = false;
388               for (String excludePrefix : excludedPrefixes) {
389                 if (authority.startsWith(excludePrefix)) {
390                   hasExcludePrefix = true;
391                   break;
392                 }
393               }
394               if (hasExcludePrefix) {
395                 prefix = "";
396               }
397             }
398           }
399           else {
400             prefix = "";
401           }
402 
403           authority = (prefix + authority).replaceAll(ROLE_CLEAN_REGEXP, ROLE_CLEAN_REPLACEMENT);
404 
405           logger.debug("Parsed LDAP role \"{}\" to role \"{}\"", value, authority);
406 
407           if (!groupRoles.isEmpty()) {
408             // The authority is a group role
409             logger.debug("Found group for the group with group role \"{}\"", authority);
410             for (Role role : groupRoles) {
411               authorities.add(new SimpleGrantedAuthority(role.getName()));
412               logger.debug("\tAdded role from role \"{}\"'s group: {}", authority, role);
413             }
414           }
415 
416           // Finally, add the authority itself
417           authorities.add(new SimpleGrantedAuthority(authority));
418 
419         } else {
420           logger.debug("Found empty authority. Ignoring...");
421         }
422       }
423     }
424   }
425 
426   /** OSGi callback for setting the role group service. */
427   public void setOrgDirectory(JpaGroupRoleProvider groupRoleProvider) {
428     this.groupRoleProvider = groupRoleProvider;
429   }
430 
431   /** OSGi callback for setting the security service. */
432   public void setSecurityService(SecurityService securityService) {
433     this.securityService = securityService;
434   }
435 
436 }