View Javadoc
1   /**
2    * This file Copyright (c) 2003-2017 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
32   *
33   */
34  package info.magnolia.cms.security;
35  
36  import static info.magnolia.cms.security.SecurityConstants.*;
37  
38  import info.magnolia.cms.core.Content;
39  import info.magnolia.cms.core.HierarchyManager;
40  import info.magnolia.cms.security.auth.ACL;
41  import info.magnolia.cms.util.ContentUtil;
42  import info.magnolia.context.MgnlContext;
43  import info.magnolia.jcr.iterator.FilteringPropertyIterator;
44  import info.magnolia.jcr.predicate.JCRMgnlPropertyHidingPredicate;
45  import info.magnolia.jcr.util.NodeNameHelper;
46  import info.magnolia.jcr.util.NodeTypes;
47  import info.magnolia.jcr.util.NodeUtil;
48  import info.magnolia.jcr.util.PropertyUtil;
49  import info.magnolia.jcr.wrapper.MgnlPropertySettingNodeWrapper;
50  import info.magnolia.objectfactory.Components;
51  import info.magnolia.repository.RepositoryConstants;
52  
53  import java.util.ArrayList;
54  import java.util.Collection;
55  import java.util.Collections;
56  import java.util.GregorianCalendar;
57  import java.util.HashMap;
58  import java.util.HashSet;
59  import java.util.Iterator;
60  import java.util.List;
61  import java.util.Map;
62  import java.util.Set;
63  import java.util.SortedSet;
64  import java.util.TreeSet;
65  import java.util.stream.Collectors;
66  
67  import javax.inject.Inject;
68  import javax.jcr.ItemNotFoundException;
69  import javax.jcr.Node;
70  import javax.jcr.NodeIterator;
71  import javax.jcr.PathNotFoundException;
72  import javax.jcr.Property;
73  import javax.jcr.PropertyIterator;
74  import javax.jcr.RepositoryException;
75  import javax.jcr.Session;
76  import javax.jcr.Value;
77  import javax.jcr.ValueFormatException;
78  import javax.jcr.lock.LockException;
79  import javax.security.auth.Subject;
80  
81  import org.apache.commons.lang3.StringUtils;
82  import org.apache.jackrabbit.JcrConstants;
83  import org.slf4j.Logger;
84  import org.slf4j.LoggerFactory;
85  
86  /**
87   * Manages the users stored in Magnolia itself.
88   */
89  public class MgnlUserManager extends RepositoryBackedSecurityManager implements UserManager {
90  
91      private static final Logger log = LoggerFactory.getLogger(MgnlUserManager.class);
92  
93      public static final String PROPERTY_EMAIL = "email";
94      public static final String PROPERTY_LANGUAGE = "language";
95      public static final String PROPERTY_LASTACCESS = "lastaccess";
96      public static final String PROPERTY_PASSWORD = "pswd";
97      public static final String PROPERTY_TITLE = "title";
98      public static final String PROPERTY_ENABLED = "enabled";
99      public static final String PROPERTY_TIMEZONE = "timezone";
100 
101     public static final String NODE_ACLUSERS = "acl_users";
102 
103     private String realmName;
104 
105     private boolean allowCrossRealmDuplicateNames = false;
106 
107     private int maxFailedLoginAttempts;
108 
109     private int lockTimePeriod;
110 
111     /**
112      * There should be no need to instantiate this class except maybe for testing. Manual instantiation might cause manager not to be initialized properly.
113      */
114     @Inject
115     public MgnlUserManager(NodeNameHelper nodeNameHelper) {
116         super(nodeNameHelper);
117     }
118 
119     /**
120      * @deprecated since 5.5.3, use {@link #MgnlUserManager(NodeNameHelper)} instead.
121      */
122     @Deprecated
123     public MgnlUserManager() {
124         this(Components.getComponent(NodeNameHelper.class));
125     }
126 
127     @Override
128     public void setMaxFailedLoginAttempts(int maxFailedLoginAttempts) {
129         this.maxFailedLoginAttempts = maxFailedLoginAttempts;
130     }
131 
132     @Override
133     public int getMaxFailedLoginAttempts() {
134         return maxFailedLoginAttempts;
135     }
136 
137     @Override
138     public int getLockTimePeriod() {
139         return lockTimePeriod;
140     }
141 
142     @Override
143     public void setLockTimePeriod(int lockTimePeriod) {
144         this.lockTimePeriod = lockTimePeriod;
145     }
146 
147     @Override
148     public User setProperty(final User user, final String propertyName, final Value propertyValue) {
149         return MgnlContext.doInSystemContext(new SilentSessionOp<User>(getRepositoryName()) {
150 
151             @Override
152             public User doExec(Session session) throws RepositoryException {
153                 String path = ((MgnlUser) user).getPath();
154                 Node userNode;
155                 try {
156                     userNode = session.getNode(path);
157                     // setting value to null would remove existing properties anyway, so no need to create a
158                     // not-yet-existing-one first and then set it to null.
159                     if (propertyValue != null || PropertyUtil.getPropertyOrNull(userNode, propertyName) != null) {
160                         if (StringUtils.equals(propertyName, PROPERTY_PASSWORD)) {
161                             setPasswordProperty(userNode, propertyValue.getString());
162                         } else {
163                             userNode.setProperty(propertyName, propertyValue);
164                             session.save();
165                         }
166                     }
167                 } catch (RepositoryException e) {
168                     session.refresh(false);
169                     log.error("Property {} can't be changed. {}", propertyName, e.getMessage());
170                     return user;
171                 }
172                 return newUserInstance(userNode);
173             }
174 
175             @Override
176             public String toString() {
177                 return getClass().getName() + " setProperty(user, propertyName, Value propertyValue)";
178             }
179         });
180     }
181 
182     @Override
183     public User setProperty(final User user, final String propertyName, final String propertyValue) {
184         return MgnlContext.doInSystemContext(new SilentSessionOp<User>(getRepositoryName()) {
185 
186             @Override
187             public User doExec(Session session) throws RepositoryException {
188                 String path = ((MgnlUser) user).getPath();
189                 Node userNode;
190                 try {
191                     userNode = session.getNode(path);
192                     // setting value to null would remove existing properties anyway, so no need to create a
193                     // not-yet-existing-one first and then set it to null.
194                     if (propertyName != null) {
195                         if (StringUtils.equals(propertyName, PROPERTY_PASSWORD)) {
196                             setPasswordProperty(userNode, propertyValue);
197                         } else {
198                             userNode.setProperty(propertyName, propertyValue);
199                             session.save();
200                         }
201                     }
202                 } catch (RepositoryException e) {
203                     session.refresh(false);
204                     log.error("Property {} can't be changed. {}", propertyName, e.getMessage());
205                     return user;
206                 }
207                 return newUserInstance(userNode);
208             }
209 
210             @Override
211             public String toString() {
212                 return getClass().getName() + " setProperty(user, propertyName, String propertyValue)";
213             }
214         });
215     }
216 
217     public void setRealmName(String name) {
218         this.realmName = name;
219     }
220 
221     public String getRealmName() {
222         return realmName;
223     }
224 
225     public void setAllowCrossRealmDuplicateNames(boolean allowCrossRealmDuplicateNames) {
226         this.allowCrossRealmDuplicateNames = allowCrossRealmDuplicateNames;
227     }
228 
229     public boolean isAllowCrossRealmDuplicateNames() {
230         return allowCrossRealmDuplicateNames;
231     }
232 
233     /**
234      * Get the user object. Uses a search
235      *
236      * @param name name of the user to retrieve
237      * @return the user object
238      */
239     @Override
240     public User getUser(final String name) {
241         try {
242             // method is called internally as well as externally, we do not need to wrap it multiple times unnecessarily.
243             if (MgnlContext.isSystemInstance()) {
244                 return getUser(name, MgnlContext.getJCRSession(getRepositoryName()));
245             } else {
246                 return MgnlContext.doInSystemContext(new JCRSessionOp<User>(getRepositoryName()) {
247                     @Override
248                     public User exec(Session session) throws RepositoryException {
249                         return getUser(name, session);
250                     }
251 
252                     @Override
253                     public String toString() {
254                         return "retrieve user " + name;
255                     }
256                 });
257             }
258         } catch (RepositoryException e) {
259             log.error("Could not retrieve user with name: {}", name, e);
260         }
261         return null;
262     }
263 
264     private User getUser(String name, Session session) throws RepositoryException {
265         Node priviledgedUserNode = findPrincipalNode(name, session);
266         return newUserInstance(priviledgedUserNode);
267     }
268 
269     /**
270      * Get the user object. Uses a search
271      *
272      * @param id user identifier
273      * @return the user object
274      */
275     @Override
276     public User getUserById(final String id) {
277         try {
278             return MgnlContext.doInSystemContext(new JCRSessionOp<User>(getRepositoryName()) {
279                 @Override
280                 public User exec(Session session) throws RepositoryException {
281                     Node priviledgedUserNode = session.getNodeByIdentifier(id);
282                     return newUserInstance(priviledgedUserNode);
283                 }
284 
285                 @Override
286                 public String toString() {
287                     return "retrieve user with id " + id;
288                 }
289             });
290         } catch (RepositoryException e) {
291             log.error("Could not retrieve user with id: {}", id, e);
292         }
293         return null;
294     }
295 
296     @Override
297     public User getUser(Subject subject) throws UnsupportedOperationException {
298         // this could be the case if no one is logged in yet
299         if (subject == null) {
300             log.debug("subject not set.");
301             return new DummyUser();
302         }
303 
304         Set<User> principalSet = subject.getPrincipals(User.class);
305         Iterator<User> entityIterator = principalSet.iterator();
306         if (!entityIterator.hasNext()) {
307             // happens when JCR authentication module set to optional and user doesn't exist in magnolia
308             log.debug("user name not contained in principal set.");
309             return new DummyUser();
310         }
311         return entityIterator.next();
312     }
313 
314     /**
315      * Helper method to find a user in a certain realm. Uses JCR Query.
316      * This will return null if user doesn't exist in realm.
317      */
318     @Override
319     protected Node findPrincipalNode(String name, Session session) throws RepositoryException {
320         final String realmName = getRealmName();
321         // the all realm searches the repository
322         final Node startNode = (Realm.REALM_ALL.getName().equals(realmName)) ? session.getRootNode() : session.getNode("/" + realmName);
323 
324         return findPrincipalNode(name, session, NodeTypes.User.NAME, startNode);
325     }
326 
327     protected User getFromRepository(final String name) throws RepositoryException {
328         return MgnlContext.doInSystemContext(new SilentSessionOp<User>(getRepositoryName()) {
329 
330             @Override
331             public User doExec(Session session) throws RepositoryException {
332                 Node userNode = findPrincipalNode(name, session);
333                 return newUserInstance(userNode);
334             }
335 
336             @Override
337             public String toString() {
338                 return "Retrieve user [" + name + "] from repository.";
339             }
340         });
341     }
342 
343     /**
344      * SystemUserManager does this.
345      */
346     @Override
347     public User getSystemUser() throws UnsupportedOperationException {
348         throw new UnsupportedOperationException();
349     }
350 
351     /**
352      * SystemUserManager does this.
353      */
354     @Override
355     public User getAnonymousUser() throws UnsupportedOperationException {
356         throw new UnsupportedOperationException();
357     }
358 
359     /**
360      * Get all users managed by this user manager.
361      */
362     @Override
363     public Collection<User> getAllUsers() {
364         return MgnlContext.doInSystemContext(new SilentSessionOp<Collection<User>>(getRepositoryName()) {
365 
366             @Override
367             public Collection<User> doExec(Session session) throws RepositoryException {
368                 List<User> users = new ArrayList<User>();
369                 Node node = session.getNode("/" + realmName);
370                 findAllUsersInFolder(node, users);
371                 return users;
372             }
373 
374             @Override
375             public String toString() {
376                 return "get all users";
377             }
378 
379         });
380     }
381 
382     /**
383      * Finds all users located in the provided node or in sub-folders within it and adds them to the given collection.
384      * As this method bases on a method using jcr queries to find nodes, it might not see nodes that have been created but not saved yet (i.e. during installation of a module).
385      */
386     public void findAllUsersInFolder(Node node, Collection<User> addTo) throws RepositoryException {
387         final NodeIterator nodesIter = findPrincipalNodes(node, NodeTypes.User.NAME);
388         while (nodesIter.hasNext()) {
389             addTo.add(newUserInstance(nodesIter.nextNode()));
390         }
391     }
392 
393     @Override
394     public User createUser(final String name, final String pw) {
395         return this.createUser(null, name, pw);
396     }
397 
398     @Override
399     public User createUser(final String path, final String name, final String pw) throws UnsupportedOperationException {
400         validateUsername(name);
401         return MgnlContext.doInSystemContext(new SilentSessionOp<MgnlUser>(getRepositoryName()) {
402 
403             @Override
404             public MgnlUser doExec(Session session) throws RepositoryException {
405                 String uPath = path == null ? "/" + getRealmName() : path;
406                 Node userNode = createUserNode(uPath, name, session);
407                 userNode.addMixin(JcrConstants.MIX_LOCKABLE);
408                 userNode.setProperty("name", name);
409                 setPasswordProperty(userNode, pw);
410                 userNode.setProperty("language", "en");
411 
412                 final String handle = userNode.getPath();
413                 final Node acls = userNode.addNode(NODE_ACLUSERS, NodeTypes.ContentNode.NAME);
414                 // read only access to the node itself
415                 Node acl = acls.addNode(nodeNameHelper.getUniqueName(session, acls.getPath(), "0"), NodeTypes.ContentNode.NAME);
416                 acl.setProperty("path", handle);
417                 acl.setProperty("permissions", Permission.READ);
418                 // those who had access to their nodes should get access to their own props
419                 addWrite(handle, PROPERTY_EMAIL, acls);
420                 addWrite(handle, PROPERTY_LANGUAGE, acls);
421                 addWrite(handle, PROPERTY_LASTACCESS, acls);
422                 addWrite(handle, PROPERTY_PASSWORD, acls);
423                 addWrite(handle, PROPERTY_TITLE, acls);
424                 addWrite(handle, PROPERTY_TIMEZONE, acls);
425                 session.save();
426                 return new MgnlUser(userNode.getName(), getRealmName(), Collections.emptySet(), Collections.emptySet(), Collections.emptyMap(), userNode.getPath(), userNode.getIdentifier(), Collections.emptySet(), Collections.emptySet());
427             }
428 
429             @Override
430             public String toString() {
431                 return "create user " + name;
432             }
433         });
434     }
435 
436     @Override
437     public User changePassword(final User user, final String newPassword) {
438         return MgnlContext.doInSystemContext(new SilentSessionOp<User>(getRepositoryName()) {
439 
440             @Override
441             public User doExec(Session session) throws RepositoryException {
442                 Node userNode = findPrincipalNode(user.getName(), session);
443                 if (userNode != null) {
444                     setPasswordProperty(userNode, newPassword);
445 
446                     return newUserInstance(userNode);
447                 }
448                 return null;
449             }
450 
451             @Override
452             public String toString() {
453                 return "change password of user " + user.getName();
454             }
455         });
456     }
457 
458     protected void setPasswordProperty(Node userNode, String clearPassword) throws RepositoryException {
459         userNode.setProperty(PROPERTY_PASSWORD, encodePassword(clearPassword));
460         userNode.getSession().save();
461     }
462 
463     protected String encodePassword(String clearPassword) {
464         return SecurityUtil.getBCrypt(clearPassword);
465     }
466 
467     protected void validateUsername(String name) {
468         if (StringUtils.isBlank(name)) {
469             throw new IllegalArgumentException(name + " is not a valid username.");
470         }
471 
472         User user;
473         if (isAllowCrossRealmDuplicateNames()) {
474             user = this.getUser(name);
475         } else {
476             user = Security.getUserManager().getUser(name);
477         }
478         if (user != null) {
479             throw new IllegalArgumentException("User with name " + name + " already exists.");
480         }
481     }
482 
483     /**
484      * @deprecated since 5.3.2 use {@link #createUserNode(String, String, javax.jcr.Session)} instead
485      */
486     @Deprecated
487     protected Content createUserNode(String name) throws RepositoryException {
488         final String path = "/" + getRealmName();
489         final String userName = name;
490         Node userNode = MgnlContext.doInSystemContext(new SilentSessionOp<Node>(getRepositoryName()) {
491             @Override
492             public Node doExec(Session session) throws RepositoryException {
493                 return createUserNode(path, userName, session);
494             }
495 
496             @Override
497             public String toString() {
498                 return getClass().getName() + " createUSerNode(name)";
499             }
500         });
501         return ContentUtil.asContent(userNode);
502 
503     }
504 
505     protected Node createUserNode(String path, String userName, Session session) throws RepositoryException {
506         return session.getNode(path).addNode(userName, NodeTypes.User.NAME);
507     }
508 
509     /**
510      * Return the HierarchyManager for the user workspace (through the system context).
511      *
512      * @deprecated since 5.3.2 without replacement
513      */
514     @Deprecated
515     protected HierarchyManager getHierarchyManager() {
516         return MgnlContext.getSystemContext().getHierarchyManager(RepositoryConstants.USERS);
517     }
518 
519     private Node addWrite(String parentPath, String property, Node acls) throws PathNotFoundException, RepositoryException, AccessDeniedException {
520         Node acl = acls.addNode(nodeNameHelper.getUniqueName(acls.getSession(), acls.getPath(), "0"), NodeTypes.ContentNode.NAME);
521         acl.setProperty("path", parentPath + "/" + property);
522         acl.setProperty("permissions", Permission.ALL);
523         return acl;
524     }
525 
526     @Override
527     public void updateLastAccessTimestamp(final User user) throws UnsupportedOperationException {
528         final String workspaceName = getRepositoryName();
529         try {
530             MgnlContext.doInSystemContext(new MgnlContext.LockingOp(workspaceName, ((MgnlUser) user).getPath()) {
531 
532                 @Override
533                 public void doExec() throws RepositoryException {
534                     Session session = MgnlContext.getJCRSession(workspaceName);
535                     String path = ((MgnlUser) user).getPath();
536                     log.debug("update access timestamp for {}", user.getName());
537                     try {
538                         Node userNode = session.getNode(path);
539                         userNode = NodeUtil.deepUnwrap(userNode, MgnlPropertySettingNodeWrapper.class);
540                         PropertyUtil.updateOrCreate(userNode, "lastaccess", new GregorianCalendar());
541                         session.save();
542                     } catch (RepositoryException e) {
543                         session.refresh(false);
544                     }
545                     return;
546                 }
547 
548                 @Override
549                 public String toString() {
550                     return getClass().getName() + " updateLastAccessTimestamp(user)";
551                 }
552             });
553         } catch (LockException e) {
554             log.debug("Failed to lock node for last access timestamp update for user {} with {}", user.getName(), e.getMessage(), e);
555         } catch (RepositoryException e) {
556             log.error("Failed to update user {} last access time stamp with {}", user.getName(), e.getMessage(), e);
557         }
558     }
559 
560     protected User newUserInstance(Node privilegedUserNode) throws ValueFormatException, PathNotFoundException, RepositoryException {
561         if (privilegedUserNode == null) {
562             return null;
563         }
564         Set<String> roles = collectUniquePropertyNames(privilegedUserNode, ROLES_NODE_NAME, RepositoryConstants.USER_ROLES, false);
565         Set<String> groups = collectUniquePropertyNames(privilegedUserNode, GROUPS_NODE_NAME, RepositoryConstants.USER_GROUPS, false);
566 
567         final String userName = privilegedUserNode.getName();
568         // SecuritySupport cannot be injected cause it's not ready during Magnolia init phase
569         final GroupManager groupManager = Components.getComponent(SecuritySupport.class).getGroupManager();
570         // groups must be resolved first as finding roles depends on them
571         Set<String> allGroups = aggregateDirectAndTransitiveGroups(groups, userName, groupManager);
572         Set<String> allRoles = aggregateDirectAndTransitiveRoles(roles, allGroups, userName, groupManager);
573 
574 
575         Map<String, String> properties = new HashMap<>();
576         for (PropertyIterator iter = new FilteringPropertyIterator(privilegedUserNode.getProperties(), new JCRMgnlPropertyHidingPredicate()); iter.hasNext();) {
577             Property prop = iter.nextProperty();
578             // TODO: should we check and skip binary props in case someone adds image to the user?
579             properties.put(prop.getName(), prop.getString());
580         }
581         return new MgnlUser(userName, getRealmName(), groups, roles, properties, privilegedUserNode.getPath(), privilegedUserNode.getIdentifier(), allGroups, allRoles);
582     }
583 
584     @Override
585     protected String getRepositoryName() {
586         return RepositoryConstants.USERS;
587     }
588 
589     /**
590      * Sets access control list from a list of roles under the provided content object.
591      */
592     @Override
593     public Map<String, ACL> getACLs(final User user) {
594         if (!(user instanceof MgnlUser)) {
595             return null;
596         }
597         return super.getACLs(user.getName());
598     }
599 
600     @Override
601     public User addRole(User user, String roleName) {
602         try {
603             super.add(user.getName(), roleName, NODE_ROLES);
604         } catch (PrincipalNotFoundException e) {
605             // user doesn't exist in this UM
606             return null;
607         }
608         return getUser(user.getName());
609     }
610 
611     /**
612      * Collects all property names of given type, sorting them (case insensitive) and removing duplicates in the process.
613      */
614     protected Set<String> collectUniquePropertyNames(Node rootNode, String subnodeName, String repositoryName, boolean isDeep) {
615         final SortedSet<String> set = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
616         String path = null;
617         try {
618             path = rootNode.getPath();
619             final Node node = rootNode.getNode(subnodeName);
620             collectUniquePropertyNames(node, repositoryName, subnodeName, set, isDeep);
621             collectUniquePropertyNames(rootNode.getNode(subnodeName), repositoryName, subnodeName, set, isDeep);
622         } catch (PathNotFoundException e) {
623             log.debug("{} does not have any {}", path, repositoryName);
624         } catch (Throwable t) {
625             log.error("Failed to read {} or sub node {} in repository {}", path, subnodeName, repositoryName, t);
626         }
627         return set;
628     }
629 
630     private void collectUniquePropertyNames(final Node node, final String repositoryName, final String subnodeName, final Collection<String> set, final boolean isDeep) throws RepositoryException {
631         if (!MgnlContext.isSystemInstance()) {
632             if (log.isDebugEnabled()) {
633                 log.debug("Collecting user properties in user context. List might not include all properties. Please check the calling code (see stacktrace)", new Exception());
634             } else {
635                 log.warn("Collecting user properties in user context. List might not include all properties. Please check the calling code (stacktrace will be printed for this call when debug logging is enabled)");
636             }
637         }
638         Session session = MgnlContext.getJCRSession(repositoryName);
639         for (PropertyIterator iter = new FilteringPropertyIterator(node.getProperties(), new JCRMgnlPropertyHidingPredicate()); iter.hasNext();) {
640             Property property = iter.nextProperty();
641             final String uuid = property.getString();
642             try {
643                 final Node targetNode = session.getNodeByIdentifier(uuid);
644                 set.add(targetNode.getName());
645                 if (isDeep && targetNode.hasNode(subnodeName)) {
646                     collectUniquePropertyNames(targetNode.getNode(subnodeName), repositoryName, subnodeName, set, true);
647                 }
648             } catch (ItemNotFoundException t) {
649                 final String path = property.getPath();
650                 // TODO: why we are using UUIDs here? shouldn't be better to use group names, since uuids can change???
651                 log.warn("Can't find {} node by UUID {} referred by node {}", repositoryName, t.getMessage(), path);
652                 log.debug("Failed while reading node by UUID", t);
653                 // we continue since it can happen that target node is removed
654                 // - UUID's are kept as simple strings thus have no referential integrity
655             }
656         }
657     }
658 
659     @Override
660     public User addGroup(User user, String groupName) {
661         try {
662             super.add(user.getName(), groupName, NODE_GROUPS);
663         } catch (PrincipalNotFoundException e) {
664             // user doesn't exist in this UM
665             return null;
666         }
667         return getUser(user.getName());
668     }
669 
670     @Override
671     public User removeGroup(User user, String groupName) {
672         try {
673             super.remove(user.getName(), groupName, NODE_GROUPS);
674         } catch (PrincipalNotFoundException e) {
675             // user doesn't exist in this UM
676             return null;
677         }
678         return getUser(user.getName());
679     }
680 
681     @Override
682     public User removeRole(User user, String roleName) {
683         try {
684             super.remove(user.getName(), roleName, NODE_ROLES);
685         } catch (PrincipalNotFoundException e) {
686             // user doesn't exist in this UM
687             return null;
688         }
689         return getUser(user.getName());
690     }
691 
692     @Override
693     public Collection<String> getUsersWithGroup(final String groupName) {
694         return MgnlContext.doInSystemContext(new SilentSessionOp<Collection<String>>(getRepositoryName()) {
695 
696             @Override
697             public Collection<String> doExec(Session session) throws RepositoryException {
698                 final Node groupNode = findPrincipalNode(groupName, MgnlContext.getJCRSession(RepositoryConstants.USER_GROUPS), NodeTypes.Group.NAME);
699                 return findUsersOrGroupsHavingAssignedGroupOrRoleWithUid(session, groupNode, GROUPS_NODE_NAME);
700             }
701 
702             @Override
703             public String toString() {
704                 return "get group " + groupName;
705             }
706         });
707     }
708 
709     @Override
710     public Collection<String> getUsersWithRole(final String roleName) {
711         return MgnlContext.doInSystemContext(new SilentSessionOp<Collection<String>>(getRepositoryName()) {
712 
713             @Override
714             public Collection<String> doExec(Session session) throws RepositoryException {
715                 final Node roleNode = findPrincipalNode(roleName, MgnlContext.getJCRSession(RepositoryConstants.USER_ROLES), NodeTypes.Role.NAME);
716                 return findUsersOrGroupsHavingAssignedGroupOrRoleWithUid(session, roleNode, ROLES_NODE_NAME);
717             }
718 
719             @Override
720             public String toString() {
721                 return "get role " + roleName;
722             }
723         });
724     }
725 
726     @Override
727     public Collection<String> getUsersWithGroup(String groupName, boolean transitive) {
728         if (!transitive) {
729             return getUsersWithGroup(groupName);
730         }
731 
732         Set<String> users = new HashSet<>();
733         // FYI: can't inject securitySupport or get static instance of SecuritySupport during the initialization phase.
734         GroupManager man = SecuritySupport.Factory.getInstance().getGroupManager();
735         Collection<String> groupNames = man.getAllSubGroups(groupName);
736         groupNames.add(groupName);
737         for (String transitiveGroup : groupNames) {
738             Collection<String> userNames = getUsersWithGroup(transitiveGroup);
739             users.addAll(userNames);
740         }
741         return users;
742     }
743 
744     /**
745      * @return a Set of all roles, sorting them (case insensitive), directly assigned and transitive for this user.
746      * @see #newUserInstance(Node)
747      */
748     protected Set<String> aggregateDirectAndTransitiveRoles(Collection<String> roles, Collection<String> allGroups, String name, GroupManager groupManager) {
749 
750         final Set<String> set = new HashSet<>(roles);
751 
752         // add roles from all groups
753         allGroups.forEach(group -> {
754             try {
755                 set.addAll(groupManager.getGroup(group).getRoles());
756             } catch (javax.jcr.AccessDeniedException e) {
757                 log.debug("Skipping denied group {} for user {}", group, name, e);
758             } catch (UnsupportedOperationException e) {
759                 log.debug("Skipping unsupported  getGroup() for group {} and user {}", group, name, e);
760             }
761         });
762 
763         return set.stream()
764                 .sorted(String::compareToIgnoreCase)
765                 .collect(Collectors.toSet());
766     }
767 
768     /**
769      * @return a Set of all groups, directly assigned and transitive for this user.
770      * @see #newUserInstance(Node)
771      */
772     protected Set<String> aggregateDirectAndTransitiveGroups(Collection<String> groups, String name, GroupManager groupManager) {
773         final Set<String> set = new HashSet<>();
774 
775         // add all subgroups
776         addSubgroups(set, groupManager, groups, name);
777 
778         return set.stream()
779                 .sorted(String::compareToIgnoreCase)
780                 .collect(Collectors.toSet());
781     }
782 
783     /**
784      * Any group from the groups is checked for the subgroups only if it is not in the allGroups yet. This is to prevent infinite loops in case of cyclic group assignment.
785      */
786     private void addSubgroups(final Set<String> allGroups, GroupManager groupManager, Collection<String> groups, String name) {
787         groups.forEach(groupName -> {
788             // check if this group was not already added to prevent infinite loops
789             if (!allGroups.contains(groupName)) {
790                 allGroups.add(groupName);
791                 try {
792                     Group group = groupManager.getGroup(groupName);
793                     if (group == null) {
794                         log.error("Failed to resolve group {} for user {}.", groupName, name);
795                         return; // skips current iteration
796                     }
797                     Collection<String> subgroups = group.getGroups();
798                     // and recursively add more subgroups
799                     addSubgroups(allGroups, groupManager, subgroups, name);
800                 } catch (javax.jcr.AccessDeniedException e) {
801                     log.debug("Skipping denied group {} for user {}", groupName, name, e);
802                 } catch (UnsupportedOperationException e) {
803                     log.debug("Skipping unsupported getGroup() for group {} and user {}", groupName, name, e);
804                 }
805             }
806         });
807     }
808 }