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