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