View Javadoc

1   /**
2    * This file Copyright (c) 2003-2013 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.context.MgnlContext;
43  import info.magnolia.jcr.iterator.FilteringPropertyIterator;
44  import info.magnolia.jcr.util.NodeTypes;
45  import info.magnolia.jcr.util.NodeUtil;
46  import info.magnolia.jcr.util.PropertyUtil;
47  import info.magnolia.repository.RepositoryConstants;
48  
49  import java.util.ArrayList;
50  import java.util.Collection;
51  import java.util.Collections;
52  import java.util.GregorianCalendar;
53  import java.util.HashMap;
54  import java.util.HashSet;
55  import java.util.Iterator;
56  import java.util.List;
57  import java.util.Map;
58  import java.util.Set;
59  import java.util.SortedSet;
60  import java.util.TreeSet;
61  
62  import javax.jcr.ItemNotFoundException;
63  import javax.jcr.Node;
64  import javax.jcr.NodeIterator;
65  import javax.jcr.PathNotFoundException;
66  import javax.jcr.Property;
67  import javax.jcr.PropertyIterator;
68  import javax.jcr.RepositoryException;
69  import javax.jcr.Session;
70  import javax.jcr.Value;
71  import javax.jcr.ValueFormatException;
72  import javax.jcr.lock.LockException;
73  import javax.security.auth.Subject;
74  
75  import org.apache.commons.lang.StringUtils;
76  import org.apache.jackrabbit.JcrConstants;
77  import org.slf4j.Logger;
78  import org.slf4j.LoggerFactory;
79  
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. " + e.getMessage(), propertyName);
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. " + e.getMessage(), propertyName);
188                     return user;
189                 }
190                 return newUserInstance(userNode);
191             }
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      * @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      * @param name name of the user to retrieve
232      * @return the user object
233      */
234     @Override
235     public User getUser(final String name) {
236         try {
237             return MgnlContext.doInSystemContext(new JCRSessionOp<User>(getRepositoryName()) {
238                 @Override
239                 public User exec(Session session) throws RepositoryException {
240                     Node priviledgedUserNode = findPrincipalNode(name, session);
241                     return newUserInstance(priviledgedUserNode);
242                 }
243                 @Override
244                 public String toString() {
245                     return "retrieve user " + name;
246                 }
247             });
248         } catch (RepositoryException e) {
249             log.error("Could not retrieve user with name: " + name, e);
250         }
251         return null;
252     }
253 
254     /**
255      * Get the user object. Uses a search
256      * @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                 @Override
269                 public String toString() {
270                     return "retrieve user with id " + id;
271                 }
272             });
273         } catch (RepositoryException e) {
274             log.error("Could not retrieve user with id: " + id, e);
275         }
276         return null;
277     }
278 
279     @Override
280     public User getUser(Subject subject) throws UnsupportedOperationException {
281         // this could be the case if no one is logged in yet
282         if (subject == null) {
283             log.debug("subject not set.");
284             return new DummyUser();
285         }
286 
287         Set<User> principalSet = subject.getPrincipals(User.class);
288         Iterator<User> entityIterator = principalSet.iterator();
289         if (!entityIterator.hasNext()) {
290             // happens when JCR authentication module set to optional and user doesn't exist in magnolia
291             log.debug("user name not contained in principal set.");
292             return new DummyUser();
293         }
294         return entityIterator.next();
295     }
296 
297     /**
298      * Helper method to find a user in a certain realm. Uses JCR Query.
299      * @deprecated since 4.5 use findPrincipalNode(java.lang.String, javax.jcr.Session) instead
300      */
301     @Deprecated
302     protected Content findUserNode(String realm, String name) throws RepositoryException {
303         // 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
304         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.");
305     }
306 
307     /**
308      * Helper method to find a user in a certain realm. Uses JCR Query.
309      * This will return null if user doesn't exist in realm.
310      */
311     @Override
312     protected Node findPrincipalNode(String name, Session session) throws RepositoryException {
313         String realmName = getRealmName();
314         final Node startNode;
315 
316         // the all realm searches the repository
317         if (Realm.REALM_ALL.getName().equals(realmName)) {
318             startNode = session.getRootNode();
319         } else {
320             startNode = session.getNode("/" + realmName);
321         }
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.
385      *
386      * @throws RepositoryException
387      */
388     public void findAllUsersInFolder(Node node, Collection<User> users) throws RepositoryException{
389         NodeIterator nodesIter = node.getNodes();
390         Collection<Node> nodes = new HashSet<Node>();
391         Collection<Node> folders = new HashSet<Node>();
392         while(nodesIter.hasNext()){
393             Node newNode = (Node) nodesIter.next();
394             if(newNode.isNodeType(NodeTypes.User.NAME)){
395                 nodes.add(newNode);
396             }else if(newNode.isNodeType(NodeTypes.Folder.NAME)){
397                 folders.add(newNode);
398             }
399         }
400 
401         if(!nodes.isEmpty()){
402             for (Node userNode : nodes) {
403                 users.add(newUserInstance(userNode));
404             }
405         }
406         if(!folders.isEmpty()){
407             for (Node folder : folders) {
408                 findAllUsersInFolder(folder, users);
409             }
410         }
411     }
412 
413     @Override
414     public User createUser(final String name, final String pw) {
415         return this.createUser(null, name, pw);
416     }
417 
418     @Override
419     public User createUser(final String path, final String name, final String pw) throws UnsupportedOperationException {
420         validateUsername(name);
421         return MgnlContext.doInSystemContext(new SilentSessionOp<MgnlUser>(getRepositoryName()) {
422 
423             @Override
424             public MgnlUser doExec(Session session) throws RepositoryException {
425                 String uPath = path == null ? "/" + getRealmName() : path;
426                 Node userNode = session.getNode(uPath).addNode(name, NodeTypes.User.NAME);
427                 userNode.addMixin(JcrConstants.MIX_LOCKABLE);
428                 userNode.setProperty("name", name);
429                 setPasswordProperty(userNode, pw);
430                 userNode.setProperty("language", "en");
431 
432                 final String handle = userNode.getPath();
433                 final Node acls = userNode.addNode(NODE_ACLUSERS, NodeTypes.ContentNode.NAME);
434                 // read only access to the node itself
435                 Node acl = acls.addNode(Path.getUniqueLabel(session, acls.getPath(), "0"), NodeTypes.ContentNode.NAME);
436                 acl.setProperty("path", handle);
437                 acl.setProperty("permissions", Permission.READ);
438                 // those who had access to their nodes should get access to their own props
439                 addWrite(handle, PROPERTY_EMAIL, acls);
440                 addWrite(handle, PROPERTY_LANGUAGE, acls);
441                 addWrite(handle, PROPERTY_LASTACCESS, acls);
442                 addWrite(handle, PROPERTY_PASSWORD, acls);
443                 addWrite(handle, PROPERTY_TITLE, acls);
444                 session.save();
445                 return new MgnlUser(userNode.getName(), getRealmName(), Collections.EMPTY_LIST, Collections.EMPTY_LIST, Collections.EMPTY_MAP,  NodeUtil.getPathIfPossible(userNode), NodeUtil.getNodeIdentifierIfPossible(userNode));
446             }
447 
448             @Override
449             public String toString() {
450                 return "create user " + name;
451             }
452         });
453     }
454 
455     @Override
456     public User changePassword(final User user, final String newPassword) {
457         return MgnlContext.doInSystemContext(new SilentSessionOp<User>(getRepositoryName()) {
458 
459             @Override
460             public User doExec(Session session) throws RepositoryException {
461                 Node userNode = session.getNode("/" + getRealmName() + "/" + user.getName());
462                 setPasswordProperty(userNode, newPassword);
463 
464                 session.save();
465                 return newUserInstance(userNode);
466             }
467 
468             @Override
469             public String toString() {
470                 return "change password of user " + user.getName();
471             }
472         });
473     }
474 
475     /**
476      * @deprecated since 4.5 use {@link #setPasswordProperty(Node, String)} instead
477      */
478     @Deprecated
479     protected void setPasswordProperty(Content userNode, String clearPassword) throws RepositoryException {
480         setPasswordProperty(userNode.getJCRNode(), clearPassword);
481     }
482 
483 
484     protected void setPasswordProperty(Node userNode, String clearPassword) throws RepositoryException {
485         userNode.setProperty(PROPERTY_PASSWORD, encodePassword(clearPassword));
486     }
487 
488     protected String encodePassword(String clearPassword) {
489         return SecurityUtil.getBCrypt(clearPassword);
490     }
491 
492     protected void validateUsername(String name) {
493         if (StringUtils.isBlank(name)) {
494             throw new IllegalArgumentException(name + " is not a valid username.");
495         }
496 
497         User user;
498         if (isAllowCrossRealmDuplicateNames()) {
499             user = this.getUser(name);
500         } else {
501             user = Security.getUserManager().getUser(name);
502         }
503         if (user != null) {
504             throw new IllegalArgumentException("User with name " + name + " already exists.");
505         }
506     }
507 
508     protected Content createUserNode(String name) throws RepositoryException {
509         final String path = "/" + getRealmName();
510         return getHierarchyManager().createContent(path, name, NodeTypes.User.NAME);
511     }
512 
513     /**
514      * Return the HierarchyManager for the user workspace (through the system context).
515      */
516     protected HierarchyManager getHierarchyManager() {
517         return MgnlContext.getSystemContext().getHierarchyManager(RepositoryConstants.USERS);
518     }
519 
520     /**
521      * Creates a {@link MgnlUser} out of a jcr node. Can be overridden in order to provide a different implementation.
522      * @since 4.3.1
523      * @deprecated since 4.5 use newUSerInstance(javax.jcr.Node) instead
524      */
525     @Deprecated
526     protected User newUserInstance(Content node) {
527         try {
528             return newUserInstance(node.getJCRNode());
529         } catch (RepositoryException e) {
530             log.error(e.getMessage(), e);
531             return null;
532         }
533     }
534 
535     private Node addWrite(String parentPath, String property, Node acls) throws PathNotFoundException, RepositoryException, AccessDeniedException {
536         Node acl = acls.addNode(Path.getUniqueLabel(acls.getSession(), acls.getPath(), "0"), NodeTypes.ContentNode.NAME);
537         acl.setProperty("path", parentPath + "/" + property);
538         acl.setProperty("permissions", Permission.ALL);
539         return acl;
540     }
541 
542     @Override
543     public void updateLastAccessTimestamp(final User user) throws UnsupportedOperationException {
544         final String workspaceName = getRepositoryName();
545         try {
546             MgnlContext.doInSystemContext(new MgnlContext.LockingOp(workspaceName, ((MgnlUser) user).getPath()) {
547 
548             @Override
549                 public void doExec() throws RepositoryException {
550                     Session session = MgnlContext.getJCRSession(workspaceName);
551                 String path = ((MgnlUser) user).getPath();
552                 log.debug("update access timestamp for {}", user.getName());
553                 try {
554                     Node userNode = session.getNode(path);
555                     PropertyUtil.updateOrCreate(userNode, "lastaccess", new GregorianCalendar());
556                     session.save();
557                 }
558                 catch (RepositoryException e) {
559                     session.refresh(false);
560                 }
561                     return;
562             }
563         });
564         } catch (LockException e) {
565             log.debug("Failed to lock node for last access timestamp update for user " + user.getName() + " with " + e.getMessage(), e);
566         } catch (RepositoryException e) {
567             log.error("Failed to update user " + user.getName() + " last access time stamp with " + e.getMessage(), e);
568         }
569     }
570 
571     protected User newUserInstance(Node privilegedUserNode) throws ValueFormatException, PathNotFoundException, RepositoryException {
572         if (privilegedUserNode == null) {
573             return null;
574         }
575         Set<String> roles = collectUniquePropertyNames(privilegedUserNode, "roles", RepositoryConstants.USER_ROLES, false);
576         Set<String> groups = collectUniquePropertyNames(privilegedUserNode, "groups", RepositoryConstants.USER_GROUPS, false);
577 
578         Map<String, String> properties = new HashMap<String, String>();
579         for (PropertyIterator iter = new FilteringPropertyIterator(privilegedUserNode.getProperties(), NodeUtil.ALL_PROPERTIES_EXCEPT_JCR_AND_MGNL_FILTER); iter.hasNext();) {
580             Property prop = iter.nextProperty();
581             //TODO: should we check and skip binary props in case someone adds image to the user?
582             properties.put(prop.getName(), prop.getString());
583         }
584 
585         MgnlUser user = new MgnlUser(privilegedUserNode.getName(), getRealmName(), groups, roles, properties, NodeUtil.getPathIfPossible(privilegedUserNode), NodeUtil.getNodeIdentifierIfPossible(privilegedUserNode));
586         return user;
587     }
588 
589     @Override
590     protected String getRepositoryName() {
591         return RepositoryConstants.USERS;
592     }
593 
594     /**
595      * Sets access control list from a list of roles under the provided content object.
596      */
597     @Override
598     public Map<String, ACL> getACLs(final User user) {
599         if (!(user instanceof MgnlUser)) {
600             return null;
601         }
602         return super.getACLs(user.getName());
603     }
604 
605     @Override
606     public User addRole(User user, String roleName) {
607         try {
608             super.add(user.getName(), roleName, NODE_ROLES);
609         } catch (PrincipalNotFoundException e) {
610             // user doesn't exist in this UM
611             return null;
612         }
613         return getUser(user.getName());
614     }
615 
616     /**
617      * Collects all property names of given type, sorting them (case insensitive) and removing duplicates in the process.
618      */
619     private Set<String> collectUniquePropertyNames(Node rootNode, String subnodeName, String repositoryName, boolean isDeep) {
620         final SortedSet<String> set = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
621         String path = null;
622         try {
623             path = rootNode.getPath();
624             final Node node = rootNode.getNode(subnodeName);
625             collectUniquePropertyNames(node, repositoryName, subnodeName, set, isDeep);
626             collectUniquePropertyNames(rootNode.getNode(subnodeName), repositoryName, subnodeName, set, isDeep);
627         } catch (PathNotFoundException e) {
628             log.debug("{} does not have any {}", path, repositoryName);
629         } catch (Throwable t) {
630             log.error("Failed to read " + path + " or sub node " + subnodeName + " in repository " + repositoryName, t);
631         }
632         return set;
633     }
634 
635     private void collectUniquePropertyNames(final Node node, final String repositoryName, final String subnodeName, final Collection<String> set, final boolean isDeep) throws RepositoryException {
636         MgnlContext.doInSystemContext(new JCRSessionOp<Void>(repositoryName) {
637 
638             @Override
639             public Void exec(Session session) throws RepositoryException {
640                 for (PropertyIterator iter = new FilteringPropertyIterator(node.getProperties(), NodeUtil.ALL_PROPERTIES_EXCEPT_JCR_AND_MGNL_FILTER); iter.hasNext();) {
641                     Property property = iter.nextProperty();
642                     final String uuid = property.getString();
643                     try {
644                         final Node targetNode = session.getNodeByIdentifier(uuid);
645                         set.add(targetNode.getName());
646                         if (isDeep && targetNode.hasNode(subnodeName)) {
647                             collectUniquePropertyNames(targetNode.getNode(subnodeName), repositoryName, subnodeName, set, true);
648                         }
649                     } catch (ItemNotFoundException t) {
650                         final String path = property.getPath();
651                         // TODO: why we are using UUIDs here? shouldn't be better to use group names, since uuids can change???
652                         log.warn("Can't find {} node by UUID {} referred by node {}", new Object[]{repositoryName, t.getMessage(), path});
653                         log.debug("Failed while reading node by UUID", t);
654                         // we continue since it can happen that target node is removed
655                         // - UUID's are kept as simple strings thus have no referential integrity
656                     }
657                 }
658                 return null;
659             }
660         });
661     }
662 
663     @Override
664     public User addGroup(User user, String groupName) {
665         try {
666             super.add(user.getName(), groupName, NODE_GROUPS);
667         } catch (PrincipalNotFoundException e) {
668             // user doesn't exist in this UM
669             return null;
670         }
671         return getUser(user.getName());
672     }
673 
674     @Override
675     public User removeGroup(User user, String groupName) {
676         try {
677             super.remove(user.getName(), groupName, NODE_GROUPS);
678         } catch (PrincipalNotFoundException e) {
679             // user doesn't exist in this UM
680             return null;
681         }
682         return getUser(user.getName());
683     }
684 
685     @Override
686     public User removeRole(User user, String roleName) {
687         try {
688             super.remove(user.getName(), roleName, NODE_ROLES);
689         } catch (PrincipalNotFoundException e) {
690             // user doesn't exist in this UM
691             return null;
692         }
693         return getUser(user.getName());
694     }
695 }