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   * 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      * @throws RepositoryException
391      */
392     public void findAllUsersInFolder(Node node, Collection<User> users) throws RepositoryException {
393         NodeIterator nodesIter = node.getNodes();
394         Collection<Node> nodes = new HashSet<Node>();
395         Collection<Node> folders = new HashSet<Node>();
396         while (nodesIter.hasNext()) {
397             Node newNode = (Node) nodesIter.next();
398             if (newNode.isNodeType(NodeTypes.User.NAME)) {
399                 nodes.add(newNode);
400             } else if (newNode.isNodeType(NodeTypes.Folder.NAME)) {
401                 folders.add(newNode);
402             }
403         }
404 
405         if (!nodes.isEmpty()) {
406             for (Node userNode : nodes) {
407                 users.add(newUserInstance(userNode));
408             }
409         }
410         if (!folders.isEmpty()) {
411             for (Node folder : folders) {
412                 findAllUsersInFolder(folder, users);
413             }
414         }
415     }
416 
417     @Override
418     public User createUser(final String name, final String pw) {
419         return this.createUser(null, name, pw);
420     }
421 
422     @Override
423     public User createUser(final String path, final String name, final String pw) throws UnsupportedOperationException {
424         validateUsername(name);
425         return MgnlContext.doInSystemContext(new SilentSessionOp<MgnlUser>(getRepositoryName()) {
426 
427             @Override
428             public MgnlUser doExec(Session session) throws RepositoryException {
429                 String uPath = path == null ? "/" + getRealmName() : path;
430                 Node userNode = session.getNode(uPath).addNode(name, NodeTypes.User.NAME);
431                 userNode.addMixin(JcrConstants.MIX_LOCKABLE);
432                 userNode.setProperty("name", name);
433                 setPasswordProperty(userNode, pw);
434                 userNode.setProperty("language", "en");
435 
436                 final String handle = userNode.getPath();
437                 final Node acls = userNode.addNode(NODE_ACLUSERS, NodeTypes.ContentNode.NAME);
438                 // read only access to the node itself
439                 Node acl = acls.addNode(Path.getUniqueLabel(session, acls.getPath(), "0"), NodeTypes.ContentNode.NAME);
440                 acl.setProperty("path", handle);
441                 acl.setProperty("permissions", Permission.READ);
442                 // those who had access to their nodes should get access to their own props
443                 addWrite(handle, PROPERTY_EMAIL, acls);
444                 addWrite(handle, PROPERTY_LANGUAGE, acls);
445                 addWrite(handle, PROPERTY_LASTACCESS, acls);
446                 addWrite(handle, PROPERTY_PASSWORD, acls);
447                 addWrite(handle, PROPERTY_TITLE, acls);
448                 session.save();
449                 return new MgnlUser(userNode.getName(), getRealmName(), Collections.EMPTY_LIST, Collections.EMPTY_LIST, Collections.EMPTY_MAP, userNode.getPath(), userNode.getIdentifier());
450             }
451 
452             @Override
453             public String toString() {
454                 return "create user " + name;
455             }
456         });
457     }
458 
459     @Override
460     public User changePassword(final User user, final String newPassword) {
461         return MgnlContext.doInSystemContext(new SilentSessionOp<User>(getRepositoryName()) {
462 
463             @Override
464             public User doExec(Session session) throws RepositoryException {
465                 Node userNode = session.getNode("/" + getRealmName() + "/" + user.getName());
466                 setPasswordProperty(userNode, newPassword);
467 
468                 session.save();
469                 return newUserInstance(userNode);
470             }
471 
472             @Override
473             public String toString() {
474                 return "change password of user " + user.getName();
475             }
476         });
477     }
478 
479     /**
480      * @deprecated since 4.5 use {@link #setPasswordProperty(Node, String)} instead
481      */
482     @Deprecated
483     protected void setPasswordProperty(Content userNode, String clearPassword) throws RepositoryException {
484         setPasswordProperty(userNode.getJCRNode(), clearPassword);
485     }
486 
487     protected void setPasswordProperty(Node userNode, String clearPassword) throws RepositoryException {
488         userNode.setProperty(PROPERTY_PASSWORD, encodePassword(clearPassword));
489     }
490 
491     protected String encodePassword(String clearPassword) {
492         return SecurityUtil.getBCrypt(clearPassword);
493     }
494 
495     protected void validateUsername(String name) {
496         if (StringUtils.isBlank(name)) {
497             throw new IllegalArgumentException(name + " is not a valid username.");
498         }
499 
500         User user;
501         if (isAllowCrossRealmDuplicateNames()) {
502             user = this.getUser(name);
503         } else {
504             user = Security.getUserManager().getUser(name);
505         }
506         if (user != null) {
507             throw new IllegalArgumentException("User with name " + name + " already exists.");
508         }
509     }
510 
511     protected Content createUserNode(String name) throws RepositoryException {
512         final String path = "/" + getRealmName();
513         return getHierarchyManager().createContent(path, name, NodeTypes.User.NAME);
514     }
515 
516     /**
517      * Return the HierarchyManager for the user workspace (through the system context).
518      */
519     protected HierarchyManager getHierarchyManager() {
520         return MgnlContext.getSystemContext().getHierarchyManager(RepositoryConstants.USERS);
521     }
522 
523     /**
524      * Creates a {@link MgnlUser} out of a jcr node. Can be overridden in order to provide a different implementation.
525      * 
526      * @since 4.3.1
527      * @deprecated since 4.5 use newUSerInstance(javax.jcr.Node) instead
528      */
529     @Deprecated
530     protected User newUserInstance(Content node) {
531         try {
532             return newUserInstance(node.getJCRNode());
533         } catch (RepositoryException e) {
534             log.error(e.getMessage(), e);
535             return null;
536         }
537     }
538 
539     private Node addWrite(String parentPath, String property, Node acls) throws PathNotFoundException, RepositoryException, AccessDeniedException {
540         Node acl = acls.addNode(Path.getUniqueLabel(acls.getSession(), acls.getPath(), "0"), NodeTypes.ContentNode.NAME);
541         acl.setProperty("path", parentPath + "/" + property);
542         acl.setProperty("permissions", Permission.ALL);
543         return acl;
544     }
545 
546     @Override
547     public void updateLastAccessTimestamp(final User user) throws UnsupportedOperationException {
548         final String workspaceName = getRepositoryName();
549         try {
550             MgnlContext.doInSystemContext(new MgnlContext.LockingOp(workspaceName, ((MgnlUser) user).getPath()) {
551 
552                 @Override
553                 public void doExec() throws RepositoryException {
554                     Session session = MgnlContext.getJCRSession(workspaceName);
555                     String path = ((MgnlUser) user).getPath();
556                     log.debug("update access timestamp for {}", user.getName());
557                     try {
558                         Node userNode = session.getNode(path);
559                         PropertyUtil.updateOrCreate(userNode, "lastaccess", new GregorianCalendar());
560                         session.save();
561                     }
562                     catch (RepositoryException e) {
563                         session.refresh(false);
564                     }
565                     return;
566                 }
567             });
568         } catch (LockException e) {
569             log.debug("Failed to lock node for last access timestamp update for user " + user.getName() + " with " + e.getMessage(), e);
570         } catch (RepositoryException e) {
571             log.error("Failed to update user " + user.getName() + " last access time stamp with " + e.getMessage(), e);
572         }
573     }
574 
575     protected User newUserInstance(Node privilegedUserNode) throws ValueFormatException, PathNotFoundException, RepositoryException {
576         if (privilegedUserNode == null) {
577             return null;
578         }
579         Set<String> roles = collectUniquePropertyNames(privilegedUserNode, "roles", RepositoryConstants.USER_ROLES, false);
580         Set<String> groups = collectUniquePropertyNames(privilegedUserNode, "groups", RepositoryConstants.USER_GROUPS, false);
581 
582         Map<String, String> properties = new HashMap<String, String>();
583         for (PropertyIterator iter = new FilteringPropertyIterator(privilegedUserNode.getProperties(), NodeUtil.ALL_PROPERTIES_EXCEPT_JCR_AND_MGNL_FILTER); iter.hasNext();) {
584             Property prop = iter.nextProperty();
585             // TODO: should we check and skip binary props in case someone adds image to the user?
586             properties.put(prop.getName(), prop.getString());
587         }
588 
589         MgnlUser user = new MgnlUser(privilegedUserNode.getName(), getRealmName(), groups, roles, properties, privilegedUserNode.getPath(), privilegedUserNode.getIdentifier());
590         return user;
591     }
592 
593     @Override
594     protected String getRepositoryName() {
595         return RepositoryConstants.USERS;
596     }
597 
598     /**
599      * Sets access control list from a list of roles under the provided content object.
600      */
601     @Override
602     public Map<String, ACL> getACLs(final User user) {
603         if (!(user instanceof MgnlUser)) {
604             return null;
605         }
606         return super.getACLs(user.getName());
607     }
608 
609     @Override
610     public User addRole(User user, String roleName) {
611         try {
612             super.add(user.getName(), roleName, NODE_ROLES);
613         } catch (PrincipalNotFoundException e) {
614             // user doesn't exist in this UM
615             return null;
616         }
617         return getUser(user.getName());
618     }
619 
620     /**
621      * Collects all property names of given type, sorting them (case insensitive) and removing duplicates in the process.
622      */
623     private Set<String> collectUniquePropertyNames(Node rootNode, String subnodeName, String repositoryName, boolean isDeep) {
624         final SortedSet<String> set = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
625         String path = null;
626         try {
627             path = rootNode.getPath();
628             final Node node = rootNode.getNode(subnodeName);
629             collectUniquePropertyNames(node, repositoryName, subnodeName, set, isDeep);
630             collectUniquePropertyNames(rootNode.getNode(subnodeName), repositoryName, subnodeName, set, isDeep);
631         } catch (PathNotFoundException e) {
632             log.debug("{} does not have any {}", path, repositoryName);
633         } catch (Throwable t) {
634             log.error("Failed to read " + path + " or sub node " + subnodeName + " in repository " + repositoryName, t);
635         }
636         return set;
637     }
638 
639     private void collectUniquePropertyNames(final Node node, final String repositoryName, final String subnodeName, final Collection<String> set, final boolean isDeep) throws RepositoryException {
640         MgnlContext.doInSystemContext(new JCRSessionOp<Void>(repositoryName) {
641 
642             @Override
643             public Void exec(Session session) throws RepositoryException {
644                 for (PropertyIterator iter = new FilteringPropertyIterator(node.getProperties(), NodeUtil.ALL_PROPERTIES_EXCEPT_JCR_AND_MGNL_FILTER); iter.hasNext();) {
645                     Property property = iter.nextProperty();
646                     final String uuid = property.getString();
647                     try {
648                         final Node targetNode = session.getNodeByIdentifier(uuid);
649                         set.add(targetNode.getName());
650                         if (isDeep && targetNode.hasNode(subnodeName)) {
651                             collectUniquePropertyNames(targetNode.getNode(subnodeName), repositoryName, subnodeName, set, true);
652                         }
653                     } catch (ItemNotFoundException t) {
654                         final String path = property.getPath();
655                         // TODO: why we are using UUIDs here? shouldn't be better to use group names, since uuids can change???
656                         log.warn("Can't find {} node by UUID {} referred by node {}", new Object[] { repositoryName, t.getMessage(), path });
657                         log.debug("Failed while reading node by UUID", t);
658                         // we continue since it can happen that target node is removed
659                         // - UUID's are kept as simple strings thus have no referential integrity
660                     }
661                 }
662                 return null;
663             }
664         });
665     }
666 
667     @Override
668     public User addGroup(User user, String groupName) {
669         try {
670             super.add(user.getName(), groupName, NODE_GROUPS);
671         } catch (PrincipalNotFoundException e) {
672             // user doesn't exist in this UM
673             return null;
674         }
675         return getUser(user.getName());
676     }
677 
678     @Override
679     public User removeGroup(User user, String groupName) {
680         try {
681             super.remove(user.getName(), groupName, NODE_GROUPS);
682         } catch (PrincipalNotFoundException e) {
683             // user doesn't exist in this UM
684             return null;
685         }
686         return getUser(user.getName());
687     }
688 
689     @Override
690     public User removeRole(User user, String roleName) {
691         try {
692             super.remove(user.getName(), roleName, NODE_ROLES);
693         } catch (PrincipalNotFoundException e) {
694             // user doesn't exist in this UM
695             return null;
696         }
697         return getUser(user.getName());
698     }
699 }