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