View Javadoc
1   /**
2    * This file Copyright (c) 2003-2018 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                 // this permission was always meant for admins only. see Antti's comment in MAGNOLIA-7108
425                 if (Realm.REALM_ADMIN.getName().equals(getRealmName())) {
426                     addWrite(handle, PROPERTY_TIMEZONE, acls);
427                 }
428                 session.save();
429                 return new MgnlUser(userNode.getName(), getRealmName(), Collections.emptySet(), Collections.emptySet(), Collections.emptyMap(), userNode.getPath(), userNode.getIdentifier(), Collections.emptySet(), Collections.emptySet());
430             }
431 
432             @Override
433             public String toString() {
434                 return "create user " + name;
435             }
436         });
437     }
438 
439     @Override
440     public User changePassword(final User user, final String newPassword) {
441         return MgnlContext.doInSystemContext(new SilentSessionOp<User>(getRepositoryName()) {
442 
443             @Override
444             public User doExec(Session session) throws RepositoryException {
445                 Node userNode = findPrincipalNode(user.getName(), session);
446                 if (userNode != null) {
447                     setPasswordProperty(userNode, newPassword);
448 
449                     return newUserInstance(userNode);
450                 }
451                 return null;
452             }
453 
454             @Override
455             public String toString() {
456                 return "change password of user " + user.getName();
457             }
458         });
459     }
460 
461     protected void setPasswordProperty(Node userNode, String clearPassword) throws RepositoryException {
462         userNode.setProperty(PROPERTY_PASSWORD, encodePassword(clearPassword));
463         userNode.getSession().save();
464     }
465 
466     protected String encodePassword(String clearPassword) {
467         return SecurityUtil.getBCrypt(clearPassword);
468     }
469 
470     protected void validateUsername(String name) {
471         if (StringUtils.isBlank(name)) {
472             throw new IllegalArgumentException(name + " is not a valid username.");
473         }
474 
475         User user;
476         if (isAllowCrossRealmDuplicateNames()) {
477             user = this.getUser(name);
478         } else {
479             user = Security.getUserManager().getUser(name);
480         }
481         if (user != null) {
482             throw new IllegalArgumentException("User with name " + name + " already exists.");
483         }
484     }
485 
486     /**
487      * @deprecated since 5.3.2 use {@link #createUserNode(String, String, javax.jcr.Session)} instead
488      */
489     @Deprecated
490     protected Content createUserNode(String name) throws RepositoryException {
491         final String path = "/" + getRealmName();
492         final String userName = name;
493         Node userNode = MgnlContext.doInSystemContext(new SilentSessionOp<Node>(getRepositoryName()) {
494             @Override
495             public Node doExec(Session session) throws RepositoryException {
496                 return createUserNode(path, userName, session);
497             }
498 
499             @Override
500             public String toString() {
501                 return getClass().getName() + " createUSerNode(name)";
502             }
503         });
504         return ContentUtil.asContent(userNode);
505 
506     }
507 
508     protected Node createUserNode(String path, String userName, Session session) throws RepositoryException {
509         return session.getNode(path).addNode(userName, NodeTypes.User.NAME);
510     }
511 
512     /**
513      * Return the HierarchyManager for the user workspace (through the system context).
514      *
515      * @deprecated since 5.3.2 without replacement
516      */
517     @Deprecated
518     protected HierarchyManager getHierarchyManager() {
519         return MgnlContext.getSystemContext().getHierarchyManager(RepositoryConstants.USERS);
520     }
521 
522     private Node addWrite(String parentPath, String property, Node acls) throws PathNotFoundException, RepositoryException, AccessDeniedException {
523         Node acl = acls.addNode(nodeNameHelper.getUniqueName(acls.getSession(), acls.getPath(), "0"), NodeTypes.ContentNode.NAME);
524         acl.setProperty("path", parentPath + "/" + property);
525         acl.setProperty("permissions", Permission.ALL);
526         return acl;
527     }
528 
529     @Override
530     public void updateLastAccessTimestamp(final User user) throws UnsupportedOperationException {
531         final String workspaceName = getRepositoryName();
532         try {
533             MgnlContext.doInSystemContext(new MgnlContext.LockingOp(workspaceName, ((MgnlUser) user).getPath()) {
534 
535                 @Override
536                 public void doExec() throws RepositoryException {
537                     Session session = MgnlContext.getJCRSession(workspaceName);
538                     String path = ((MgnlUser) user).getPath();
539                     log.debug("update access timestamp for {}", user.getName());
540                     try {
541                         Node userNode = session.getNode(path);
542                         userNode = NodeUtil.deepUnwrap(userNode, MgnlPropertySettingNodeWrapper.class);
543                         PropertyUtil.updateOrCreate(userNode, "lastaccess", new GregorianCalendar());
544                         session.save();
545                     } catch (RepositoryException e) {
546                         session.refresh(false);
547                     }
548                     return;
549                 }
550 
551                 @Override
552                 public String toString() {
553                     return getClass().getName() + " updateLastAccessTimestamp(user)";
554                 }
555             });
556         } catch (LockException e) {
557             log.debug("Failed to lock node for last access timestamp update for user {} with {}", user.getName(), e.getMessage(), e);
558         } catch (RepositoryException e) {
559             log.error("Failed to update user {} last access time stamp with {}", user.getName(), e.getMessage(), e);
560         }
561     }
562 
563     protected User newUserInstance(Node privilegedUserNode) throws ValueFormatException, PathNotFoundException, RepositoryException {
564         if (privilegedUserNode == null) {
565             return null;
566         }
567         Set<String> roles = collectUniquePropertyNames(privilegedUserNode, ROLES_NODE_NAME, RepositoryConstants.USER_ROLES, false);
568         Set<String> groups = collectUniquePropertyNames(privilegedUserNode, GROUPS_NODE_NAME, RepositoryConstants.USER_GROUPS, false);
569 
570         final String userName = privilegedUserNode.getName();
571         // SecuritySupport cannot be injected cause it's not ready during Magnolia init phase
572         final GroupManager groupManager = Components.getComponent(SecuritySupport.class).getGroupManager();
573         // groups must be resolved first as finding roles depends on them
574         Set<String> allGroups = aggregateDirectAndTransitiveGroups(groups, userName, groupManager);
575         Set<String> allRoles = aggregateDirectAndTransitiveRoles(roles, allGroups, userName, groupManager);
576 
577 
578         Map<String, String> properties = new HashMap<>();
579         for (PropertyIterator iter = new FilteringPropertyIterator(privilegedUserNode.getProperties(), new JCRMgnlPropertyHidingPredicate()); iter.hasNext();) {
580             Property prop = iter.nextProperty();
581             // TODO: should we check and skip binary props in case someone adds image to the user?
582             properties.put(prop.getName(), prop.getString());
583         }
584         return new MgnlUser(userName, getRealmName(), groups, roles, properties, privilegedUserNode.getPath(), privilegedUserNode.getIdentifier(), allGroups, allRoles);
585     }
586 
587     @Override
588     protected String getRepositoryName() {
589         return RepositoryConstants.USERS;
590     }
591 
592     /**
593      * Sets access control list from a list of roles under the provided content object.
594      */
595     @Override
596     public Map<String, ACL> getACLs(final User user) {
597         if (!(user instanceof MgnlUser)) {
598             return null;
599         }
600         return super.getACLs(user.getName());
601     }
602 
603     @Override
604     public User addRole(User user, String roleName) {
605         try {
606             super.add(user.getName(), roleName, NODE_ROLES);
607         } catch (PrincipalNotFoundException e) {
608             // user doesn't exist in this UM
609             return null;
610         }
611         return getUser(user.getName());
612     }
613 
614     /**
615      * Collects all property names of given type, sorting them (case insensitive) and removing duplicates in the process.
616      */
617     protected Set<String> collectUniquePropertyNames(Node rootNode, String subnodeName, String repositoryName, boolean isDeep) {
618         final SortedSet<String> set = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
619         String path = null;
620         try {
621             path = rootNode.getPath();
622             final Node node = rootNode.getNode(subnodeName);
623             collectUniquePropertyNames(node, repositoryName, subnodeName, set, isDeep);
624             collectUniquePropertyNames(rootNode.getNode(subnodeName), repositoryName, subnodeName, set, isDeep);
625         } catch (PathNotFoundException e) {
626             log.debug("{} does not have any {}", path, repositoryName);
627         } catch (Throwable t) {
628             log.error("Failed to read {} or sub node {} in repository {}", path, subnodeName, repositoryName, t);
629         }
630         return set;
631     }
632 
633     private void collectUniquePropertyNames(final Node node, final String repositoryName, final String subnodeName, final Collection<String> set, final boolean isDeep) throws RepositoryException {
634         if (!MgnlContext.isSystemInstance()) {
635             if (log.isDebugEnabled()) {
636                 log.debug("Collecting user properties in user context. List might not include all properties. Please check the calling code (see stacktrace)", new Exception());
637             } else {
638                 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)");
639             }
640         }
641         Session session = MgnlContext.getJCRSession(repositoryName);
642         for (PropertyIterator iter = new FilteringPropertyIterator(node.getProperties(), new JCRMgnlPropertyHidingPredicate()); iter.hasNext();) {
643             Property property = iter.nextProperty();
644             final String uuid = property.getString();
645             try {
646                 final Node targetNode = session.getNodeByIdentifier(uuid);
647                 set.add(targetNode.getName());
648                 if (isDeep && targetNode.hasNode(subnodeName)) {
649                     collectUniquePropertyNames(targetNode.getNode(subnodeName), repositoryName, subnodeName, set, true);
650                 }
651             } catch (ItemNotFoundException t) {
652                 final String path = property.getPath();
653                 // TODO: why we are using UUIDs here? shouldn't be better to use group names, since uuids can change???
654                 log.warn("Can't find {} node by UUID {} referred by node {}", repositoryName, t.getMessage(), path);
655                 log.debug("Failed while reading node by UUID", t);
656                 // we continue since it can happen that target node is removed
657                 // - UUID's are kept as simple strings thus have no referential integrity
658             }
659         }
660     }
661 
662     @Override
663     public User addGroup(User user, String groupName) {
664         try {
665             super.add(user.getName(), groupName, NODE_GROUPS);
666         } catch (PrincipalNotFoundException e) {
667             // user doesn't exist in this UM
668             return null;
669         }
670         return getUser(user.getName());
671     }
672 
673     @Override
674     public User removeGroup(User user, String groupName) {
675         try {
676             super.remove(user.getName(), groupName, NODE_GROUPS);
677         } catch (PrincipalNotFoundException e) {
678             // user doesn't exist in this UM
679             return null;
680         }
681         return getUser(user.getName());
682     }
683 
684     @Override
685     public User removeRole(User user, String roleName) {
686         try {
687             super.remove(user.getName(), roleName, NODE_ROLES);
688         } catch (PrincipalNotFoundException e) {
689             // user doesn't exist in this UM
690             return null;
691         }
692         return getUser(user.getName());
693     }
694 
695     @Override
696     public Collection<String> getUsersWithGroup(final String groupName) {
697         return MgnlContext.doInSystemContext(new SilentSessionOp<Collection<String>>(getRepositoryName()) {
698 
699             @Override
700             public Collection<String> doExec(Session session) throws RepositoryException {
701                 final Node groupNode = findPrincipalNode(groupName, MgnlContext.getJCRSession(RepositoryConstants.USER_GROUPS), NodeTypes.Group.NAME);
702                 return findUsersOrGroupsHavingAssignedGroupOrRoleWithUid(session, groupNode, GROUPS_NODE_NAME);
703             }
704 
705             @Override
706             public String toString() {
707                 return "get group " + groupName;
708             }
709         });
710     }
711 
712     @Override
713     public Collection<String> getUsersWithRole(final String roleName) {
714         return MgnlContext.doInSystemContext(new SilentSessionOp<Collection<String>>(getRepositoryName()) {
715 
716             @Override
717             public Collection<String> doExec(Session session) throws RepositoryException {
718                 final Node roleNode = findPrincipalNode(roleName, MgnlContext.getJCRSession(RepositoryConstants.USER_ROLES), NodeTypes.Role.NAME);
719                 return findUsersOrGroupsHavingAssignedGroupOrRoleWithUid(session, roleNode, ROLES_NODE_NAME);
720             }
721 
722             @Override
723             public String toString() {
724                 return "get role " + roleName;
725             }
726         });
727     }
728 
729     @Override
730     public Collection<String> getUsersWithGroup(String groupName, boolean transitive) {
731         if (!transitive) {
732             return getUsersWithGroup(groupName);
733         }
734 
735         Set<String> users = new HashSet<>();
736         // FYI: can't inject securitySupport or get static instance of SecuritySupport during the initialization phase.
737         GroupManager man = SecuritySupport.Factory.getInstance().getGroupManager();
738         Collection<String> groupNames = man.getAllSubGroups(groupName);
739         groupNames.add(groupName);
740         for (String transitiveGroup : groupNames) {
741             Collection<String> userNames = getUsersWithGroup(transitiveGroup);
742             users.addAll(userNames);
743         }
744         return users;
745     }
746 
747     /**
748      * @return a Set of all roles, sorting them (case insensitive), directly assigned and transitive for this user.
749      * @see #newUserInstance(Node)
750      */
751     protected Set<String> aggregateDirectAndTransitiveRoles(Collection<String> roles, Collection<String> allGroups, String name, GroupManager groupManager) {
752 
753         final Set<String> set = new HashSet<>(roles);
754 
755         // add roles from all groups
756         allGroups.forEach(group -> {
757             try {
758                 set.addAll(groupManager.getGroup(group).getRoles());
759             } catch (javax.jcr.AccessDeniedException e) {
760                 log.debug("Skipping denied group {} for user {}", group, name, e);
761             } catch (UnsupportedOperationException e) {
762                 log.debug("Skipping unsupported  getGroup() for group {} and user {}", group, name, e);
763             }
764         });
765 
766         return set.stream()
767                 .sorted(String::compareToIgnoreCase)
768                 .collect(Collectors.toSet());
769     }
770 
771     /**
772      * @return a Set of all groups, directly assigned and transitive for this user.
773      * @see #newUserInstance(Node)
774      */
775     protected Set<String> aggregateDirectAndTransitiveGroups(Collection<String> groups, String name, GroupManager groupManager) {
776         final Set<String> set = new HashSet<>();
777 
778         // add all subgroups
779         addSubgroups(set, groupManager, groups, name);
780 
781         return set.stream()
782                 .sorted(String::compareToIgnoreCase)
783                 .collect(Collectors.toSet());
784     }
785 
786     /**
787      * 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.
788      */
789     private void addSubgroups(final Set<String> allGroups, GroupManager groupManager, Collection<String> groups, String name) {
790         groups.forEach(groupName -> {
791             // check if this group was not already added to prevent infinite loops
792             if (!allGroups.contains(groupName)) {
793                 allGroups.add(groupName);
794                 try {
795                     Group group = groupManager.getGroup(groupName);
796                     if (group == null) {
797                         log.error("Failed to resolve group {} for user {}.", groupName, name);
798                         return; // skips current iteration
799                     }
800                     Collection<String> subgroups = group.getGroups();
801                     // and recursively add more subgroups
802                     addSubgroups(allGroups, groupManager, subgroups, name);
803                 } catch (javax.jcr.AccessDeniedException e) {
804                     log.debug("Skipping denied group {} for user {}", groupName, name, e);
805                 } catch (UnsupportedOperationException e) {
806                     log.debug("Skipping unsupported getGroup() for group {} and user {}", groupName, name, e);
807                 }
808             }
809         });
810     }
811 }