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  
22  package org.opencastproject.userdirectory;
23  
24  import org.opencastproject.db.DBSession;
25  import org.opencastproject.db.DBSessionFactory;
26  import org.opencastproject.security.api.Group;
27  import org.opencastproject.security.api.GroupProvider;
28  import org.opencastproject.security.api.JaxbGroupList;
29  import org.opencastproject.security.api.JaxbOrganization;
30  import org.opencastproject.security.api.JaxbRole;
31  import org.opencastproject.security.api.OrganizationDirectoryService;
32  import org.opencastproject.security.api.Role;
33  import org.opencastproject.security.api.RoleProvider;
34  import org.opencastproject.security.api.SecurityService;
35  import org.opencastproject.security.api.UnauthorizedException;
36  import org.opencastproject.security.api.UserDirectoryService;
37  import org.opencastproject.security.api.UserProvider;
38  import org.opencastproject.security.impl.jpa.JpaGroup;
39  import org.opencastproject.security.impl.jpa.JpaOrganization;
40  import org.opencastproject.security.impl.jpa.JpaRole;
41  import org.opencastproject.userdirectory.api.AAIRoleProvider;
42  import org.opencastproject.userdirectory.api.GroupRoleProvider;
43  import org.opencastproject.userdirectory.utils.UserDirectoryUtils;
44  import org.opencastproject.util.NotFoundException;
45  import org.opencastproject.util.requests.SortCriterion;
46  
47  import org.apache.commons.lang3.StringUtils;
48  import org.osgi.service.component.ComponentContext;
49  import org.osgi.service.component.annotations.Activate;
50  import org.osgi.service.component.annotations.Component;
51  import org.osgi.service.component.annotations.Reference;
52  import org.slf4j.Logger;
53  import org.slf4j.LoggerFactory;
54  
55  import java.io.IOException;
56  import java.util.ArrayList;
57  import java.util.HashSet;
58  import java.util.Iterator;
59  import java.util.List;
60  import java.util.Optional;
61  import java.util.Set;
62  import java.util.regex.Pattern;
63  
64  import javax.persistence.EntityManagerFactory;
65  
66  /**
67   * Manages and locates users using JPA.
68   */
69  @Component(
70      property = {
71          "service.description=Provides a group role directory"
72      },
73      immediate = true,
74      service = { RoleProvider.class, JpaGroupRoleProvider.class }
75  )
76  public class JpaGroupRoleProvider implements AAIRoleProvider, GroupProvider, GroupRoleProvider {
77  
78    /** The logger */
79    private static final Logger logger = LoggerFactory.getLogger(JpaGroupRoleProvider.class);
80  
81    /** The JPA persistence unit name */
82    public static final String PERSISTENCE_UNIT = "org.opencastproject.common";
83  
84    /** The security service */
85    protected SecurityService securityService = null;
86  
87    /** The factory used to generate the entity manager */
88    protected EntityManagerFactory emf = null;
89  
90    protected DBSessionFactory dbSessionFactory;
91  
92    protected DBSession db;
93  
94    /** The organization directory service */
95    protected OrganizationDirectoryService organizationDirectoryService;
96  
97    /** The user directory service */
98    protected UserDirectoryService userDirectoryService = null;
99  
100   /** The component context */
101   private ComponentContext cc;
102 
103   /** OSGi DI */
104   @Reference(target = "(osgi.unit.name=org.opencastproject.common)")
105   public void setEntityManagerFactory(EntityManagerFactory emf) {
106     this.emf = emf;
107   }
108 
109   @Reference
110   public void setDBSessionFactory(DBSessionFactory dbSessionFactory) {
111     this.dbSessionFactory = dbSessionFactory;
112   }
113 
114   /**
115    * Sets the user directory service
116    *
117    * @param userDirectoryService
118    *          the userDirectoryService to set
119    */
120   @Reference
121   public void setUserDirectoryService(UserDirectoryService userDirectoryService) {
122     this.userDirectoryService = userDirectoryService;
123   }
124 
125   /**
126    * @param securityService
127    *          the securityService to set
128    */
129   @Reference
130   public void setSecurityService(SecurityService securityService) {
131     this.securityService = securityService;
132   }
133 
134   /**
135    * @param organizationDirectoryService
136    *          the organizationDirectoryService to set
137    */
138   @Reference
139   public void setOrganizationDirectoryService(OrganizationDirectoryService organizationDirectoryService) {
140     this.organizationDirectoryService = organizationDirectoryService;
141   }
142 
143   /**
144    * Callback for activation of this component.
145    *
146    * @param cc
147    *          the component context
148    */
149   @Activate
150   public void activate(ComponentContext cc) {
151     logger.debug("Activate group role provider");
152     this.cc = cc;
153     db = dbSessionFactory.createSession(emf);
154   }
155 
156   /**
157    * {@inheritDoc}
158    *
159    * @see org.opencastproject.userdirectory.api.AAIRoleProvider#getRoles()
160    */
161   @Override
162   public Iterator<Role> getRoles() {
163     String orgId = securityService.getOrganization().getId();
164     List<JpaGroup> roles = db.exec(UserDirectoryPersistenceUtil.findGroupsQuery(orgId, 0, 0));
165     return getGroupsRoles(roles).iterator();
166   }
167 
168   /**
169    * {@inheritDoc}
170    *
171    * @see org.opencastproject.security.api.RoleProvider#getRolesForUser(String)
172    */
173   @Override
174   public List<Role> getRolesForUser(String userName) {
175     String orgId = securityService.getOrganization().getId();
176     List<JpaGroup> roles = db.exec(UserDirectoryPersistenceUtil.findGroupsByUserQuery(userName, orgId));
177     return getGroupsRoles(roles);
178   }
179 
180   /**
181    * {@inheritDoc}
182    *
183    * @see org.opencastproject.security.api.RoleProvider#getRolesForUser(String)
184    */
185   @Override
186   public List<Role> getRolesForGroup(String groupName) {
187     List<Role> roles = new ArrayList<>();
188     String orgId = securityService.getOrganization().getId();
189     Optional<JpaGroup> group = db.exec(UserDirectoryPersistenceUtil.findGroupByRoleQuery(groupName, orgId));
190     if (group.isPresent()) {
191       for (Role role : group.get().getRoles()) {
192         roles.add(new JaxbRole(role.getName(), role.getOrganizationId(), role.getDescription(), Role.Type.DERIVED));
193       }
194     } else {
195       logger.warn("Group {} not found", groupName);
196     }
197     return roles;
198   }
199 
200 
201   /**
202    * {@inheritDoc}
203    *
204    * @see org.opencastproject.security.api.RoleProvider#getOrganization()
205    */
206   @Override
207   public String getOrganization() {
208     return UserProvider.ALL_ORGANIZATIONS;
209   }
210 
211   /**
212    * {@inheritDoc}
213    *
214    * @see org.opencastproject.security.api.RoleProvider#findRoles(String, Role.Target, int, int)
215    */
216   @Override
217   public Iterator<Role> findRoles(String query, Role.Target target, int offset, int limit) {
218     if (query == null) {
219       throw new IllegalArgumentException("Query must be set");
220     }
221     String orgId = securityService.getOrganization().getId();
222 
223     //  Here we want to return only the ROLE_GROUP_ names, not the roles associated with a group
224     List<JpaGroup> groups = db.exec(UserDirectoryPersistenceUtil.findGroupsQuery(orgId, 0, 0));
225 
226     List<Role> roles = new ArrayList<>();
227     for (JpaGroup group : groups) {
228       if (like(group.getRole(), query)) {
229         roles.add(new JaxbRole(
230             group.getRole(),
231             JaxbOrganization.fromOrganization(group.getOrganization()),
232             "",
233             Role.Type.GROUP
234         ));
235       }
236     }
237 
238     Set<Role> result = new HashSet<>();
239     int i = 0;
240     for (Role entry : roles) {
241       if (limit != 0 && result.size() >= limit) {
242         break;
243       }
244       if (i >= offset) {
245         result.add(entry);
246       }
247       i++;
248     }
249     return result.iterator();
250   }
251 
252   /**
253    * Updates a user's group membership
254    *
255    * @param userName
256    *          the username
257    * @param orgId
258    *          the user's organization
259    * @param roleList
260    *          the list of group role names
261    */
262   public void updateGroupMembershipFromRoles(String userName, String orgId, List<String> roleList) {
263     updateGroupMembershipFromRoles(userName, orgId, roleList, "");
264   }
265 
266   /**
267    * Updates a user's group membership
268    *
269    * @param userName
270    *          the username
271    * @param orgId
272               the user's organization
273    * @param roleList
274    *          the list of group role names
275    * @param prefix
276    *          handle only roles with given prefix
277    */
278   public void updateGroupMembershipFromRoles(String userName, String orgId, List<String> roleList, String prefix) {
279     logger.debug("updateGroupMembershipFromRoles({}, size={})", userName, roleList.size());
280 
281     // Add the user to all groups which are in the roleList, but allow the user to be part of groups
282     // without having the group role
283 
284     Set<String> membershipRoles = new HashSet<>();
285 
286     // List of the user's groups
287     List<JpaGroup> membership = db.exec(UserDirectoryPersistenceUtil.findGroupsByUserQuery(userName, orgId));
288     for (JpaGroup group : membership) {
289       if (StringUtils.isNotBlank(prefix) && !group.getRole().startsWith(prefix)) {
290         //ignore groups of other providers
291         continue;
292       }
293       if (roleList.contains(group.getRole())) {
294         // record this membership
295         membershipRoles.add(group.getRole());
296       }
297     }
298 
299     // Now add the user to any groups that they are not already a member of
300     for (String rolename : roleList) {
301       if (!membershipRoles.contains(rolename)) {
302         Optional<JpaGroup> group = db.exec(UserDirectoryPersistenceUtil.findGroupByRoleQuery(rolename, orgId));
303         if (group.isPresent()) {
304           try {
305             logger.debug("Adding user {} to group {}", userName, rolename);
306             group.get().getMembers().add(userName);
307             addGroup(group.get());
308           } catch (UnauthorizedException e) {
309             logger.warn("Unauthorized to add user {} to group {}", userName, group.get().getRole(), e);
310           }
311         } else {
312           logger.warn("Cannot add user {} to group {} - no group found with that role", userName, rolename);
313         }
314       }
315     }
316   }
317 
318   /**
319    * Removes a user from all groups
320    *
321    * @param userName
322    *          the username
323    * @param orgId
324    *          the user's organization
325    *
326    */
327   public void removeMemberFromAllGroups(String userName, String orgId) {
328     // List of the user's groups
329     List<JpaGroup> membership = db.exec(UserDirectoryPersistenceUtil.findGroupsByUserQuery(userName, orgId));
330     for (JpaGroup group : membership) {
331       try {
332         logger.debug("Removing user {} from group {}", userName, group.getRole());
333         group.getMembers().remove(userName);
334         addGroup(group);
335       } catch (UnauthorizedException e) {
336         logger.warn("Unauthorized to add or remove user {} from group {}", userName, group.getRole(), e);
337       }
338     }
339   }
340 
341   /**
342    * Loads a group from persistence
343    *
344    * @param groupId
345    *          the group id
346    * @param orgId
347    *          the organization id
348    * @return the loaded group or <code>null</code> if not found
349    */
350   public JpaGroup loadGroup(String groupId, String orgId) {
351     return db.exec(UserDirectoryPersistenceUtil.findGroupQuery(groupId, orgId))
352         .orElse(null);
353   }
354 
355   /**
356    * Get group.
357    *
358    * @param groupId
359    *
360    * @return the group
361    */
362   public JpaGroup getGroup(String groupId) {
363     String orgId = securityService.getOrganization().getId();
364     return loadGroup(groupId, orgId);
365   }
366 
367   /**
368    * Adds or updates a group to the persistence.
369    *
370    * @param group
371    *          the group to add
372    */
373   @Override
374   public void addGroup(final JpaGroup group) throws UnauthorizedException {
375     if (group != null && !UserDirectoryUtils.isCurrentUserAuthorizedHandleRoles(securityService, group.getRoles())) {
376       throw new UnauthorizedException("The user is not allowed to add or update a group with the admin role");
377     }
378 
379     Group existingGroup = loadGroup(group.getGroupId(), group.getOrganization().getId());
380     if (existingGroup != null
381         && !UserDirectoryUtils.isCurrentUserAuthorizedHandleRoles(securityService, existingGroup.getRoles())) {
382       throw new UnauthorizedException("The user is not allowed to update a group with the admin role");
383     }
384 
385     db.execTx(em -> {
386       Set<JpaRole> roles = UserDirectoryPersistenceUtil.saveRolesQuery(group.getRoles()).apply(em);
387       JpaOrganization organization = UserDirectoryPersistenceUtil.saveOrganizationQuery(group.getOrganization())
388           .apply(em);
389 
390       JpaGroup jpaGroup = new JpaGroup(group.getGroupId(), organization, group.getName(), group.getDescription(), roles,
391           group.getMembers());
392 
393       // Then save the jpaGroup
394       Optional<JpaGroup> foundGroup = UserDirectoryPersistenceUtil.findGroupQuery(jpaGroup.getGroupId(),
395           jpaGroup.getOrganization().getId()).apply(em);
396       if (foundGroup.isEmpty()) {
397         em.persist(jpaGroup);
398       } else {
399         foundGroup.get().setName(jpaGroup.getName());
400         foundGroup.get().setDescription(jpaGroup.getDescription());
401         foundGroup.get().setMembers(jpaGroup.getMembers());
402         foundGroup.get().setRoles(roles);
403         em.merge(foundGroup.get());
404       }
405     });
406   }
407 
408   private void removeGroup(String groupId, String orgId) throws NotFoundException, UnauthorizedException {
409     Group group = loadGroup(groupId, orgId);
410     if (group != null && !UserDirectoryUtils.isCurrentUserAuthorizedHandleRoles(securityService, group.getRoles())) {
411       throw new UnauthorizedException("The user is not allowed to delete a group with the admin role");
412     }
413 
414     db.execTxChecked(UserDirectoryPersistenceUtil.removeGroupQuery(groupId, orgId));
415   }
416 
417   /**
418    * Returns all roles from a given group list
419    *
420    * @param groups
421    *          the group list
422    * @return the role list
423    */
424   private List<Role> getGroupsRoles(List<JpaGroup> groups) {
425     List<Role> roles = new ArrayList<>();
426     for (Group group : groups) {
427       roles.add(new JaxbRole(
428           group.getRole(),
429           JaxbOrganization.fromOrganization(group.getOrganization()),
430           "",
431           Role.Type.GROUP
432       ));
433       for (Role role : group.getRoles()) {
434         roles.add(new JaxbRole(role.getName(), role.getOrganizationId(), role.getDescription(), Role.Type.DERIVED));
435       }
436     }
437     return roles;
438   }
439 
440   public Iterator<Group> getGroups() {
441     String orgId = securityService.getOrganization().getId();
442     return new ArrayList<Group>(db.exec(UserDirectoryPersistenceUtil.findGroupsQuery(orgId, 0, 0)))
443         .iterator();
444   }
445 
446   private boolean like(final String str, final String expr) {
447     if (str == null) {
448       return false;
449     }
450     String regex = expr.replace("_", ".").replace("%", ".*?");
451     Pattern p = Pattern.compile(regex, Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
452     return p.matcher(str).matches();
453   }
454 
455   /**
456    * Returns a XML representation of the list of groups available the current user's organization.
457    *
458    * @param limit
459    *          the int amount to limit the results
460    * @param offset
461    *          the offset to start this result set at
462    * @return the JaxbGroupList of results
463    * @throws IOException
464    *           if unexpected IO exception occurs
465    */
466   public JaxbGroupList getGroups(int limit, int offset) throws IOException {
467     if (limit < 1) {
468       limit = 100;
469     }
470     String orgId = securityService.getOrganization().getId();
471     JaxbGroupList groupList = new JaxbGroupList();
472     List<JpaGroup> groups = db.exec(UserDirectoryPersistenceUtil.findGroupsQuery(orgId, limit, offset));
473     for (JpaGroup group : groups) {
474       groupList.add(group);
475     }
476     return groupList;
477   }
478 
479   /**
480    * Get groups by the defined filter and sorting criteria.
481    *
482    * @param limit
483    *          how many groups to get (optional)
484    * @param offset
485    *          where to start the list for pagination (optional)
486    * @param nameFilter
487    *          filter by group name (optional)
488    * @param textFilter
489    *          fulltext filter (optional)
490    * @param sortCriteria
491    *          the sorting criteria
492    *
493    * @return a list of groups
494    */
495   public List<JpaGroup> getGroups(Optional<Integer> limit, Optional<Integer> offset, Optional<String> nameFilter,
496           Optional<String> textFilter, ArrayList<SortCriterion> sortCriteria) {
497     String orgId = securityService.getOrganization().getId();
498     return db.exec(UserDirectoryPersistenceUtil.findGroupsQuery(orgId, limit, offset, nameFilter, textFilter,
499         sortCriteria));
500   }
501 
502   /**
503    * Count groups that fit the filter criteria in total.
504    *
505    * @param nameFilter
506    *          filter by group name (optional)
507    * @param textFilter
508    *          fulltext filter (optional)
509    *
510    * @return a list of groups
511    */
512   public long countTotalGroups(Optional<String> nameFilter, Optional<String> textFilter) {
513     String orgId = securityService.getOrganization().getId();
514     return db.exec(UserDirectoryPersistenceUtil.countTotalGroupsQuery(orgId, nameFilter, textFilter));
515   }
516 
517   /**
518    * Remove a group by id
519    *
520    * @param groupId
521    *          the id of the group to remove
522    * @throws Exception
523    *           unexpected error occurred
524    * @throws UnauthorizedException
525    *           user is not authorized to remove this group
526    * @throws NotFoundException
527    *           the group was not found
528    */
529   public void removeGroup(String groupId) throws NotFoundException, UnauthorizedException, Exception {
530     String orgId = securityService.getOrganization().getId();
531     removeGroup(groupId, orgId);
532   }
533 
534   /**
535    * Create a new group
536    *
537    * @param name
538    *          the name of the group
539    * @param description
540    *          a description of the group
541    * @param roles
542    *          the roles of the group
543    * @param users
544    *          the users in the group
545    * @throws IllegalArgumentException
546    *           if missing or bad parameters
547    * @throws UnauthorizedException
548    *           if user does not have rights to create group
549    * @throws ConflictException
550    *           if group already exists
551    */
552   public void createGroup(String name, String description, String roles, String users)
553           throws IllegalArgumentException, UnauthorizedException, ConflictException {
554     JpaOrganization organization = (JpaOrganization) securityService.getOrganization();
555 
556     HashSet<JpaRole> roleSet = new HashSet<>();
557     if (roles != null) {
558       for (String role : StringUtils.split(roles, ",")) {
559         roleSet.add(new JpaRole(StringUtils.trim(role), organization));
560       }
561     }
562 
563     HashSet<String> members = new HashSet<>();
564     if (users != null) {
565       for (String member : StringUtils.split(users, ",")) {
566         members.add(StringUtils.trim(member));
567       }
568     }
569 
570     final String groupId = name.toLowerCase().replaceAll("\\W", "_");
571 
572     Optional<JpaGroup> existingGroup = db.exec(UserDirectoryPersistenceUtil.findGroupQuery(groupId,
573         organization.getId()));
574     if (existingGroup.isPresent()) {
575       throw new ConflictException("group already exists");
576     }
577 
578     addGroup(new JpaGroup(groupId, organization, name, description, roleSet, members));
579   }
580 
581   /**
582    * Remove member from group.
583    *
584    * @param groupId
585    * @param member
586    *
587    * @return true if we updated the group, false otherwise
588    *
589    * @throws NotFoundException
590    * @throws UnauthorizedException
591    */
592   public boolean removeMemberFromGroup(String groupId, String member) throws NotFoundException, UnauthorizedException {
593     JpaGroup group = getGroup(groupId);
594     if (group == null) {
595       throw new NotFoundException();
596     }
597     Set<String> members = group.getMembers();
598     if (!members.contains(member)) {
599       return false; // nothing to do here
600     }
601     group.removeMember(member);
602     userDirectoryService.invalidate(member);
603 
604     addGroup(group);
605     return true;
606   }
607 
608   /**
609    * Add member to group.
610    *
611    * @param groupId
612    * @param member
613    *
614    * @return true if we updated the group, false otherwise
615    *
616    * @throws NotFoundException
617    * @throws UnauthorizedException
618    */
619   public boolean addMemberToGroup(String groupId, String member) throws NotFoundException, UnauthorizedException {
620     JpaGroup group = getGroup(groupId);
621     if (group == null) {
622       throw new NotFoundException();
623     }
624     Set<String> members = group.getMembers();
625     if (members.contains(member)) {
626       return false; // nothing to do here
627     }
628     group.addMember(member);
629     userDirectoryService.invalidate(member);
630 
631     addGroup(group);
632     return true;
633   }
634 
635   /**
636    * {@inheritDoc}
637    *
638    * @see org.opencastproject.userdirectory.api.GroupRoleProvider#updateGroup(String, String, String, String, String)
639    */
640   @Override
641   public void updateGroup(String groupId, String name, String description, String roles, String users)
642           throws NotFoundException, UnauthorizedException {
643     JpaOrganization organization = (JpaOrganization) securityService.getOrganization();
644 
645     Optional<JpaGroup> groupOpt = db.exec(UserDirectoryPersistenceUtil.findGroupQuery(groupId, organization.getId()));
646     if (groupOpt.isEmpty()) {
647       throw new NotFoundException();
648     }
649     JpaGroup group = groupOpt.get();
650 
651     if (StringUtils.isNotBlank(name)) {
652       group.setName(StringUtils.trim(name));
653     }
654 
655     if (StringUtils.isNotBlank(description)) {
656       group.setDescription(StringUtils.trim(description));
657     }
658 
659     if (StringUtils.isNotBlank(roles)) {
660       HashSet<JpaRole> roleSet = new HashSet<>();
661       for (String role : StringUtils.split(roles, ",")) {
662         roleSet.add(new JpaRole(StringUtils.trim(role), organization));
663       }
664       group.setRoles(roleSet);
665     } else {
666       group.setRoles(new HashSet<>());
667     }
668 
669     if (users != null) {
670       HashSet<String> members = new HashSet<>();
671       HashSet<String> invalidateUsers = new HashSet<>();
672 
673       Set<String> groupMembers = group.getMembers();
674 
675       for (String member : StringUtils.split(users, ",")) {
676         String newMember = StringUtils.trim(member);
677         members.add(newMember);
678         if (!groupMembers.contains(newMember)) {
679           invalidateUsers.add(newMember);
680         }
681       }
682 
683       for (String member : groupMembers) {
684         if (!members.contains(member)) {
685           invalidateUsers.add(member);
686         }
687       }
688 
689       group.setMembers(members);
690 
691       // Invalidate cache entries for users who have been added or removed
692       for (String member : invalidateUsers) {
693         userDirectoryService.invalidate(member);
694       }
695     }
696     addGroup(group);
697   }
698 }