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                         }
148                         else {
149                             userNode.setProperty(propertyName, propertyValue);
150                             session.save();
151                         }
152                     }
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     }
164 
165     @Override
166     public User setProperty(final User user, final String propertyName, final String propertyValue) {
167         return MgnlContext.doInSystemContext(new SilentSessionOp<User>(getRepositoryName()) {
168 
169             @Override
170             public User doExec(Session session) throws RepositoryException {
171                 String path = ((MgnlUser) user).getPath();
172                 Node userNode;
173                 try {
174                     userNode = session.getNode(path);
175                     // setting value to null would remove existing properties anyway, so no need to create a
176                     // not-yet-existing-one first and then set it to null.
177                     if (propertyName != null) {
178                         if (StringUtils.equals(propertyName, PROPERTY_PASSWORD)) {
179                             setPasswordProperty(userNode, propertyValue);
180                         }
181                         else {
182                             userNode.setProperty(propertyName, propertyValue);
183                             session.save();
184                         }
185                     }
186                 } catch (RepositoryException e) {
187                     session.refresh(false);
188                     log.error("Property {} can't be changed. {}", propertyName, e.getMessage());
189                     return user;
190                 }
191                 return newUserInstance(userNode);
192             }
193         });
194     }
195 
196     /**
197      * TODO : rename to getRealmName and setRealmName (and make sure Content2Bean still sets realmName using the parent's node name).
198      * 
199      * @deprecated since 4.5 use realmName instead
200      */
201     @Deprecated
202     public String getName() {
203         return getRealmName();
204     }
205 
206     /**
207      * @deprecated since 4.5 use realmName instead
208      */
209     @Deprecated
210     public void setName(String name) {
211         setRealmName(name);
212     }
213 
214     public void setRealmName(String name) {
215         this.realmName = name;
216     }
217 
218     public String getRealmName() {
219         return realmName;
220     }
221 
222     public void setAllowCrossRealmDuplicateNames(boolean allowCrossRealmDuplicateNames) {
223         this.allowCrossRealmDuplicateNames = allowCrossRealmDuplicateNames;
224     }
225 
226     public boolean isAllowCrossRealmDuplicateNames() {
227         return allowCrossRealmDuplicateNames;
228     }
229 
230     /**
231      * Get the user object. Uses a search
232      * 
233      * @param name name of the user to retrieve
234      * @return the user object
235      */
236     @Override
237     public User getUser(final String name) {
238         try {
239             return MgnlContext.doInSystemContext(new JCRSessionOp<User>(getRepositoryName()) {
240                 @Override
241                 public User exec(Session session) throws RepositoryException {
242                     Node priviledgedUserNode = findPrincipalNode(name, session);
243                     return newUserInstance(priviledgedUserNode);
244                 }
245 
246                 @Override
247                 public String toString() {
248                     return "retrieve user " + name;
249                 }
250             });
251         } catch (RepositoryException e) {
252             log.error("Could not retrieve user with name: {}", name, e);
253         }
254         return null;
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      * 
305      * @deprecated since 4.5 use findPrincipalNode(java.lang.String, javax.jcr.Session) instead
306      */
307     @Deprecated
308     protected Content findUserNode(String realm, String name) throws RepositoryException {
309         // 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
310         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.");
311     }
312 
313     /**
314      * Helper method to find a user in a certain realm. Uses JCR Query.
315      * This will return null if user doesn't exist in realm.
316      */
317     @Override
318     protected Node findPrincipalNode(String name, Session session) throws RepositoryException {
319         final String realmName = getRealmName();
320         // the all realm searches the repository
321         final Node startNode = (Realm.REALM_ALL.getName().equals(realmName)) ? session.getRootNode() : session.getNode("/" + realmName);
322 
323         return findPrincipalNode(name, session, NodeTypes.User.NAME, startNode);
324     }
325 
326     protected User getFromRepository(String name) throws RepositoryException {
327         final Content node = findUserNode(this.realmName, name);
328         if (node == null) {
329             log.debug("User not found: [{}]", name);
330             return null;
331         }
332 
333         return newUserInstance(node);
334     }
335 
336     /**
337      * SystemUserManager does this.
338      */
339     @Override
340     public User getSystemUser() throws UnsupportedOperationException {
341         throw new UnsupportedOperationException();
342     }
343 
344     /**
345      * SystemUserManager does this.
346      */
347     @Override
348     public User getAnonymousUser() throws UnsupportedOperationException {
349         throw new UnsupportedOperationException();
350     }
351 
352     /**
353      * Get all users managed by this user manager.
354      */
355     @Override
356     public Collection<User> getAllUsers() {
357         return MgnlContext.doInSystemContext(new SilentSessionOp<Collection<User>>(getRepositoryName()) {
358 
359             @Override
360             public Collection<User> doExec(Session session) throws RepositoryException {
361                 List<User> users = new ArrayList<User>();
362                 Node node = session.getNode("/" + realmName);
363                 findAllUsersInFolder(node, users);
364                 return users;
365             }
366 
367             @Override
368             public String toString() {
369                 return "get all users";
370             }
371 
372         });
373     }
374 
375     /**
376      * @deprecated since 5.2 use findAllUsersInFolder instead
377      */
378     @Deprecated
379     public void updateUserListWithAllChildren(Node node, Collection<User> users) throws RepositoryException {
380         findAllUsersInFolder(node, users);
381     }
382 
383     /**
384      * Finds all users located in the provided node or in sub-folders within it and adds them to the given collection.
385      * 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).
386      */
387     public void findAllUsersInFolder(Node node, Collection<User> addTo) throws RepositoryException {
388         final NodeIterator nodesIter = findPrincipalNodes(node, NodeTypes.User.NAME);
389         while (nodesIter.hasNext()) {
390             addTo.add(newUserInstance(nodesIter.nextNode()));
391         }
392     }
393 
394     @Override
395     public User createUser(final String name, final String pw) {
396         return this.createUser(null, name, pw);
397     }
398     @Override
399     public User createUser(final String path, final String name, final String pw) throws UnsupportedOperationException {
400         validateUsername(name);
401         return MgnlContext.doInSystemContext(new SilentSessionOp<MgnlUser>(getRepositoryName()) {
402 
403             @Override
404             public MgnlUser doExec(Session session) throws RepositoryException {
405                 String uPath = path == null ? "/" + getRealmName() : path;
406                 Node userNode = createUserNode(uPath, name, session);
407                 userNode.addMixin(JcrConstants.MIX_LOCKABLE);
408                 userNode.setProperty("name", name);
409                 setPasswordProperty(userNode, pw);
410                 userNode.setProperty("language", "en");
411 
412                 final String handle = userNode.getPath();
413                 final Node acls = userNode.addNode(NODE_ACLUSERS, NodeTypes.ContentNode.NAME);
414                 // read only access to the node itself
415                 Node acl = acls.addNode(Path.getUniqueLabel(session, acls.getPath(), "0"), NodeTypes.ContentNode.NAME);
416                 acl.setProperty("path", handle);
417                 acl.setProperty("permissions", Permission.READ);
418                 // those who had access to their nodes should get access to their own props
419                 addWrite(handle, PROPERTY_EMAIL, acls);
420                 addWrite(handle, PROPERTY_LANGUAGE, acls);
421                 addWrite(handle, PROPERTY_LASTACCESS, acls);
422                 addWrite(handle, PROPERTY_PASSWORD, acls);
423                 addWrite(handle, PROPERTY_TITLE, acls);
424                 session.save();
425                 return new MgnlUser(userNode.getName(), getRealmName(), Collections.EMPTY_LIST, Collections.EMPTY_LIST, Collections.EMPTY_MAP, userNode.getPath(), userNode.getIdentifier());
426             }
427 
428             @Override
429             public String toString() {
430                 return "create user " + name;
431             }
432         });
433     }
434 
435     @Override
436     public User changePassword(final User user, final String newPassword) {
437         return MgnlContext.doInSystemContext(new SilentSessionOp<User>(getRepositoryName()) {
438 
439             @Override
440             public User doExec(Session session) throws RepositoryException {
441                 Node userNode = findPrincipalNode(user.getName(), session);
442                 setPasswordProperty(userNode, newPassword);
443 
444                 session.save();
445                 return newUserInstance(userNode);
446             }
447 
448             @Override
449             public String toString() {
450                 return "change password of user " + user.getName();
451             }
452         });
453     }
454 
455     /**
456      * @deprecated since 4.5 use {@link #setPasswordProperty(Node, String)} instead
457      */
458     @Deprecated
459     protected void setPasswordProperty(Content userNode, String clearPassword) throws RepositoryException {
460         setPasswordProperty(userNode.getJCRNode(), clearPassword);
461     }
462 
463     protected void setPasswordProperty(Node userNode, String clearPassword) throws RepositoryException {
464         userNode.setProperty(PROPERTY_PASSWORD, encodePassword(clearPassword));
465     }
466 
467     protected String encodePassword(String clearPassword) {
468         return SecurityUtil.getBCrypt(clearPassword);
469     }
470 
471     protected void validateUsername(String name) {
472         if (StringUtils.isBlank(name)) {
473             throw new IllegalArgumentException(name + " is not a valid username.");
474         }
475 
476         User user;
477         if (isAllowCrossRealmDuplicateNames()) {
478             user = this.getUser(name);
479         } else {
480             user = Security.getUserManager().getUser(name);
481         }
482         if (user != null) {
483             throw new IllegalArgumentException("User with name " + name + " already exists.");
484         }
485     }
486 
487     /**
488      * @deprecated since 5.3.2 use {@link #createUserNode(String, String, javax.jcr.Session)} instead
489      */
490     protected Content createUserNode(String name) throws RepositoryException {
491         final String path = "/" + getRealmName();
492         final String userName = name;
493         Node userNode = MgnlContext.doInSystemContext(new SilentSessionOp<Node>(getRepositoryName()) {
494             @Override
495             public Node doExec(Session session) throws RepositoryException {
496                 return createUserNode(path, userName, session);
497             }
498         });
499         return ContentUtil.asContent(userNode);
500 
501     }
502 
503     protected Node createUserNode(String path, String userName, Session session) throws RepositoryException {
504         return session.getNode(path).addNode(userName, NodeTypes.User.NAME);
505     }
506 
507     /**
508      * Return the HierarchyManager for the user workspace (through the system context).
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                     }
555                     catch (RepositoryException e) {
556                         session.refresh(false);
557                     }
558                     return;
559                 }
560             });
561         } catch (LockException e) {
562             log.debug("Failed to lock node for last access timestamp update for user {} with {}", user.getName(), e.getMessage(), e);
563         } catch (RepositoryException e) {
564             log.error("Failed to update user {} last access time stamp with {}", user.getName(), e.getMessage(), e);
565         }
566     }
567 
568     protected User newUserInstance(Node privilegedUserNode) throws ValueFormatException, PathNotFoundException, RepositoryException {
569         if (privilegedUserNode == null) {
570             return null;
571         }
572         Set<String> roles = collectUniquePropertyNames(privilegedUserNode, ROLES_NODE_NAME, RepositoryConstants.USER_ROLES, false);
573         Set<String> groups = collectUniquePropertyNames(privilegedUserNode, GROUPS_NODE_NAME, RepositoryConstants.USER_GROUPS, false);
574 
575         Map<String, String> properties = new HashMap<String, String>();
576         for (PropertyIterator iter = new FilteringPropertyIterator(privilegedUserNode.getProperties(), NodeUtil.ALL_PROPERTIES_EXCEPT_JCR_AND_MGNL_FILTER); iter.hasNext();) {
577             Property prop = iter.nextProperty();
578             // TODO: should we check and skip binary props in case someone adds image to the user?
579             properties.put(prop.getName(), prop.getString());
580         }
581 
582         MgnlUser user = new MgnlUser(privilegedUserNode.getName(), getRealmName(), groups, roles, properties, privilegedUserNode.getPath(), privilegedUserNode.getIdentifier());
583         return user;
584     }
585 
586     @Override
587     protected String getRepositoryName() {
588         return RepositoryConstants.USERS;
589     }
590 
591     /**
592      * Sets access control list from a list of roles under the provided content object.
593      */
594     @Override
595     public Map<String, ACL> getACLs(final User user) {
596         if (!(user instanceof MgnlUser)) {
597             return null;
598         }
599         return super.getACLs(user.getName());
600     }
601 
602     @Override
603     public User addRole(User user, String roleName) {
604         try {
605             super.add(user.getName(), roleName, NODE_ROLES);
606         } catch (PrincipalNotFoundException e) {
607             // user doesn't exist in this UM
608             return null;
609         }
610         return getUser(user.getName());
611     }
612 
613     /**
614      * Collects all property names of given type, sorting them (case insensitive) and removing duplicates in the process.
615      */
616     private Set<String> collectUniquePropertyNames(Node rootNode, String subnodeName, String repositoryName, boolean isDeep) {
617         final SortedSet<String> set = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
618         String path = null;
619         try {
620             path = rootNode.getPath();
621             final Node node = rootNode.getNode(subnodeName);
622             collectUniquePropertyNames(node, repositoryName, subnodeName, set, isDeep);
623             collectUniquePropertyNames(rootNode.getNode(subnodeName), repositoryName, subnodeName, set, isDeep);
624         } catch (PathNotFoundException e) {
625             log.debug("{} does not have any {}", path, repositoryName);
626         } catch (Throwable t) {
627             log.error("Failed to read {} or sub node {} in repository {}", path, subnodeName, repositoryName, t);
628         }
629         return set;
630     }
631 
632     private void collectUniquePropertyNames(final Node node, final String repositoryName, final String subnodeName, final Collection<String> set, final boolean isDeep) throws RepositoryException {
633         MgnlContext.doInSystemContext(new JCRSessionOp<Void>(repositoryName) {
634 
635             @Override
636             public Void exec(Session session) throws RepositoryException {
637                 for (PropertyIterator iter = new FilteringPropertyIterator(node.getProperties(), NodeUtil.ALL_PROPERTIES_EXCEPT_JCR_AND_MGNL_FILTER); iter.hasNext();) {
638                     Property property = iter.nextProperty();
639                     final String uuid = property.getString();
640                     try {
641                         final Node targetNode = session.getNodeByIdentifier(uuid);
642                         set.add(targetNode.getName());
643                         if (isDeep && targetNode.hasNode(subnodeName)) {
644                             collectUniquePropertyNames(targetNode.getNode(subnodeName), repositoryName, subnodeName, set, true);
645                         }
646                     } catch (ItemNotFoundException t) {
647                         final String path = property.getPath();
648                         // TODO: why we are using UUIDs here? shouldn't be better to use group names, since uuids can change???
649                         log.warn("Can't find {} node by UUID {} referred by node {}", repositoryName, t.getMessage(), path);
650                         log.debug("Failed while reading node by UUID", t);
651                         // we continue since it can happen that target node is removed
652                         // - UUID's are kept as simple strings thus have no referential integrity
653                     }
654                 }
655                 return null;
656             }
657         });
658     }
659 
660     @Override
661     public User addGroup(User user, String groupName) {
662         try {
663             super.add(user.getName(), groupName, NODE_GROUPS);
664         } catch (PrincipalNotFoundException e) {
665             // user doesn't exist in this UM
666             return null;
667         }
668         return getUser(user.getName());
669     }
670 
671     @Override
672     public User removeGroup(User user, String groupName) {
673         try {
674             super.remove(user.getName(), groupName, NODE_GROUPS);
675         } catch (PrincipalNotFoundException e) {
676             // user doesn't exist in this UM
677             return null;
678         }
679         return getUser(user.getName());
680     }
681 
682     @Override
683     public User removeRole(User user, String roleName) {
684         try {
685             super.remove(user.getName(), roleName, NODE_ROLES);
686         } catch (PrincipalNotFoundException e) {
687             // user doesn't exist in this UM
688             return null;
689         }
690         return getUser(user.getName());
691     }
692 
693     @Override
694     public Collection<String> getUsersWithGroup(final String groupName) {
695         return MgnlContext.doInSystemContext(new SilentSessionOp<Collection<String>>(getRepositoryName()) {
696 
697             @Override
698             public Collection<String> doExec(Session session) throws RepositoryException {
699                 final Node groupNode = findPrincipalNode(groupName, MgnlContext.getJCRSession(RepositoryConstants.USER_GROUPS), NodeTypes.Group.NAME);
700                 return findUsersOrGroupsHavingAssignedGroupOrRoleWithUid(session, groupNode, GROUPS_NODE_NAME);
701             }
702 
703             @Override
704             public String toString() {
705                 return "get group " + groupName;
706             }
707         });
708     }
709 
710     @Override
711     public Collection<String> getUsersWithRole(final String roleName) {
712         return MgnlContext.doInSystemContext(new SilentSessionOp<Collection<String>>(getRepositoryName()) {
713 
714             @Override
715             public Collection<String> doExec(Session session) throws RepositoryException {
716                 final Node roleNode = findPrincipalNode(roleName, MgnlContext.getJCRSession(RepositoryConstants.USER_ROLES), NodeTypes.Role.NAME);
717                 return findUsersOrGroupsHavingAssignedGroupOrRoleWithUid(session, roleNode, ROLES_NODE_NAME);
718             }
719 
720             @Override
721             public String toString() {
722                 return "get role " + roleName;
723             }
724         });
725     }
726 
727     @Override
728     public Collection<String> getUsersWithGroup(String groupName, boolean transitive) {
729         if (!transitive) {
730             return getUsersWithGroup(groupName);
731         }
732 
733         Set<String> users = new HashSet<String>();
734         MgnlGroupManager mgnlGroupManager = new MgnlGroupManager();
735         Collection<String> groupNames = mgnlGroupManager.getAllGroups(groupName);
736         groupNames.add(groupName);
737         for (String transitiveGroup : groupNames) {
738             Collection<String> userNames = getUsersWithGroup(transitiveGroup);
739             users.addAll(userNames);
740         }
741         return users;
742     }
743 
744 }