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