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.kernel.security.CustomPasswordEncoder;
29  import org.opencastproject.security.api.Group;
30  import org.opencastproject.security.api.Role;
31  import org.opencastproject.security.api.RoleProvider;
32  import org.opencastproject.security.api.SecurityService;
33  import org.opencastproject.security.api.UnauthorizedException;
34  import org.opencastproject.security.api.User;
35  import org.opencastproject.security.api.UserProvider;
36  import org.opencastproject.security.impl.jpa.JpaOrganization;
37  import org.opencastproject.security.impl.jpa.JpaRole;
38  import org.opencastproject.security.impl.jpa.JpaUser;
39  import org.opencastproject.userdirectory.utils.UserDirectoryUtils;
40  import org.opencastproject.util.NotFoundException;
41  
42  import com.google.common.cache.CacheBuilder;
43  import com.google.common.cache.CacheLoader;
44  import com.google.common.cache.LoadingCache;
45  
46  import org.apache.commons.lang3.StringUtils;
47  import org.apache.commons.lang3.tuple.Pair;
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.util.ArrayList;
56  import java.util.Collection;
57  import java.util.Collections;
58  import java.util.HashSet;
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.stream.Collectors;
65  
66  import javax.persistence.EntityManagerFactory;
67  
68  /**
69   * Manages and locates users using JPA.
70   */
71  @Component(
72      property = {
73          "service.description=Provides a user directory"
74      },
75      immediate = true,
76      service = { UserProvider.class, RoleProvider.class, JpaUserAndRoleProvider.class }
77  )
78  public class JpaUserAndRoleProvider implements UserProvider, RoleProvider {
79  
80    /** The logger */
81    private static final Logger logger = LoggerFactory.getLogger(JpaUserAndRoleProvider.class);
82  
83    public static final String PERSISTENCE_UNIT = "org.opencastproject.common";
84  
85    /** The user provider name */
86    public static final String PROVIDER_NAME = "opencast";
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 delimiter for the User cache */
98    private static final String DELIMITER = ";==;";
99  
100   /** The security service */
101   protected SecurityService securityService = null;
102 
103   /** Group provider */
104   protected JpaGroupRoleProvider groupRoleProvider;
105 
106   /** A cache of users, which lightens the load on the SQL server */
107   private LoadingCache<String, Object> cache = null;
108 
109   /** A token to store in the miss cache */
110   protected Object nullToken = new Object();
111 
112   /** Password encoder for storing user passwords */
113   private CustomPasswordEncoder passwordEncoder = new CustomPasswordEncoder();
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   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 database", key[0], key[1]);
167         User user = loadUser(key[0], key[1]);
168         return user == null ? nullToken : user;
169       }
170     });
171 
172     db = dbSessionFactory.createSession(emf);
173   }
174 
175   /**
176    * {@inheritDoc}
177    *
178    * @see org.opencastproject.security.api.RoleProvider#getRolesForUser(String)
179    */
180   @Override
181   public List<Role> getRolesForUser(String userName) {
182     ArrayList<Role> roles = new ArrayList<>();
183     User user = loadUser(userName);
184     if (user == null) {
185       return roles;
186     }
187     roles.addAll(user.getRoles());
188     return roles;
189   }
190 
191   /**
192    * {@inheritDoc}
193    *
194    * @see org.opencastproject.security.api.UserProvider#findUsers(String, int, int)
195    */
196   @Override
197   public Iterator<User> findUsers(String query, int offset, int limit) {
198     if (query == null) {
199       throw new IllegalArgumentException("Query must be set");
200     }
201     String orgId = securityService.getOrganization().getId();
202     return db.exec(UserDirectoryPersistenceUtil.findUsersByQuery(orgId, query, limit, offset)).stream()
203         .map(JpaUserAndRoleProvider::addProviderName)
204         .collect(Collectors.toList())
205         .iterator();
206   }
207 
208   @Override
209   public Iterator<User> findUsers(Collection<String> userNames) {
210     String orgId = securityService.getOrganization().getId();
211     return db.exec(UserDirectoryPersistenceUtil.findUsersByUserNameQuery(userNames, orgId)).stream()
212         .map(JpaUserAndRoleProvider::addProviderName)
213         .collect(Collectors.toList())
214         .iterator();
215   }
216 
217   /**
218    * List all users with insecure password hashes
219    */
220   public List<User> findInsecurePasswordHashes() {
221     final String orgId = securityService.getOrganization().getId();
222     return db.exec(namedQuery.findAll(
223         "User.findInsecureHash",
224         User.class,
225         Pair.of("org", orgId)
226     ));
227   }
228 
229   /**
230    * {@inheritDoc}
231    *
232    * @see org.opencastproject.security.api.RoleProvider#findRoles(String, Role.Target, int, int)
233    */
234   @Override
235   public Iterator<Role> findRoles(String query, Role.Target target, int offset, int limit) {
236     if (query == null) {
237       throw new IllegalArgumentException("Query must be set");
238     }
239 
240     // This provider persists roles but is not authoritative for any roles, so return an empty set
241     return Collections.emptyIterator();
242   }
243 
244   /**
245    * {@inheritDoc}
246    *
247    * @see org.opencastproject.security.api.UserProvider#loadUser(java.lang.String)
248    */
249   @Override
250   public User loadUser(String userName) {
251     String orgId = securityService.getOrganization().getId();
252     Object user = cache.getUnchecked(userName.concat(DELIMITER).concat(orgId));
253     if (user == nullToken) {
254       return null;
255     } else {
256       return (User) user;
257     }
258   }
259 
260   @Override
261   public Iterator<User> getUsers() {
262     String orgId = securityService.getOrganization().getId();
263     return db.exec(UserDirectoryPersistenceUtil.findUsersQuery(orgId, 0, 0)).stream()
264         .map(JpaUserAndRoleProvider::addProviderName)
265         .collect(Collectors.toList())
266         .iterator();
267   }
268 
269   /**
270    * {@inheritDoc}
271    *
272    * @see org.opencastproject.security.api.UserProvider#getOrganization()
273    */
274   @Override
275   public String getOrganization() {
276     return ALL_ORGANIZATIONS;
277   }
278 
279   /**
280    * {@inheritDoc}
281    *
282    * @see java.lang.Object#toString()
283    */
284   @Override
285   public String toString() {
286     return getClass().getName();
287   }
288 
289   /**
290    * Loads a user from persistence
291    *
292    * @param userName
293    *          the user name
294    * @param organization
295    *          the organization id
296    * @return the loaded user or <code>null</code> if not found
297    */
298   public User loadUser(String userName, String organization) {
299     return db.exec(UserDirectoryPersistenceUtil.findUserQuery(userName, organization))
300         .map(JpaUserAndRoleProvider::addProviderName)
301         .orElse(null);
302   }
303 
304   /**
305    * Loads a user from persistence
306    *
307    * @param userId
308    *          the user's id
309    * @param organization
310    *          the organization id
311    * @return the loaded user or <code>null</code> if not found
312    */
313   public User loadUser(long userId, String organization) {
314     return db.exec(UserDirectoryPersistenceUtil.findUserQuery(userId, organization))
315         .map(JpaUserAndRoleProvider::addProviderName)
316         .orElse(null);
317   }
318 
319   /**
320    * Adds a user to the persistence
321    *
322    * @param user
323    *          the user to add
324    *
325    * @throws org.opencastproject.security.api.UnauthorizedException
326    *          if the user is not allowed to create other user with the given roles
327    */
328   public void addUser(JpaUser user) throws UnauthorizedException {
329     addUser(user, false);
330   }
331 
332   /**
333    * Adds a user to the persistence
334    *
335    * @param user
336    *          the user to add
337    * @param passwordEncoded
338    *          if the password is already encoded or should be encoded
339    *
340    * @throws org.opencastproject.security.api.UnauthorizedException
341    *          if the user is not allowed to create other user with the given roles
342    */
343   public void addUser(JpaUser user, final boolean passwordEncoded) throws UnauthorizedException {
344     if (!UserDirectoryUtils.isCurrentUserAuthorizedHandleRoles(securityService, user.getRoles())) {
345       throw new UnauthorizedException("The user is not allowed to set the admin role on other users");
346     }
347 
348     // Create a JPA user with an encoded password.
349     String encodedPassword = passwordEncoded
350         ? user.getPassword()
351         : passwordEncoder.encodePassword(user.getPassword());
352 
353     db.execTx(em -> {
354       // Only save internal roles
355       Set<JpaRole> roles = UserDirectoryPersistenceUtil.saveRolesQuery(filterRoles(user.getRoles())).apply(em);
356       JpaOrganization organization = UserDirectoryPersistenceUtil.saveOrganizationQuery(
357           (JpaOrganization) user.getOrganization()).apply(em);
358 
359       JpaUser newUser = new JpaUser(user.getUsername(), encodedPassword, organization, user.getName(), user.getEmail(),
360           user.getProvider(), user.isManageable(), roles);
361 
362       // Then save the user
363       em.persist(newUser);
364       cache.put(user.getUsername() + DELIMITER + user.getOrganization().getId(), newUser);
365     });
366     updateGroupMembership(user);
367   }
368 
369   /**
370    * Updates a user to the persistence
371    *
372    * @param user
373    *          the user to save
374    * @throws NotFoundException
375    * @throws org.opencastproject.security.api.UnauthorizedException
376    *          if the current user is not allowed to update user with the given roles
377    */
378   public User updateUser(JpaUser user) throws NotFoundException, UnauthorizedException {
379     return updateUser(user, false);
380   }
381 
382   /**
383    * Updates a user to the persistence
384    *
385    * @param user
386    *          the user to save
387    * @param passwordEncoded
388    *          if the password is already encoded or should be encoded
389    * @throws NotFoundException
390    * @throws org.opencastproject.security.api.UnauthorizedException
391    *          if the current user is not allowed to update user with the given roles
392    */
393   public User updateUser(JpaUser user, final boolean passwordEncoded) throws NotFoundException, UnauthorizedException {
394     if (!UserDirectoryUtils.isCurrentUserAuthorizedHandleRoles(securityService, user.getRoles())) {
395       throw new UnauthorizedException("The user is not allowed to set the admin role on other users");
396     }
397 
398     try {
399       return db.execTxChecked(em -> {
400         Optional<JpaUser> updateUser = UserDirectoryPersistenceUtil.findUserQuery(user.getUsername(),
401             user.getOrganization().getId()).apply(em);
402         if (updateUser.isEmpty()) {
403           throw new NotFoundException("User " + user.getUsername() + " not found.");
404         }
405 
406         logger.debug("updateUser({})", user.getUsername());
407 
408         if (!UserDirectoryUtils.isCurrentUserAuthorizedHandleRoles(securityService, updateUser.get().getRoles())) {
409           throw new UnauthorizedException("The user is not allowed to update an admin user");
410         }
411 
412         String encodedPassword;
413         //only update Password if a value is set
414         if (StringUtils.isEmpty(user.getPassword())) {
415           encodedPassword = updateUser.get().getPassword();
416         } else  {
417           // Update an JPA user with an encoded password.
418           if (passwordEncoded) {
419             encodedPassword = user.getPassword();
420           } else {
421             encodedPassword = passwordEncoder.encodePassword(user.getPassword());
422           }
423         }
424 
425         // Only save internal roles
426         Set<JpaRole> roles = UserDirectoryPersistenceUtil.saveRolesQuery(filterRoles(user.getRoles())).apply(em);
427         JpaOrganization organization = UserDirectoryPersistenceUtil.saveOrganizationQuery(
428             (JpaOrganization) user.getOrganization()).apply(em);
429 
430         JpaUser updatedUser = UserDirectoryPersistenceUtil.saveUserQuery(
431             new JpaUser(user.getUsername(), encodedPassword, organization, user.getName(), user.getEmail(), user
432                 .getProvider(), true, roles)).apply(em);
433         cache.put(user.getUsername() + DELIMITER + organization.getId(), updatedUser);
434 
435         updateGroupMembership(user);
436 
437         return updatedUser;
438       });
439     } catch (NotFoundException | UnauthorizedException | RuntimeException e) {
440       throw e;
441     } catch (Exception e) {
442       throw new IllegalStateException(e);
443     }
444   }
445 
446   /**
447    * Select only internal roles
448    *
449    * @param userRoles
450    *          the user's full set of roles
451    */
452   private Set<JpaRole> filterRoles(Set<Role> userRoles) {
453     Set<JpaRole> roles = new HashSet<>();
454     for (Role role : userRoles) {
455       if (Role.Type.INTERNAL.equals(role.getType()) && !role.getName().startsWith(Group.ROLE_PREFIX)) {
456         JpaRole jpaRole = (JpaRole) role;
457         roles.add(jpaRole);
458       }
459     }
460     return roles;
461   }
462 
463   /**
464    * Updates a user's groups based on assigned roles
465    *
466    * @param user
467    *          the user for whom groups should be updated
468    */
469   private void updateGroupMembership(JpaUser user) {
470     logger.debug("updateGroupMembership({}, roles={})", user.getUsername(), user.getRoles().size());
471     List<String> internalGroupRoles = new ArrayList<>();
472 
473     for (Role role : user.getRoles()) {
474       if (Role.Type.GROUP.equals(role.getType())
475           || (Role.Type.INTERNAL.equals(role.getType()) && role.getName().startsWith(Group.ROLE_PREFIX))) {
476         internalGroupRoles.add(role.getName());
477       }
478     }
479 
480     groupRoleProvider.updateGroupMembershipFromRoles(
481         user.getUsername(),
482         user.getOrganization().getId(),
483         internalGroupRoles
484     );
485   }
486 
487   /**
488    * Delete the given user
489    *
490    * @param username
491    *          the name of the user to delete
492    * @param orgId
493    *          the organization id
494    * @throws NotFoundException
495    *          if the requested user is not exist
496    * @throws org.opencastproject.security.api.UnauthorizedException
497    *          if you havn't permissions to delete an admin user (only admins may do that)
498    * @throws Exception
499    */
500   public void deleteUser(String username, String orgId) throws NotFoundException, UnauthorizedException, Exception {
501     User user = loadUser(username, orgId);
502     if (user != null && !UserDirectoryUtils.isCurrentUserAuthorizedHandleRoles(securityService, user.getRoles())) {
503       throw new UnauthorizedException("The user is not allowed to delete an admin user");
504     }
505 
506     // Remove the user's group membership
507     groupRoleProvider.removeMemberFromAllGroups(username, orgId);
508 
509     // Remove the user
510     db.execTxChecked(UserDirectoryPersistenceUtil.deleteUserQuery(username, orgId));
511 
512     cache.invalidate(username + DELIMITER + orgId);
513   }
514 
515   /**
516    * Adds a role to the persistence
517    *
518    * @param jpaRole
519    *          the role
520    */
521   public void addRole(JpaRole jpaRole) {
522     HashSet<JpaRole> roles = new HashSet<>();
523     roles.add(jpaRole);
524     db.execTx(UserDirectoryPersistenceUtil.saveRolesQuery(roles));
525   }
526 
527   @Override
528   public String getName() {
529     return PROVIDER_NAME;
530   }
531 
532   private static User addProviderName(JpaUser u) {
533     u.setProvider(PROVIDER_NAME);
534     return u;
535   }
536 
537   @Override
538   public long countUsers() {
539     String orgId = securityService.getOrganization().getId();
540     return db.exec(UserDirectoryPersistenceUtil.countUsersQuery(orgId));
541   }
542 
543   /**
544    * Returns the number of all users in the database
545    *
546    * @return the count of all users in the database
547    */
548   public long countAllUsers() {
549     return db.exec(UserDirectoryPersistenceUtil.countUsersQuery());
550   }
551 
552   @Override
553   public void invalidate(String userName) {
554     String orgId = securityService.getOrganization().getId();
555     cache.invalidate(userName + DELIMITER + orgId);
556   }
557 }