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