View Javadoc

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