View Javadoc
1   /**
2    * This file Copyright (c) 2003-2017 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
32   *
33   */
34  package info.magnolia.cms.security;
35  
36  import static info.magnolia.cms.security.SecurityConstants.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                 if (userNode != null) {
432                     setPasswordProperty(userNode, newPassword);
433 
434                     return newUserInstance(userNode);
435                 }
436                 return null;
437             }
438 
439             @Override
440             public String toString() {
441                 return "change password of user " + user.getName();
442             }
443         });
444     }
445 
446     protected void setPasswordProperty(Node userNode, String clearPassword) throws RepositoryException {
447         userNode.setProperty(PROPERTY_PASSWORD, encodePassword(clearPassword));
448         userNode.getSession().save();
449     }
450 
451     protected String encodePassword(String clearPassword) {
452         return SecurityUtil.getBCrypt(clearPassword);
453     }
454 
455     protected void validateUsername(String name) {
456         if (StringUtils.isBlank(name)) {
457             throw new IllegalArgumentException(name + " is not a valid username.");
458         }
459 
460         User user;
461         if (isAllowCrossRealmDuplicateNames()) {
462             user = this.getUser(name);
463         } else {
464             user = Security.getUserManager().getUser(name);
465         }
466         if (user != null) {
467             throw new IllegalArgumentException("User with name " + name + " already exists.");
468         }
469     }
470 
471     /**
472      * @deprecated since 5.3.2 use {@link #createUserNode(String, String, javax.jcr.Session)} instead
473      */
474     @Deprecated
475     protected Content createUserNode(String name) throws RepositoryException {
476         final String path = "/" + getRealmName();
477         final String userName = name;
478         Node userNode = MgnlContext.doInSystemContext(new SilentSessionOp<Node>(getRepositoryName()) {
479             @Override
480             public Node doExec(Session session) throws RepositoryException {
481                 return createUserNode(path, userName, session);
482             }
483 
484             @Override
485             public String toString() {
486                 return getClass().getName() + " createUSerNode(name)";
487             }
488         });
489         return ContentUtil.asContent(userNode);
490 
491     }
492 
493     protected Node createUserNode(String path, String userName, Session session) throws RepositoryException {
494         return session.getNode(path).addNode(userName, NodeTypes.User.NAME);
495     }
496 
497     /**
498      * Return the HierarchyManager for the user workspace (through the system context).
499      *
500      * @deprecated since 5.3.2 without replacement
501      */
502     @Deprecated
503     protected HierarchyManager getHierarchyManager() {
504         return MgnlContext.getSystemContext().getHierarchyManager(RepositoryConstants.USERS);
505     }
506 
507     private Node addWrite(String parentPath, String property, Node acls) throws PathNotFoundException, RepositoryException, AccessDeniedException {
508         Node acl = acls.addNode(Path.getUniqueLabel(acls.getSession(), acls.getPath(), "0"), NodeTypes.ContentNode.NAME);
509         acl.setProperty("path", parentPath + "/" + property);
510         acl.setProperty("permissions", Permission.ALL);
511         return acl;
512     }
513 
514     @Override
515     public void updateLastAccessTimestamp(final User user) throws UnsupportedOperationException {
516         final String workspaceName = getRepositoryName();
517         try {
518             MgnlContext.doInSystemContext(new MgnlContext.LockingOp(workspaceName, ((MgnlUser) user).getPath()) {
519 
520                 @Override
521                 public void doExec() throws RepositoryException {
522                     Session session = MgnlContext.getJCRSession(workspaceName);
523                     String path = ((MgnlUser) user).getPath();
524                     log.debug("update access timestamp for {}", user.getName());
525                     try {
526                         Node userNode = session.getNode(path);
527                         userNode = NodeUtil.deepUnwrap(userNode, MgnlPropertySettingNodeWrapper.class);
528                         PropertyUtil.updateOrCreate(userNode, "lastaccess", new GregorianCalendar());
529                         session.save();
530                     } catch (RepositoryException e) {
531                         session.refresh(false);
532                     }
533                     return;
534                 }
535 
536                 @Override
537                 public String toString() {
538                     return getClass().getName() + " updateLastAccessTimestamp(user)";
539                 }
540             });
541         } catch (LockException e) {
542             log.debug("Failed to lock node for last access timestamp update for user {} with {}", user.getName(), e.getMessage(), e);
543         } catch (RepositoryException e) {
544             log.error("Failed to update user {} last access time stamp with {}", user.getName(), e.getMessage(), e);
545         }
546     }
547 
548     protected User newUserInstance(Node privilegedUserNode) throws ValueFormatException, PathNotFoundException, RepositoryException {
549         if (privilegedUserNode == null) {
550             return null;
551         }
552         Set<String> roles = collectUniquePropertyNames(privilegedUserNode, ROLES_NODE_NAME, RepositoryConstants.USER_ROLES, false);
553         Set<String> groups = collectUniquePropertyNames(privilegedUserNode, GROUPS_NODE_NAME, RepositoryConstants.USER_GROUPS, false);
554 
555         Map<String, String> properties = new HashMap<String, String>();
556         for (PropertyIterator iter = new FilteringPropertyIterator(privilegedUserNode.getProperties(), new JCRMgnlPropertyHidingPredicate()); iter.hasNext();) {
557             Property prop = iter.nextProperty();
558             // TODO: should we check and skip binary props in case someone adds image to the user?
559             properties.put(prop.getName(), prop.getString());
560         }
561 
562         MgnlUser user = new MgnlUser(privilegedUserNode.getName(), getRealmName(), groups, roles, properties, privilegedUserNode.getPath(), privilegedUserNode.getIdentifier());
563         return user;
564     }
565 
566     @Override
567     protected String getRepositoryName() {
568         return RepositoryConstants.USERS;
569     }
570 
571     /**
572      * Sets access control list from a list of roles under the provided content object.
573      */
574     @Override
575     public Map<String, ACL> getACLs(final User user) {
576         if (!(user instanceof MgnlUser)) {
577             return null;
578         }
579         return super.getACLs(user.getName());
580     }
581 
582     @Override
583     public User addRole(User user, String roleName) {
584         try {
585             super.add(user.getName(), roleName, NODE_ROLES);
586         } catch (PrincipalNotFoundException e) {
587             // user doesn't exist in this UM
588             return null;
589         }
590         return getUser(user.getName());
591     }
592 
593     /**
594      * Collects all property names of given type, sorting them (case insensitive) and removing duplicates in the process.
595      */
596     private Set<String> collectUniquePropertyNames(Node rootNode, String subnodeName, String repositoryName, boolean isDeep) {
597         final SortedSet<String> set = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
598         String path = null;
599         try {
600             path = rootNode.getPath();
601             final Node node = rootNode.getNode(subnodeName);
602             collectUniquePropertyNames(node, repositoryName, subnodeName, set, isDeep);
603             collectUniquePropertyNames(rootNode.getNode(subnodeName), repositoryName, subnodeName, set, isDeep);
604         } catch (PathNotFoundException e) {
605             log.debug("{} does not have any {}", path, repositoryName);
606         } catch (Throwable t) {
607             log.error("Failed to read {} or sub node {} in repository {}", path, subnodeName, repositoryName, t);
608         }
609         return set;
610     }
611 
612     private void collectUniquePropertyNames(final Node node, final String repositoryName, final String subnodeName, final Collection<String> set, final boolean isDeep) throws RepositoryException {
613         if (!MgnlContext.isSystemInstance()) {
614             if (log.isDebugEnabled()) {
615                 log.debug("Collecting user properties in user context. List might not include all properties. Please check the calling code (see stacktrace)", new Exception());
616             } else {
617                 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)");
618             }
619         }
620         Session session = MgnlContext.getJCRSession(repositoryName);
621         for (PropertyIterator iter = new FilteringPropertyIterator(node.getProperties(), new JCRMgnlPropertyHidingPredicate()); iter.hasNext();) {
622             Property property = iter.nextProperty();
623             final String uuid = property.getString();
624             try {
625                 final Node targetNode = session.getNodeByIdentifier(uuid);
626                 set.add(targetNode.getName());
627                 if (isDeep && targetNode.hasNode(subnodeName)) {
628                     collectUniquePropertyNames(targetNode.getNode(subnodeName), repositoryName, subnodeName, set, true);
629                 }
630             } catch (ItemNotFoundException t) {
631                 final String path = property.getPath();
632                 // TODO: why we are using UUIDs here? shouldn't be better to use group names, since uuids can change???
633                 log.warn("Can't find {} node by UUID {} referred by node {}", repositoryName, t.getMessage(), path);
634                 log.debug("Failed while reading node by UUID", t);
635                 // we continue since it can happen that target node is removed
636                 // - UUID's are kept as simple strings thus have no referential integrity
637             }
638         }
639     }
640 
641     @Override
642     public User addGroup(User user, String groupName) {
643         try {
644             super.add(user.getName(), groupName, NODE_GROUPS);
645         } catch (PrincipalNotFoundException e) {
646             // user doesn't exist in this UM
647             return null;
648         }
649         return getUser(user.getName());
650     }
651 
652     @Override
653     public User removeGroup(User user, String groupName) {
654         try {
655             super.remove(user.getName(), groupName, NODE_GROUPS);
656         } catch (PrincipalNotFoundException e) {
657             // user doesn't exist in this UM
658             return null;
659         }
660         return getUser(user.getName());
661     }
662 
663     @Override
664     public User removeRole(User user, String roleName) {
665         try {
666             super.remove(user.getName(), roleName, NODE_ROLES);
667         } catch (PrincipalNotFoundException e) {
668             // user doesn't exist in this UM
669             return null;
670         }
671         return getUser(user.getName());
672     }
673 
674     @Override
675     public Collection<String> getUsersWithGroup(final String groupName) {
676         return MgnlContext.doInSystemContext(new SilentSessionOp<Collection<String>>(getRepositoryName()) {
677 
678             @Override
679             public Collection<String> doExec(Session session) throws RepositoryException {
680                 final Node groupNode = findPrincipalNode(groupName, MgnlContext.getJCRSession(RepositoryConstants.USER_GROUPS), NodeTypes.Group.NAME);
681                 return findUsersOrGroupsHavingAssignedGroupOrRoleWithUid(session, groupNode, GROUPS_NODE_NAME);
682             }
683 
684             @Override
685             public String toString() {
686                 return "get group " + groupName;
687             }
688         });
689     }
690 
691     @Override
692     public Collection<String> getUsersWithRole(final String roleName) {
693         return MgnlContext.doInSystemContext(new SilentSessionOp<Collection<String>>(getRepositoryName()) {
694 
695             @Override
696             public Collection<String> doExec(Session session) throws RepositoryException {
697                 final Node roleNode = findPrincipalNode(roleName, MgnlContext.getJCRSession(RepositoryConstants.USER_ROLES), NodeTypes.Role.NAME);
698                 return findUsersOrGroupsHavingAssignedGroupOrRoleWithUid(session, roleNode, ROLES_NODE_NAME);
699             }
700 
701             @Override
702             public String toString() {
703                 return "get role " + roleName;
704             }
705         });
706     }
707 
708     @Override
709     public Collection<String> getUsersWithGroup(String groupName, boolean transitive) {
710         if (!transitive) {
711             return getUsersWithGroup(groupName);
712         }
713 
714         Set<String> users = new HashSet<>();
715         // FYI: can't inject securitySupport or get static instance of SecuritySupport during the initialization phase.
716         GroupManager man = SecuritySupport.Factory.getInstance().getGroupManager();
717         Collection<String> groupNames = man.getAllSubGroups(groupName);
718         groupNames.add(groupName);
719         for (String transitiveGroup : groupNames) {
720             Collection<String> userNames = getUsersWithGroup(transitiveGroup);
721             users.addAll(userNames);
722         }
723         return users;
724     }
725 }