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