View Javadoc

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