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.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         String realmName = getRealmName();
319         final Node startNode;
320 
321         // the all realm searches the repository
322         if (Realm.REALM_ALL.getName().equals(realmName)) {
323             startNode = session.getRootNode();
324         } else {
325             startNode = session.getNode("/" + realmName);
326         }
327 
328         return findPrincipalNode(name, session, NodeTypes.User.NAME, startNode);
329     }
330 
331     protected User getFromRepository(String name) throws RepositoryException {
332         final Content node = findUserNode(this.realmName, name);
333         if (node == null) {
334             log.debug("User not found: [{}]", name);
335             return null;
336         }
337 
338         return newUserInstance(node);
339     }
340 
341     /**
342      * SystemUserManager does this.
343      */
344     @Override
345     public User getSystemUser() throws UnsupportedOperationException {
346         throw new UnsupportedOperationException();
347     }
348 
349     /**
350      * SystemUserManager does this.
351      */
352     @Override
353     public User getAnonymousUser() throws UnsupportedOperationException {
354         throw new UnsupportedOperationException();
355     }
356 
357     /**
358      * Get all users managed by this user manager.
359      */
360     @Override
361     public Collection<User> getAllUsers() {
362         return MgnlContext.doInSystemContext(new SilentSessionOp<Collection<User>>(getRepositoryName()) {
363 
364             @Override
365             public Collection<User> doExec(Session session) throws RepositoryException {
366                 List<User> users = new ArrayList<User>();
367                 Node node = session.getNode("/" + realmName);
368                 findAllUsersInFolder(node, users);
369                 return users;
370             }
371 
372             @Override
373             public String toString() {
374                 return "get all users";
375             }
376 
377         });
378     }
379 
380     /**
381      * @deprecated since 5.2 use findAllUsersInFolder instead
382      */
383     @Deprecated
384     public void updateUserListWithAllChildren(Node node, Collection<User> users) throws RepositoryException {
385         findAllUsersInFolder(node, users);
386     }
387 
388     /**
389      * Finds all users located in the provided node or in sub-folders within it.
390      */
391     public void findAllUsersInFolder(Node node, Collection<User> users) throws RepositoryException {
392         NodeIterator nodesIter = node.getNodes();
393         Collection<Node> nodes = new HashSet<Node>();
394         Collection<Node> folders = new HashSet<Node>();
395         while (nodesIter.hasNext()) {
396             Node newNode = (Node) nodesIter.next();
397             if (newNode.isNodeType(NodeTypes.User.NAME)) {
398                 nodes.add(newNode);
399             } else if (newNode.isNodeType(NodeTypes.Folder.NAME)) {
400                 folders.add(newNode);
401             }
402         }
403 
404         if (!nodes.isEmpty()) {
405             for (Node userNode : nodes) {
406                 users.add(newUserInstance(userNode));
407             }
408         }
409         if (!folders.isEmpty()) {
410             for (Node folder : folders) {
411                 findAllUsersInFolder(folder, users);
412             }
413         }
414     }
415 
416     @Override
417     public User createUser(final String name, final String pw) {
418         return this.createUser(null, name, pw);
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 = createUserNode(uPath, name, session);
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 = findPrincipalNode(user.getName(), session);
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     /**
510      * @deprecated since 5.2.8 use {@link #createUserNode(String, String, javax.jcr.Session)} instead
511      */
512     protected Content createUserNode(String name) throws RepositoryException {
513         final String path = "/" + getRealmName();
514         final String userName = name;
515         Node userNode = MgnlContext.doInSystemContext(new SilentSessionOp<Node>(getRepositoryName()) {
516             @Override
517             public Node doExec(Session session) throws RepositoryException {
518                 return createUserNode(path, userName, session);
519             }
520         });
521         return ContentUtil.asContent(userNode);
522 
523     }
524 
525     protected Node createUserNode(String path, String userName, Session session) throws RepositoryException {
526         return session.getNode(path).addNode(userName, NodeTypes.User.NAME);
527     }
528 
529     /**
530      * Return the HierarchyManager for the user workspace (through the system context).
531      * @deprecated since 5.2.8 without replacement
532      */
533     protected HierarchyManager getHierarchyManager() {
534         return MgnlContext.getSystemContext().getHierarchyManager(RepositoryConstants.USERS);
535     }
536 
537     /**
538      * Creates a {@link MgnlUser} out of a jcr node. Can be overridden in order to provide a different implementation.
539      * 
540      * @since 4.3.1
541      * @deprecated since 4.5 use newUSerInstance(javax.jcr.Node) instead
542      */
543     @Deprecated
544     protected User newUserInstance(Content node) {
545         try {
546             return newUserInstance(node.getJCRNode());
547         } catch (RepositoryException e) {
548             log.error(e.getMessage(), e);
549             return null;
550         }
551     }
552 
553     private Node addWrite(String parentPath, String property, Node acls) throws PathNotFoundException, RepositoryException, AccessDeniedException {
554         Node acl = acls.addNode(Path.getUniqueLabel(acls.getSession(), acls.getPath(), "0"), NodeTypes.ContentNode.NAME);
555         acl.setProperty("path", parentPath + "/" + property);
556         acl.setProperty("permissions", Permission.ALL);
557         return acl;
558     }
559 
560     @Override
561     public void updateLastAccessTimestamp(final User user) throws UnsupportedOperationException {
562         final String workspaceName = getRepositoryName();
563         try {
564             MgnlContext.doInSystemContext(new MgnlContext.LockingOp(workspaceName, ((MgnlUser) user).getPath()) {
565 
566                 @Override
567                 public void doExec() throws RepositoryException {
568                     Session session = MgnlContext.getJCRSession(workspaceName);
569                     String path = ((MgnlUser) user).getPath();
570                     log.debug("update access timestamp for {}", user.getName());
571                     try {
572                         Node userNode = session.getNode(path);
573                         PropertyUtil.updateOrCreate(userNode, "lastaccess", new GregorianCalendar());
574                         session.save();
575                     }
576                     catch (RepositoryException e) {
577                         session.refresh(false);
578                     }
579                     return;
580                 }
581             });
582         } catch (LockException e) {
583             log.debug("Failed to lock node for last access timestamp update for user " + user.getName() + " with " + e.getMessage(), e);
584         } catch (RepositoryException e) {
585             log.error("Failed to update user " + user.getName() + " last access time stamp with " + e.getMessage(), e);
586         }
587     }
588 
589     protected User newUserInstance(Node privilegedUserNode) throws ValueFormatException, PathNotFoundException, RepositoryException {
590         if (privilegedUserNode == null) {
591             return null;
592         }
593         Set<String> roles = collectUniquePropertyNames(privilegedUserNode, "roles", RepositoryConstants.USER_ROLES, false);
594         Set<String> groups = collectUniquePropertyNames(privilegedUserNode, "groups", RepositoryConstants.USER_GROUPS, false);
595 
596         Map<String, String> properties = new HashMap<String, String>();
597         for (PropertyIterator iter = new FilteringPropertyIterator(privilegedUserNode.getProperties(), NodeUtil.ALL_PROPERTIES_EXCEPT_JCR_AND_MGNL_FILTER); iter.hasNext();) {
598             Property prop = iter.nextProperty();
599             // TODO: should we check and skip binary props in case someone adds image to the user?
600             properties.put(prop.getName(), prop.getString());
601         }
602 
603         MgnlUser user = new MgnlUser(privilegedUserNode.getName(), getRealmName(), groups, roles, properties, privilegedUserNode.getPath(), privilegedUserNode.getIdentifier());
604         return user;
605     }
606 
607     @Override
608     protected String getRepositoryName() {
609         return RepositoryConstants.USERS;
610     }
611 
612     /**
613      * Sets access control list from a list of roles under the provided content object.
614      */
615     @Override
616     public Map<String, ACL> getACLs(final User user) {
617         if (!(user instanceof MgnlUser)) {
618             return null;
619         }
620         return super.getACLs(user.getName());
621     }
622 
623     @Override
624     public User addRole(User user, String roleName) {
625         try {
626             super.add(user.getName(), roleName, NODE_ROLES);
627         } catch (PrincipalNotFoundException e) {
628             // user doesn't exist in this UM
629             return null;
630         }
631         return getUser(user.getName());
632     }
633 
634     /**
635      * Collects all property names of given type, sorting them (case insensitive) and removing duplicates in the process.
636      */
637     private Set<String> collectUniquePropertyNames(Node rootNode, String subnodeName, String repositoryName, boolean isDeep) {
638         final SortedSet<String> set = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
639         String path = null;
640         try {
641             path = rootNode.getPath();
642             final Node node = rootNode.getNode(subnodeName);
643             collectUniquePropertyNames(node, repositoryName, subnodeName, set, isDeep);
644             collectUniquePropertyNames(rootNode.getNode(subnodeName), repositoryName, subnodeName, set, isDeep);
645         } catch (PathNotFoundException e) {
646             log.debug("{} does not have any {}", path, repositoryName);
647         } catch (Throwable t) {
648             log.error("Failed to read " + path + " or sub node " + subnodeName + " in repository " + repositoryName, t);
649         }
650         return set;
651     }
652 
653     private void collectUniquePropertyNames(final Node node, final String repositoryName, final String subnodeName, final Collection<String> set, final boolean isDeep) throws RepositoryException {
654         MgnlContext.doInSystemContext(new JCRSessionOp<Void>(repositoryName) {
655 
656             @Override
657             public Void exec(Session session) throws RepositoryException {
658                 for (PropertyIterator iter = new FilteringPropertyIterator(node.getProperties(), NodeUtil.ALL_PROPERTIES_EXCEPT_JCR_AND_MGNL_FILTER); iter.hasNext();) {
659                     Property property = iter.nextProperty();
660                     final String uuid = property.getString();
661                     try {
662                         final Node targetNode = session.getNodeByIdentifier(uuid);
663                         set.add(targetNode.getName());
664                         if (isDeep && targetNode.hasNode(subnodeName)) {
665                             collectUniquePropertyNames(targetNode.getNode(subnodeName), repositoryName, subnodeName, set, true);
666                         }
667                     } catch (ItemNotFoundException t) {
668                         final String path = property.getPath();
669                         // TODO: why we are using UUIDs here? shouldn't be better to use group names, since uuids can change???
670                         log.warn("Can't find {} node by UUID {} referred by node {}", new Object[] { repositoryName, t.getMessage(), path });
671                         log.debug("Failed while reading node by UUID", t);
672                         // we continue since it can happen that target node is removed
673                         // - UUID's are kept as simple strings thus have no referential integrity
674                     }
675                 }
676                 return null;
677             }
678         });
679     }
680 
681     @Override
682     public User addGroup(User user, String groupName) {
683         try {
684             super.add(user.getName(), groupName, NODE_GROUPS);
685         } catch (PrincipalNotFoundException e) {
686             // user doesn't exist in this UM
687             return null;
688         }
689         return getUser(user.getName());
690     }
691 
692     @Override
693     public User removeGroup(User user, String groupName) {
694         try {
695             super.remove(user.getName(), groupName, NODE_GROUPS);
696         } catch (PrincipalNotFoundException e) {
697             // user doesn't exist in this UM
698             return null;
699         }
700         return getUser(user.getName());
701     }
702 
703     @Override
704     public User removeRole(User user, String roleName) {
705         try {
706             super.remove(user.getName(), roleName, NODE_ROLES);
707         } catch (PrincipalNotFoundException e) {
708             // user doesn't exist in this UM
709             return null;
710         }
711         return getUser(user.getName());
712     }
713 }