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 static org.opencastproject.db.Queries.namedQuery;
25  
26  import org.opencastproject.db.DBSession;
27  import org.opencastproject.db.DBSessionFactory;
28  import org.opencastproject.security.api.Group;
29  import org.opencastproject.security.api.Role;
30  import org.opencastproject.security.api.RoleProvider;
31  import org.opencastproject.security.api.SecurityService;
32  import org.opencastproject.security.api.UnauthorizedException;
33  import org.opencastproject.security.api.User;
34  import org.opencastproject.security.api.UserProvider;
35  import org.opencastproject.security.impl.jpa.JpaOrganization;
36  import org.opencastproject.security.impl.jpa.JpaRole;
37  import org.opencastproject.security.impl.jpa.JpaUserReference;
38  import org.opencastproject.userdirectory.api.AAIRoleProvider;
39  import org.opencastproject.userdirectory.api.UserReferenceProvider;
40  import org.opencastproject.userdirectory.utils.UserDirectoryUtils;
41  import org.opencastproject.util.NotFoundException;
42  import org.opencastproject.util.function.ThrowingConsumer;
43  
44  import com.google.common.cache.CacheBuilder;
45  import com.google.common.cache.CacheLoader;
46  import com.google.common.cache.LoadingCache;
47  
48  import org.apache.commons.lang3.tuple.Pair;
49  import org.osgi.service.component.ComponentContext;
50  import org.osgi.service.component.annotations.Activate;
51  import org.osgi.service.component.annotations.Component;
52  import org.osgi.service.component.annotations.Reference;
53  import org.slf4j.Logger;
54  import org.slf4j.LoggerFactory;
55  
56  import java.util.ArrayList;
57  import java.util.Collection;
58  import java.util.Collections;
59  import java.util.Iterator;
60  import java.util.List;
61  import java.util.Optional;
62  import java.util.Set;
63  import java.util.concurrent.TimeUnit;
64  import java.util.function.Function;
65  import java.util.stream.Collectors;
66  
67  import javax.persistence.EntityManager;
68  import javax.persistence.EntityManagerFactory;
69  import javax.persistence.TypedQuery;
70  
71  /**
72   * Manages and locates users references using JPA.
73   */
74  @Component(
75      property = {
76          "service.description=Provides a user reference directory"
77      },
78      immediate = true,
79      service = { UserProvider.class, RoleProvider.class, UserReferenceProvider.class, JpaUserReferenceProvider.class }
80  )
81  public class JpaUserReferenceProvider implements UserReferenceProvider, UserProvider, RoleProvider {
82  
83    /** The logger */
84    private static final Logger logger = LoggerFactory.getLogger(JpaUserReferenceProvider.class);
85  
86    public static final String PROVIDER_NAME = "matterhorn-reference";
87  
88    /** Username constant used in JSON formatted users */
89    public static final String USERNAME = "username";
90  
91    /** Role constant used in JSON formatted users */
92    public static final String ROLES = "roles";
93  
94    /** Encoding expected from all inputs */
95    public static final String ENCODING = "UTF-8";
96  
97    /** The security service */
98    protected SecurityService securityService = null;
99  
100   /** Group Role provider */
101   protected JpaGroupRoleProvider groupRoleProvider;
102 
103   /** Role provider */
104   protected AAIRoleProvider roleProvider;
105 
106   /** The delimiter for the User cache */
107   private static final String DELIMITER = ";==;";
108 
109   /** A cache of users, which lightens the load on the SQL server */
110   private LoadingCache<String, Object> cache = null;
111 
112   /** A token to store in the miss cache */
113   protected final Object nullToken = new Object();
114 
115   /** The factory used to generate the entity manager */
116   protected EntityManagerFactory emf = null;
117 
118   protected DBSessionFactory dbSessionFactory;
119 
120   protected DBSession db;
121 
122   /** OSGi DI */
123   @Reference(target = "(osgi.unit.name=org.opencastproject.common)")
124   void setEntityManagerFactory(EntityManagerFactory emf) {
125     this.emf = emf;
126   }
127 
128   @Reference
129   public void setDBSessionFactory(DBSessionFactory dbSessionFactory) {
130     this.dbSessionFactory = dbSessionFactory;
131   }
132 
133   /**
134    * @param securityService
135    *          the securityService to set
136    */
137   @Reference
138   public void setSecurityService(SecurityService securityService) {
139     this.securityService = securityService;
140   }
141 
142   /**
143    * @param groupRoleProvider
144    *          the GroupRoleProvider to set
145    */
146   @Reference
147   public void setGroupRoleProvider(JpaGroupRoleProvider groupRoleProvider) {
148     this.groupRoleProvider = groupRoleProvider;
149   }
150 
151   /**
152    * Callback for activation of this component.
153    *
154    * @param cc
155    *          the component context
156    */
157   @Activate
158   public void activate(ComponentContext cc) {
159     logger.debug("activate");
160 
161     // Setup the caches
162     cache = CacheBuilder.newBuilder().expireAfterWrite(1, TimeUnit.MINUTES).build(new CacheLoader<>() {
163       @Override
164       public Object load(String id) {
165         String[] key = id.split(DELIMITER);
166         logger.trace("Loading user '{}':'{}' from reference database", key[0], key[1]);
167         User user = loadUserFromDB(key[0], key[1]);
168         return user == null ? nullToken : user;
169       }
170     });
171 
172     // Set up persistence
173     db = dbSessionFactory.createSession(emf);
174   }
175 
176   @Override
177   public String getName() {
178     return PROVIDER_NAME;
179   }
180 
181   /**
182    * {@inheritDoc}
183    *
184    * @see java.lang.Object#toString()
185    */
186   @Override
187   public String toString() {
188     return getClass().getName();
189   }
190 
191   /**
192    * {@inheritDoc}
193    *
194    * @see org.opencastproject.security.api.RoleProvider#getRolesForUser(String)
195    */
196   @Override
197   public List<Role> getRolesForUser(String userName) {
198     if (roleProvider != null) {
199       return roleProvider.getRolesForUser(userName);
200     }
201 
202     ArrayList<Role> roles = new ArrayList<>();
203     User user = loadUser(userName);
204     if (user != null) {
205       roles.addAll(user.getRoles());
206     }
207     return roles;
208   }
209 
210   /**
211    * {@inheritDoc}
212    *
213    * @see org.opencastproject.security.api.UserProvider#findUsers(String, int, int)
214    */
215   @Override
216   public Iterator<User> findUsers(String query, int offset, int limit) {
217     if (query == null) {
218       throw new IllegalArgumentException("Query must be set");
219     }
220     String orgId = securityService.getOrganization().getId();
221     return db.exec(findUserReferencesByQueryQuery(orgId, query, limit, offset)).stream()
222         .map(ref -> ref.toUser(PROVIDER_NAME))
223         .collect(Collectors.toList())
224         .iterator();
225   }
226 
227   @Override
228   public Iterator<User> findUsers(Collection<String> userNames) {
229     String orgId = securityService.getOrganization().getId();
230     return db.exec(findUsersByUserNameQuery(orgId, userNames)).stream()
231         .map(ref -> ref.toUser(PROVIDER_NAME))
232         .collect(Collectors.toList())
233         .iterator();
234   }
235 
236   /**
237    * {@inheritDoc}
238    *
239    * @see org.opencastproject.security.api.RoleProvider#findRoles(String, Role.Target, int, int)
240    */
241   @Override
242   public Iterator<Role> findRoles(String query, Role.Target target, int offset, int limit) {
243     if (roleProvider == null) {
244       return Collections.emptyIterator();
245     }
246     return roleProvider.findRoles(query, target, offset, limit);
247   }
248 
249   /**
250    * {@inheritDoc}
251    *
252    * @see org.opencastproject.security.api.UserProvider#loadUser(java.lang.String)
253    */
254   @Override
255   public User loadUser(String userName) {
256     String orgId = securityService.getOrganization().getId();
257     return loadUserFromCache(userName, orgId);
258   }
259 
260   /**
261    * Loads a user from persistence
262    *
263    * @param userName
264    *          the user name
265    * @param organization
266    *          the organization id
267    * @return the loaded user or <code>null</code> if not found
268    */
269   private User loadUserFromDB(String userName, String organization) {
270     return db.exec(findUserReferenceQuery(userName, organization))
271         .map(ref -> ref.toUser(PROVIDER_NAME))
272         .orElse(null);
273   }
274 
275   /**
276    * Loads a user from cache
277    *
278    * @param userName
279    *          the user name
280    * @param organization
281    *          the organization id
282    * @return the loaded user or <code>null</code> if not found
283    */
284   private User loadUserFromCache(String userName, String organization) {
285     Object user = cache.getUnchecked(userName.concat(DELIMITER).concat(organization));
286     if (user == nullToken) {
287       return null;
288     } else {
289       return (User) user;
290     }
291   }
292 
293   @Override
294   public Iterator<User> getUsers() {
295     String orgId = securityService.getOrganization().getId();
296     return db.exec(findUserReferences(orgId, 0, 0)).stream()
297         .map(ref -> ref.toUser(PROVIDER_NAME))
298         .collect(Collectors.toList())
299         .iterator();
300   }
301 
302   /**
303    * Return the roles
304    *
305    * @return the roles
306    */
307   public Iterator<Role> getRoles() {
308     if (roleProvider == null) {
309       return Collections.emptyIterator();
310     }
311     return roleProvider.getRoles();
312   }
313 
314   /**
315    * {@inheritDoc}
316    *
317    * @see org.opencastproject.security.api.UserProvider#getOrganization()
318    */
319   @Override
320   public String getOrganization() {
321     return ALL_ORGANIZATIONS;
322   }
323 
324   /**
325    * {@inheritDoc}
326    */
327   public void addUserReference(JpaUserReference user, String mechanism) {
328     db.execTx(em -> {
329       // Create a JPA user with an encoded password.
330       Set<JpaRole> roles = UserDirectoryPersistenceUtil.saveRolesQuery(user.getRoles()).apply(em);
331       JpaOrganization organization = UserDirectoryPersistenceUtil.saveOrganizationQuery(
332           (JpaOrganization) user.getOrganization()).apply(em);
333       JpaUserReference userReference = new JpaUserReference(user.getUsername(), user.getName(), user.getEmail(),
334           mechanism, user.getLastLogin(), organization, roles);
335 
336       // Then save the user reference
337       Optional<JpaUserReference> foundUserRef = findUserReferenceQuery(user.getUsername(),
338           user.getOrganization().getId()).apply(em);
339       if (foundUserRef.isPresent()) {
340         throw new IllegalStateException("User '" + user.getUsername() + "' already exists");
341       }
342       em.persist(userReference);
343     });
344     // There is still a race when this method is executed multiple times. However, the user reference is unlikely to be
345     // different.
346     cache.put(user.getUsername() + DELIMITER + user.getOrganization().getId(), user.toUser(PROVIDER_NAME));
347     updateGroupMembership(user);
348   }
349 
350   /**
351    * {@inheritDoc}
352    */
353   public void updateUserReference(JpaUserReference user) {
354     db.execTx(em -> {
355       Optional<JpaUserReference> foundUserRef = findUserReferenceQuery(user.getUsername(),
356           user.getOrganization().getId()).apply(em);
357       if (foundUserRef.isEmpty()) {
358         throw new IllegalStateException("User '" + user.getUsername() + "' does not exist");
359       }
360       foundUserRef.get().setName(user.getName());
361       foundUserRef.get().setEmail(user.getEmail());
362       foundUserRef.get().setLastLogin(user.getLastLogin());
363       foundUserRef.get().setRoles(UserDirectoryPersistenceUtil.saveRolesQuery(user.getRoles()).apply(em));
364       em.merge(foundUserRef.get());
365     });
366     // There is still a race when this method is executed multiple times. However, the user reference is unlikely to be
367     // different.
368     cache.put(user.getUsername() + DELIMITER + user.getOrganization().getId(), user.toUser(PROVIDER_NAME));
369     updateGroupMembership(user);
370   }
371 
372   /**
373    * Updates a user's groups based on assigned roles
374    *
375    * @param user
376    *          the user for whom groups should be updated
377    */
378   private void updateGroupMembership(JpaUserReference user) {
379     logger.debug("updateGroupMembership({}, roles={})", user.getUsername(), user.getRoles().size());
380     List<String> internalGroupRoles = new ArrayList<>();
381 
382     for (Role role : user.getRoles()) {
383       if (Role.Type.GROUP.equals(role.getType())
384           || (Role.Type.INTERNAL.equals(role.getType()) && role.getName().startsWith(Group.ROLE_PREFIX))) {
385         internalGroupRoles.add(role.getName());
386       }
387     }
388 
389     groupRoleProvider.updateGroupMembershipFromRoles(
390         user.getUsername(),
391         user.getOrganization().getId(),
392         internalGroupRoles
393     );
394   }
395 
396   /**
397    * Returns the persisted user reference by the user name and organization id
398    *
399    * @param userName
400    *          the user name
401    * @param organizationId
402    *          the organization id
403    * @return the user or <code>null</code> if not found
404    */
405   public JpaUserReference findUserReference(String userName, String organizationId) {
406     return db.exec(findUserReferenceQuery(userName, organizationId))
407         .orElse(null);
408   }
409 
410   /**
411    * Returns the persisted user reference by the user name and organization id
412    *
413    * @param userName
414    *          the user name
415    * @param organizationId
416    *          the organization id
417    * @return the user or <code>null</code> if not found
418    */
419   private Function<EntityManager, Optional<JpaUserReference>> findUserReferenceQuery(String userName,
420       String organizationId) {
421     return namedQuery.findOpt(
422         "UserReference.findByUsername",
423         JpaUserReference.class,
424         Pair.of("u", userName),
425         Pair.of("org", organizationId)
426     );
427   }
428 
429   /**
430    * Returns a list of user references by a search query if set or all user references if search query is
431    * <code>null</code>
432    *
433    * @param orgId
434    *          the organization identifier
435    * @param query
436    *          the query to search
437    * @param limit
438    *          the limit
439    * @param offset
440    *          the offset
441    * @return the user references list
442    */
443   private Function<EntityManager, List<JpaUserReference>> findUserReferencesByQueryQuery(String orgId, String query,
444       int limit, int offset) {
445     return em -> {
446       TypedQuery<JpaUserReference> q = em.createNamedQuery("UserReference.findByQuery", JpaUserReference.class)
447           .setMaxResults(limit)
448           .setFirstResult(offset);
449       q.setParameter("query", query.toUpperCase());
450       q.setParameter("org", orgId);
451       return q.getResultList();
452     };
453   }
454 
455   /**
456    * Returns user references for specific user names (and an organization)
457    * @param orgId The organization to search for
458    * @param names The names to search for
459    * @return the user references list
460    */
461   private Function<EntityManager, List<JpaUserReference>> findUsersByUserNameQuery(String orgId,
462       Collection<String> names) {
463     return em -> {
464       if (names.isEmpty()) {
465         return Collections.emptyList();
466       }
467       TypedQuery<JpaUserReference> q = em.createNamedQuery("UserReference.findAllByUserNames", JpaUserReference.class);
468       q.setParameter("org", orgId);
469       q.setParameter("names", names);
470       return q.getResultList();
471     };
472   }
473 
474   /**
475    * Returns all user references
476    *
477    * @param orgId
478    *          the organization identifier
479    * @param limit
480    *          the limit
481    * @param offset
482    *          the offset
483    * @return the user references list
484    */
485   private Function<EntityManager, List<JpaUserReference>> findUserReferences(String orgId, int limit, int offset) {
486     return em -> {
487       TypedQuery<JpaUserReference> q = em.createNamedQuery("UserReference.findAll", JpaUserReference.class)
488           .setMaxResults(limit)
489           .setFirstResult(offset);
490       q.setParameter("org", orgId);
491       return q.getResultList();
492     };
493   }
494 
495   @Override
496   public long countUsers() {
497     String orgId = securityService.getOrganization().getId();
498     return db.exec(namedQuery.find(
499         "UserReference.countAll",
500         Number.class,
501         Pair.of("org", orgId)
502     )).longValue();
503   }
504 
505   @Override
506   public void invalidate(String userName) {
507     String orgId = securityService.getOrganization().getId();
508     cache.invalidate(userName.concat(DELIMITER).concat(orgId));
509   }
510 
511   /**
512    * Delete the given user
513    *
514    * @param username
515    *          the name of the user to delete
516    * @param orgId
517    *          the organization id
518    * @throws NotFoundException
519    *          if the requested user is not exist
520    * @throws org.opencastproject.security.api.UnauthorizedException
521    *          if you havn't permissions to delete an admin user (only admins may do that)
522    * @throws Exception
523    */
524   public void deleteUser(String username, String orgId) throws NotFoundException, UnauthorizedException, Exception {
525     User user = loadUser(username);
526     if (user != null && !UserDirectoryUtils.isCurrentUserAuthorizedHandleRoles(securityService, user.getRoles())) {
527       throw new UnauthorizedException("The user is not allowed to delete an admin user");
528     }
529 
530     // Remove the user's group membership
531     groupRoleProvider.removeMemberFromAllGroups(username, orgId);
532 
533     // Remove the user
534     db.execTxChecked(deleteUserQuery(username, orgId));
535 
536     cache.invalidate(username + DELIMITER + orgId);
537   }
538 
539   private ThrowingConsumer<EntityManager, NotFoundException> deleteUserQuery(String username, String orgId) {
540     return em -> {
541       Optional<JpaUserReference> user = findUserReferenceQuery(username, orgId).apply(em);
542       if (user.isEmpty()) {
543         throw new NotFoundException("User with name " + username + " does not exist");
544       }
545       em.remove(em.merge(user.get()));
546     };
547   }
548 
549   public void setRoleProvider(RoleProvider roleProvider) {
550     this.roleProvider = (AAIRoleProvider) roleProvider;
551   }
552 }