View Javadoc
1   /*
2    * This file is part of ***  M y C o R e  ***
3    * See http://www.mycore.de/ for details.
4    *
5    * MyCoRe is free software: you can redistribute it and/or modify
6    * it under the terms of the GNU General Public License as published by
7    * the Free Software Foundation, either version 3 of the License, or
8    * (at your option) any later version.
9    *
10   * MyCoRe is distributed in the hope that it will be useful,
11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   * GNU General Public License for more details.
14   *
15   * You should have received a copy of the GNU General Public License
16   * along with MyCoRe.  If not, see <http://www.gnu.org/licenses/>.
17   */
18  
19  package org.mycore.user2;
20  
21  import java.security.NoSuchAlgorithmException;
22  import java.security.SecureRandom;
23  import java.util.ArrayList;
24  import java.util.Base64;
25  import java.util.Collection;
26  import java.util.Collections;
27  import java.util.List;
28  import java.util.Optional;
29  import java.util.stream.Stream;
30  
31  import org.apache.commons.lang3.StringUtils;
32  import org.apache.logging.log4j.LogManager;
33  import org.apache.logging.log4j.Logger;
34  import org.mycore.access.MCRAccessManager;
35  import org.mycore.backend.jpa.MCREntityManagerProvider;
36  import org.mycore.common.MCRException;
37  import org.mycore.common.MCRSession;
38  import org.mycore.common.MCRSessionMgr;
39  import org.mycore.common.MCRSystemUserInformation;
40  import org.mycore.common.MCRUserInformation;
41  import org.mycore.common.MCRUtils;
42  import org.mycore.common.config.MCRConfiguration2;
43  import org.mycore.common.events.MCREvent;
44  import org.mycore.common.events.MCREventManager;
45  import org.mycore.common.xml.MCRXMLFunctions;
46  import org.mycore.datamodel.classifications2.MCRCategoryID;
47  import org.mycore.datamodel.common.MCRISO8601Format;
48  
49  import jakarta.persistence.EntityManager;
50  import jakarta.persistence.NoResultException;
51  import jakarta.persistence.TypedQuery;
52  import jakarta.persistence.criteria.CriteriaBuilder;
53  import jakarta.persistence.criteria.CriteriaQuery;
54  import jakarta.persistence.criteria.Join;
55  import jakarta.persistence.criteria.JoinType;
56  import jakarta.persistence.criteria.Predicate;
57  import jakarta.persistence.criteria.Root;
58  import jakarta.persistence.metamodel.SingularAttribute;
59  
60  /**
61   * Manages all users using a database table.
62   *
63   * @author Frank L\u00fctzenkirchen
64   * @author Thomas Scheffler (yagee)
65   */
66  public class MCRUserManager {
67  
68      private static final int HASH_ITERATIONS = MCRConfiguration2
69          .getInt(MCRUser2Constants.CONFIG_PREFIX + "HashIterations").orElse(1000);
70  
71      private static final Logger LOGGER = LogManager.getLogger();
72  
73      private static final SecureRandom SECURE_RANDOM;
74  
75      static {
76          try {
77              SECURE_RANDOM = SecureRandom.getInstance("SHA1PRNG");
78          } catch (NoSuchAlgorithmException e) {
79              throw new MCRException("Could not initialize secure SECURE_RANDOM number", e);
80          }
81      }
82  
83      /** The table that stores login user information */
84      static String table;
85  
86      /**
87       * Returns the user with the given userName, in the default realm
88       *
89       * @param userName the unique userName within the default realm
90       * @return the user with the given login name, or null
91       */
92      public static MCRUser getUser(String userName) {
93          if (!userName.contains("@")) {
94              return getUser(userName, MCRRealmFactory.getLocalRealm());
95          } else {
96              String[] parts = userName.split("@");
97              if (parts.length == 2) {
98                  return getUser(parts[0], parts[1]);
99              } else {
100                 return null;
101             }
102         }
103     }
104 
105     /**
106      * Returns the user with the given userName, in the given realm
107      *
108      * @param userName the unique userName within the given realm
109      * @param realm the realm the user belongs to
110      * @return the user with the given login name, or null
111      */
112     public static MCRUser getUser(String userName, MCRRealm realm) {
113         return getUser(userName, realm.getID());
114     }
115 
116     /**
117      * Returns the user with the given userName, in the given realm
118      *
119      * @param userName the unique userName within the given realm
120      * @param realmId the ID of the realm the user belongs to
121      * @return the user with the given login name, or null
122      */
123     public static MCRUser getUser(String userName, String realmId) {
124         EntityManager em = MCREntityManagerProvider.getCurrentEntityManager();
125         return getByNaturalID(em, userName, realmId)
126             .map(MCRUserManager::setRoles)
127             .orElseGet(() -> {
128                 LOGGER.warn("Could not find requested user: {}@{}", userName, realmId);
129                 return null;
130             });
131     }
132 
133     /**
134      * Returns a Stream of users where the user has a given attribute.
135      * @param attrName name of the user attribute
136      * @param attrValue value of the user attribute
137      */
138     public static Stream<MCRUser> getUsers(String attrName, String attrValue) {
139         EntityManager em = MCREntityManagerProvider.getCurrentEntityManager();
140         TypedQuery<MCRUser> propertyQuery = em.createNamedQuery("MCRUser.byPropertyValue", MCRUser.class);
141         propertyQuery.setParameter("name", attrName);
142         propertyQuery.setParameter("value", attrValue);
143         MCRUserAttribute attr = new MCRUserAttribute(attrName, attrValue);
144         return propertyQuery.getResultList()
145             .stream()
146             .filter(u -> u.getAttributes().contains(attr))
147             .peek(em::refresh); //fixes MCR-1885
148     }
149 
150     private static MCRUser setRoles(MCRUser mcrUser) {
151         Collection<MCRCategoryID> roleIDs = MCRRoleManager.getRoleIDs(mcrUser);
152         mcrUser.getSystemRoleIDs().clear();
153         mcrUser.getExternalRoleIDs().clear();
154         for (MCRCategoryID roleID : roleIDs) {
155             if (roleID.getRootID().equals(MCRUser2Constants.ROLE_CLASSID.getRootID())) {
156                 mcrUser.getSystemRoleIDs().add(roleID.getID());
157             } else {
158                 mcrUser.getExternalRoleIDs().add(roleID.toString());
159             }
160         }
161         return mcrUser;
162     }
163 
164     /**
165      * Checks if a user with the given login name exists in the default realm.
166      *
167      * @param userName the login user name.
168      * @return true, if a user with the given login name exists.
169      */
170     public static boolean exists(String userName) {
171         return exists(userName, MCRRealmFactory.getLocalRealm());
172     }
173 
174     /**
175      * Checks if a user with the given login name exists in the given realm.
176      *
177      * @param userName the login user name.
178      * @param realm the realm the user belongs to
179      * @return true, if a user with the given login name exists.
180      */
181     public static boolean exists(String userName, MCRRealm realm) {
182         return exists(userName, realm.getID());
183     }
184 
185     /**
186      * Checks if a user with the given login name exists in the given realm.
187      *
188      * @param userName the login user name.
189      * @param realm the ID of the realm the user belongs to
190      * @return true, if a user with the given login name exists.
191      */
192     public static boolean exists(String userName, String realm) {
193         EntityManager em = MCREntityManagerProvider.getCurrentEntityManager();
194         CriteriaBuilder cb = em.getCriteriaBuilder();
195         CriteriaQuery<Number> query = cb.createQuery(Number.class);
196         Root<MCRUser> users = query.from(MCRUser.class);
197         return em.createQuery(
198             query
199                 .select(cb.count(users))
200                 .where(getUserRealmCriterion(cb, users, userName, realm)))
201             .getSingleResult().intValue() > 0;
202     }
203 
204     /**
205      * Creates and stores a new login user in the database.
206      * This will also store role membership information.
207      *
208      * @param user the user to create in the database.
209      */
210     public static void createUser(MCRUser user) {
211         if (isInvalidUser(user)) {
212             throw new MCRException("User is invalid: " + user.getUserID());
213         }
214 
215         if (user instanceof MCRTransientUser) {
216             createUser((MCRTransientUser) user);
217             return;
218         }
219 
220         EntityManager em = MCREntityManagerProvider.getCurrentEntityManager();
221         em.persist(user);
222         LOGGER.info(() -> "user saved: " + user.getUserID());
223         MCRRoleManager.storeRoleAssignments(user);
224         MCREvent evt = new MCREvent(MCREvent.ObjectType.USER, MCREvent.EventType.CREATE);
225         evt.put(MCREvent.USER_KEY, user);
226         MCREventManager.instance().handleEvent(evt);
227     }
228 
229     /**
230      * Creates and store a new login user in the database, do also attribute mapping is needed.
231      * This will also store role membership information.
232      *
233      * @param user the user to create in the database.
234      */
235     public static void createUser(MCRTransientUser user) {
236         if (isInvalidUser(user)) {
237             throw new MCRException("User is invalid: " + user.getUserID());
238         }
239 
240         createUser(user.clone());
241     }
242 
243     /**
244      * Checks whether the user is invalid.
245      *
246      * MCRUser is not allowed to overwrite information returned by {@link MCRSystemUserInformation#getGuestInstance()}
247      * or {@link MCRSystemUserInformation#getSystemUserInstance()}.
248      * @return true if {@link #createUser(MCRUser)} or {@link #updateUser(MCRUser)} would reject the given user
249      */
250     public static boolean isInvalidUser(MCRUser user) {
251         if (MCRSystemUserInformation.getGuestInstance().getUserID().equals(user.getUserID())) {
252             return true;
253         }
254         return MCRSystemUserInformation.getSystemUserInstance().getUserID().equals(user.getUserID());
255     }
256 
257     /**
258      * Updates an existing login user in the database.
259      * This will also update role membership information.
260      *
261      * @param user the user to update in the database.
262      */
263     public static void updateUser(MCRUser user) {
264         if (isInvalidUser(user)) {
265             throw new MCRException("User is invalid: " + user.getUserID());
266         }
267         EntityManager em = MCREntityManagerProvider.getCurrentEntityManager();
268         Optional<MCRUser> inDb = getByNaturalID(em, user.getUserName(), user.getRealmID());
269         if (!inDb.isPresent()) {
270             createUser(user);
271             return;
272         }
273         inDb.ifPresent(db -> {
274             user.internalID = db.internalID;
275             em.detach(db);
276             em.merge(user);
277             MCRRoleManager.unassignRoles(user);
278             MCRRoleManager.storeRoleAssignments(user);
279             MCREvent evt = new MCREvent(MCREvent.ObjectType.USER, MCREvent.EventType.UPDATE);
280             evt.put(MCREvent.USER_KEY, user);
281             MCREventManager.instance().handleEvent(evt);
282         });
283     }
284 
285     /**
286      * Deletes a user from the given database
287      *
288      * @param userName the login name of the user to delete, in the default realm.
289      */
290     public static void deleteUser(String userName) {
291         if (!userName.contains("@")) {
292             deleteUser(userName, MCRRealmFactory.getLocalRealm());
293         } else {
294             String[] parts = userName.split("@");
295             deleteUser(parts[0], parts[1]);
296         }
297     }
298 
299     /**
300      * Deletes a user from the given database
301      *
302      * @param userName the login name of the user to delete, in the given realm.
303      * @param realm the realm the user belongs to
304      */
305     public static void deleteUser(String userName, MCRRealm realm) {
306         deleteUser(userName, realm.getID());
307     }
308 
309     /**
310      * Deletes a user from the given database
311      *
312      * @param userName the login name of the user to delete, in the given realm.
313      * @param realmId the ID of the realm the user belongs to
314      */
315     public static void deleteUser(String userName, String realmId) {
316         MCRUser user = getUser(userName, realmId);
317         MCREvent evt = new MCREvent(MCREvent.ObjectType.USER, MCREvent.EventType.DELETE);
318         evt.put(MCREvent.USER_KEY, user);
319         MCREventManager.instance().handleEvent(evt);
320         MCRRoleManager.unassignRoles(user);
321         EntityManager em = MCREntityManagerProvider.getCurrentEntityManager();
322         em.remove(user);
323     }
324 
325     /**
326      * Deletes a user from the given database
327      *
328      * @param user the user to delete
329      */
330     public static void deleteUser(MCRUser user) {
331         deleteUser(user.getUserName(), user.getRealmID());
332     }
333 
334     /**
335      * Returns a list of all users the given user is owner of.
336      *
337      * @param owner the user that owns other users
338      */
339     public static List<MCRUser> listUsers(MCRUser owner) {
340         EntityManager em = MCREntityManagerProvider.getCurrentEntityManager();
341         CriteriaBuilder cb = em.getCriteriaBuilder();
342         CriteriaQuery<MCRUser> query = cb.createQuery(MCRUser.class);
343         Root<MCRUser> users = query.from(MCRUser.class);
344         users.fetch(MCRUser_.owner);
345         return em.createQuery(
346             query
347                 .distinct(true)
348                 .where(cb.equal(users.get(MCRUser_.owner), owner)))
349             .getResultList();
350     }
351 
352     private static String buildSearchPattern(String searchPattern) {
353         searchPattern = searchPattern.replace('*', '%');
354         searchPattern = searchPattern.replace('?', '_');
355         return searchPattern.toLowerCase(MCRSessionMgr.getCurrentSession().getLocale());
356     }
357 
358     private static boolean isValidSearchPattern(String searchPattern) {
359         return StringUtils.isNotEmpty(searchPattern);
360     }
361 
362     private static Predicate[] buildCondition(CriteriaBuilder cb, Root<MCRUser> root, String userPattern, String realm,
363         String namePattern, String mailPattern, String attributeNamePattern) {
364 
365         ArrayList<Predicate> predicates = new ArrayList<>(2);
366         addEqualsPredicate(cb, root, MCRUser_.realmID, realm, predicates);
367 
368         ArrayList<Predicate> searchPredicates = new ArrayList<>(3);
369         addSearchPredicate(cb, root, MCRUser_.userName, userPattern, searchPredicates);
370         addSearchPredicate(cb, root, MCRUser_.realName, namePattern, searchPredicates);
371         addSearchPredicate(cb, root, MCRUser_.EMail, mailPattern, searchPredicates);
372 
373         if (isValidSearchPattern(attributeNamePattern)) {
374             Join<MCRUser, MCRUserAttribute> userAttributeJoin = root.join(MCRUser_.attributes);
375             searchPredicates.add(cb.like(userAttributeJoin.get(MCRUserAttribute_.name),
376                 buildSearchPattern(attributeNamePattern)));
377         }
378 
379         if (!searchPredicates.isEmpty()) {
380             if (1 == searchPredicates.size()) {
381                 predicates.add(searchPredicates.get(0));
382             } else {
383                 predicates.add(cb.or(searchPredicates.toArray(new Predicate[searchPredicates.size()])));
384             }
385         }
386 
387         return predicates.toArray(new Predicate[predicates.size()]);
388     }
389 
390     private static void addEqualsPredicate(CriteriaBuilder cb, Root<MCRUser> root,
391         SingularAttribute<MCRUser, String> attribute, String string, ArrayList<Predicate> predicates) {
392         if (isValidSearchPattern(string)) {
393             predicates.add(cb.equal(root.get(attribute), string));
394         }
395     }
396 
397     private static void addSearchPredicate(CriteriaBuilder cb, Root<MCRUser> root,
398         SingularAttribute<MCRUser, String> attribute, String searchPattern, ArrayList<Predicate> predicates) {
399         if (isValidSearchPattern(searchPattern)) {
400             predicates.add(buildSearchPredicate(cb, root, attribute, searchPattern));
401         }
402     }
403 
404     private static Predicate buildSearchPredicate(CriteriaBuilder cb, Root<MCRUser> root,
405         SingularAttribute<MCRUser, String> attribute, String searchPattern) {
406         return cb.like(cb.lower(root.get(attribute)), buildSearchPattern(searchPattern));
407     }
408 
409     /**
410      * Searches for users in the database and returns a list of matching users.
411      * Wildcards containing * and ? for single character may be used for searching
412      * by login user name or real name.
413      *
414      * Pay attention that no role information is attached to user data. If you need
415      * this information call {@link MCRUserManager#getUser(String, String)}.
416      *
417      * @param userPattern a wildcard pattern for the login user name, may be null
418      * @param realm the realm the user belongs to, may be null
419      * @param namePattern a wildcard pattern for the person's real name, may be null
420      * @return a list of all matching users
421      * @deprecated Use {@link MCRUserManager#listUsers(String, String, String, String)} instead.
422      */
423     @Deprecated
424     public static List<MCRUser> listUsers(String userPattern, String realm, String namePattern) {
425         return listUsers(userPattern, realm, namePattern, null);
426     }
427 
428     /**
429      * Searches for users in the database and returns a list of all matching users.
430      * Wildcards containing * and ? for single character may be used for searching
431      * by login user name or real name.
432      *
433      * Pay attention that no role information is attached to user data. If you need
434      * this information call {@link MCRUserManager#getUser(String, String)}.
435      *
436      * @param userPattern a wildcard pattern for the login user name, may be null
437      * @param realm the realm the user belongs to, may be null
438      * @param namePattern a wildcard pattern for the person's real name, may be null
439      * @param mailPattern a wildcard pattern for the person's email, may be null
440      * @return a list of all matching users
441      */
442     public static List<MCRUser> listUsers(String userPattern, String realm, String namePattern, String mailPattern) {
443         return listUsers(userPattern, realm, namePattern, mailPattern, null, 0, Integer.MAX_VALUE);
444     }
445 
446     /**
447      * Searches for users in the database and returns a list of matching users.
448      * Wildcards containing * and ? for single character may be used for searching
449      * by login user name or real name.
450      *
451      * Pay attention that no role information is attached to user data. If you need
452      * this information call {@link MCRUserManager#getUser(String, String)}.
453      *
454      * @param userPattern a wildcard pattern for the login user name, may be null
455      * @param realm the realm the user belongs to, may be null
456      * @param namePattern a wildcard pattern for the person's real name, may be null
457      * @param mailPattern a wildcard pattern for the person's email, may be null
458      * @param attributeNamePattern a wildcard pattern for person's attribute names, may be null
459      * @param offset an offset for matching users
460      * @param limit a limit for matching users
461      * @return a list of matching users in offset and limit range
462      */
463     public static List<MCRUser> listUsers(String userPattern, String realm, String namePattern, String mailPattern,
464         String attributeNamePattern, int offset, int limit) {
465         EntityManager em = MCREntityManagerProvider.getCurrentEntityManager();
466         CriteriaBuilder cb = em.getCriteriaBuilder();
467         CriteriaQuery<MCRUser> query = cb.createQuery(MCRUser.class);
468         Root<MCRUser> user = query.from(MCRUser.class);
469         return em
470             .createQuery(
471                 query
472                     .where(
473                         buildCondition(cb, user, userPattern, realm, namePattern, mailPattern, attributeNamePattern)))
474             .setFirstResult(offset)
475             .setMaxResults(limit)
476             .getResultList();
477     }
478 
479     /**
480      * Counts users in the database that match the given criteria.
481      * Wildcards containing * and ? for single character may be used for searching
482      * by login user name or real name.
483      *
484      * @param userPattern a wildcard pattern for the login user name, may be null
485      * @param realm the realm the user belongs to, may be null
486      * @param namePattern a wildcard pattern for the person's real name, may be null
487      * @return the number of matching users
488      * @deprecated Use {@link MCRUserManager#countUsers(String, String, String, String)} instead.
489      */
490     @Deprecated
491     public static int countUsers(String userPattern, String realm, String namePattern) {
492         return countUsers(userPattern, realm, namePattern, null);
493     }
494 
495     /**
496      * Counts users in the database that match the given criteria.
497      * Wildcards containing * and ? for single character may be used for searching
498      * by login user name or real name.
499      *
500      * @param userPattern a wildcard pattern for the login user name, may be null
501      * @param realm the realm the user belongs to, may be null
502      * @param namePattern a wildcard pattern for the person's real name, may be null
503      * @param mailPattern a wildcard pattern for the person's email, may be null
504      * @return the number of matching users
505      */
506     public static int countUsers(String userPattern, String realm, String namePattern, String mailPattern) {
507         return countUsers(userPattern, realm, namePattern, mailPattern, null);
508     }
509 
510     /**
511      * Counts users in the database that match the given criteria.
512      * Wildcards containing * and ? for single character may be used for searching
513      * by login user name or real name.
514      *
515      * @param userPattern a wildcard pattern for the login user name, may be null
516      * @param realm the realm the user belongs to, may be null
517      * @param namePattern a wildcard pattern for the person's real name, may be null
518      * @param mailPattern a wildcard pattern for the person's email, may be null
519      * @param attributeNamePattern a wildcard pattern for person's attribute names, may be null
520      * @return the number of matching users
521      */
522     public static int countUsers(String userPattern, String realm, String namePattern, String mailPattern,
523         String attributeNamePattern) {
524         EntityManager em = MCREntityManagerProvider.getCurrentEntityManager();
525         CriteriaBuilder cb = em.getCriteriaBuilder();
526         CriteriaQuery<Number> query = cb.createQuery(Number.class);
527         Root<MCRUser> user = query.from(MCRUser.class);
528         return em
529             .createQuery(
530                 query
531                     .select(cb.count(user))
532                     .where(
533                         buildCondition(cb, user, userPattern, realm, namePattern, mailPattern, attributeNamePattern)))
534             .getSingleResult().intValue();
535     }
536 
537     /**
538      * Checks the password of a login user in the default realm.
539      *
540      * @param userName the login user name
541      * @param password the password entered in the GUI
542      * @return true, if the password matches.
543      */
544     public static MCRUser login(String userName, String password) {
545         return login(userName, password, Collections.emptyList());
546     }
547 
548     /**
549      * Checks the password of a login user and if the user has at least one of the allowed roles in the default realm.
550      *
551      * @param userName the login user name
552      * @param password the password entered in the GUI
553      * @param allowedRoles list of allowed roles
554      * @return true, if the password matches.
555      */
556     public static MCRUser login(String userName, String password, List<String> allowedRoles) {
557         MCRUser user = checkPassword(userName, password);
558         if (user == null) {
559             return null;
560         }
561         if (!allowedRoles.isEmpty()) {
562             Collection<String> userRoles = user.getSystemRoleIDs();
563             LOGGER.info("Comparing user roles " + userRoles + " against list of allowed roles " + allowedRoles);
564             boolean hasAnyAllowedRole = userRoles.stream().anyMatch(allowedRoles::contains);
565             if (!hasAnyAllowedRole) {
566                 return null;
567             }
568         }
569         user.setLastLogin();
570         updateUser(user);
571         MCRSessionMgr.getCurrentSession().setUserInformation(user);
572         return user;
573     }
574 
575     /**
576      * Returns instance of MCRUser if current user is present in this user system
577      * @return MCRUser instance or null
578      */
579     public static MCRUser getCurrentUser() {
580         MCRUserInformation userInformation = MCRSessionMgr.getCurrentSession().getUserInformation();
581         if (userInformation instanceof MCRUser) {
582             return (MCRUser) userInformation;
583         } else {
584             return new MCRTransientUser(userInformation);
585         }
586     }
587 
588     /**
589      * Returns a {@link MCRUser} instance if the login succeeds.
590      * This method will return <code>null</code> if the user does not exist, no password was given or
591      * the login is disabled.
592      * If the {@link MCRUser#getHashType()} is {@link MCRPasswordHashType#crypt}, {@link MCRPasswordHashType#md5} or
593      * {@link MCRPasswordHashType#sha1} the hash value is automatically upgraded to {@link MCRPasswordHashType#sha256}.
594      * @param userName Name of the user to login.
595      * @param password clear text password.
596      * @return authenticated {@link MCRUser} instance or <code>null</code>.
597      */
598     public static MCRUser checkPassword(String userName, String password) {
599         MCRUser user = getUser(userName);
600         if (user == null || user.getHashType() == null) {
601             LOGGER.warn(() -> "User not found: " + userName);
602             waitLoginPanalty();
603             return null;
604         }
605         if (password == null) {
606             LOGGER.warn("No password for user {} entered", userName);
607             waitLoginPanalty();
608             return null;
609         }
610         if (!user.loginAllowed()) {
611             if (user.isDisabled()) {
612                 LOGGER.warn("User {} was disabled!", user.getUserID());
613             } else {
614                 LOGGER.warn("Password expired for user {} on {}", user.getUserID(),
615                     MCRXMLFunctions.getISODate(user.getValidUntil(), MCRISO8601Format.COMPLETE_HH_MM_SS.toString()));
616             }
617             return null;
618         }
619         try {
620             switch (user.getHashType()) {
621             case crypt:
622                 //Wahh! did we ever thought about what "salt" means for passwd management?
623                 String passwdHash = user.getPassword();
624                 String salt = passwdHash.substring(0, 3);
625                 if (!MCRUtils.asCryptString(salt, password).equals(passwdHash)) {
626                     //login failed
627                     waitLoginPanalty();
628                     return null;
629                 }
630                 //update to SHA-256
631                 updatePasswordHashToSHA256(user, password);
632                 break;
633             case md5:
634                 if (!MCRUtils.asMD5String(1, null, password).equals(user.getPassword())) {
635                     waitLoginPanalty();
636                     return null;
637                 }
638                 //update to SHA-256
639                 updatePasswordHashToSHA256(user, password);
640                 break;
641             case sha1:
642                 if (!MCRUtils.asSHA1String(HASH_ITERATIONS, Base64.getDecoder().decode(user.getSalt()), password)
643                     .equals(user.getPassword())) {
644                     waitLoginPanalty();
645                     return null;
646                 }
647                 //update to SHA-256
648                 updatePasswordHashToSHA256(user, password);
649                 break;
650             case sha256:
651                 if (!MCRUtils.asSHA256String(HASH_ITERATIONS, Base64.getDecoder().decode(user.getSalt()), password)
652                     .equals(user.getPassword())) {
653                     waitLoginPanalty();
654                     return null;
655                 }
656                 break;
657             default:
658                 throw new MCRException("Cannot validate hash type " + user.getHashType());
659             }
660         } catch (NoSuchAlgorithmException e) {
661             throw new MCRException("Error while validating login", e);
662         }
663         return user;
664     }
665 
666     private static void waitLoginPanalty() {
667         try {
668             Thread.sleep(3000);
669         } catch (InterruptedException e) {
670         }
671     }
672 
673     /**
674      * Sets password of 'user' to 'password'.
675      *
676      * Automatically updates the user in database.
677      */
678     public static void setPassword(MCRUser user, String password) {
679         MCRSession session = MCRSessionMgr.getCurrentSession();
680         MCRUserInformation currentUser = session.getUserInformation();
681         MCRUser myUser = getUser(user.getUserName(), user.getRealmID()); //only update password
682         boolean allowed = MCRAccessManager.checkPermission(MCRUser2Constants.USER_ADMIN_PERMISSION)
683             || currentUser.equals(myUser.getOwner())
684             || (currentUser.equals(user) && myUser.hasNoOwner() || !myUser.isLocked());
685         if (!allowed) {
686             throw new MCRException("You are not allowed to change password of user: " + user);
687         }
688         updatePasswordHashToSHA256(myUser, password);
689         updateUser(myUser);
690     }
691 
692     static void updatePasswordHashToSHA256(MCRUser user, String password) {
693         String newHash;
694         byte[] salt = generateSalt();
695         try {
696             newHash = MCRUtils.asSHA256String(HASH_ITERATIONS, salt, password);
697         } catch (Exception e) {
698             throw new MCRException("Could not update user password hash to SHA-256.", e);
699         }
700         user.setSalt(Base64.getEncoder().encodeToString(salt));
701         user.setHashType(MCRPasswordHashType.sha256);
702         user.setPassword(newHash);
703     }
704 
705     private static byte[] generateSalt() {
706         return SECURE_RANDOM.generateSeed(8);
707     }
708 
709     private static Optional<MCRUser> getByNaturalID(EntityManager em, String userName, String realmId) {
710         CriteriaBuilder cb = em.getCriteriaBuilder();
711         CriteriaQuery<MCRUser> query = cb.createQuery(MCRUser.class);
712         Root<MCRUser> users = query.from(MCRUser.class);
713         users.fetch(MCRUser_.owner.getName(), JoinType.LEFT);
714         try {
715             return Optional
716                 .of(em
717                     .createQuery(query
718                         .distinct(true)
719                         .where(getUserRealmCriterion(cb, users, userName, realmId)))
720                     .getSingleResult());
721         } catch (NoResultException e) {
722             return Optional.empty();
723         }
724     }
725 
726     private static Predicate[] getUserRealmCriterion(CriteriaBuilder cb, Root<MCRUser> root, String user,
727         String realmId) {
728         if (realmId == null) {
729             realmId = MCRRealmFactory.getLocalRealm().getID();
730         }
731         return new Predicate[] { cb.equal(root.get(MCRUser_.userName), user),
732             cb.equal(root.get(MCRUser_.realmID), realmId) };
733     }
734 }