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