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.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      * This will return null if user doesn't exist in realm.
322      */
323     @Override
324     protected Node findPrincipalNode(String name, Session session) throws RepositoryException {
325         String realmName = getRealmName();
326         final String where;
327         // the all realm searches the repository
328         if (Realm.REALM_ALL.getName().equals(realmName)) {
329             where = "where name() = '" + name + "'";
330         } else {
331             // FIXME: DOUBLE CHECK THE QUERY FOR REALMS ... ISDESCENDANTNODE and
332             // NAME ....
333             where = "where name() = '" + name + "' and isdescendantnode(['/" + realmName + "'])";
334             // where = "where [jcr:path] = '/" + realm + "/" + name + "'"
335             // + " or [jcr:path] like '/" + realm + "/%/" + name + "'";
336         }
337 
338         final String statement = "select * from [" + MgnlNodeType.USER + "] " + where;
339 
340         Query query = session.getWorkspace().getQueryManager().createQuery(statement, Query.JCR_SQL2);
341         NodeIterator iter = query.execute().getNodes();
342         Node user = null;
343         while (iter.hasNext()) {
344             Node node = iter.nextNode();
345             if (node.isNodeType(ItemType.USER.getSystemName())) {
346                 user = node;
347                 break;
348             }
349         }
350         if (iter.hasNext()) {
351             log.error("More than one user found with name [{}] in realm [{}]", name, realmName);
352         }
353         return user;
354     }
355 
356     protected User getFromRepository(String name) throws RepositoryException {
357         final Content node = findUserNode(this.realmName, name);
358         if (node == null) {
359             log.debug("User not found: [{}]", name);
360             return null;
361         }
362 
363         return newUserInstance(node);
364     }
365 
366     /**
367      * SystemUserManager does this.
368      */
369     @Override
370     public User getSystemUser() throws UnsupportedOperationException {
371         throw new UnsupportedOperationException();
372     }
373 
374     /**
375      * SystemUserManager does this.
376      */
377     @Override
378     public User getAnonymousUser() throws UnsupportedOperationException {
379         throw new UnsupportedOperationException();
380     }
381 
382     /**
383      * Get all users managed by this user manager.
384      */
385     @Override
386     public Collection<User> getAllUsers() {
387         return MgnlContext.doInSystemContext(new SilentSessionOp<Collection<User>>(getRepositoryName()) {
388 
389             @Override
390             public Collection<User> doExec(Session session) throws RepositoryException {
391                 List<User> users = new ArrayList<User>();
392                 Node node = session.getNode("/" + realmName);
393                 updateUserListWithAllChildren(node, users);
394                 return users;
395             }
396 
397             @Override
398             public String toString() {
399                 return "get all users";
400             }
401 
402         });
403     }
404 
405     /**
406      * Updates collection with all users located under provided node.
407      * 
408      * @throws RepositoryException
409      */
410     public void updateUserListWithAllChildren(Node node, Collection<User> users) throws RepositoryException {
411         NodeIterator nodesIter = node.getNodes();
412         Collection<Node> nodes = new HashSet<Node>();
413         Collection<Node> folders = new HashSet<Node>();
414         while (nodesIter.hasNext()) {
415             Node newNode = (Node) nodesIter.next();
416             if (newNode.isNodeType(MgnlNodeType.USER)) {
417                 nodes.add(newNode);
418             } else if (newNode.isNodeType(MgnlNodeType.NT_FOLDER)) {
419                 folders.add(newNode);
420             }
421         }
422 
423         if (!nodes.isEmpty()) {
424             for (Node userNode : nodes) {
425                 users.add(newUserInstance(userNode));
426             }
427         }
428         if (!folders.isEmpty()) {
429             Iterator<Node> it = folders.iterator();
430             while (it.hasNext()) {
431                 updateUserListWithAllChildren(it.next(), users);
432             }
433         }
434     }
435 
436     @Override
437     public User createUser(final String name, final String pw) {
438         return this.createUser(null, name, pw);
439     }
440 
441     @Override
442     public User createUser(final String path, final String name, final String pw) throws UnsupportedOperationException {
443         validateUsername(name);
444         return MgnlContext.doInSystemContext(new SilentSessionOp<MgnlUser>(getRepositoryName()) {
445 
446             @Override
447             public MgnlUser doExec(Session session) throws RepositoryException {
448                 String uPath = path == null ? "/" + getRealmName() : path;
449                 Node userNode = session.getNode(uPath).addNode(name, MgnlNodeType.USER);
450                 userNode.addMixin(MgnlNodeType.MIX_LOCKABLE);
451                 userNode.setProperty("name", name);
452                 setPasswordProperty(userNode, pw);
453                 userNode.setProperty("language", "en");
454 
455                 final String handle = userNode.getPath();
456                 final Node acls = userNode.addNode(NODE_ACLUSERS, MgnlNodeType.NT_CONTENTNODE);
457                 // read only access to the node itself
458                 Node acl = acls.addNode(Path.getUniqueLabel(session, acls.getPath(), "0"), MgnlNodeType.NT_CONTENTNODE);
459                 acl.setProperty("path", handle);
460                 acl.setProperty("permissions", Permission.READ);
461                 // those who had access to their nodes should get access to
462                 // their own props
463                 addWrite(handle, PROPERTY_EMAIL, acls);
464                 addWrite(handle, PROPERTY_LANGUAGE, acls);
465                 addWrite(handle, PROPERTY_LASTACCESS, acls);
466                 addWrite(handle, PROPERTY_PASSWORD, acls);
467                 addWrite(handle, PROPERTY_TITLE, acls);
468                 // and of course the meta data
469                 addWrite(handle, MetaData.DEFAULT_META_NODE + "/*", acls);
470 
471                 session.save();
472                 return new MgnlUser(userNode.getName(), getRealmName(), Collections.EMPTY_LIST, Collections.EMPTY_LIST, Collections.EMPTY_MAP,
473                         NodeUtil.getPathIfPossible(userNode), NodeUtil.getNodeIdentifierIfPossible(userNode));
474             }
475 
476             @Override
477             public String toString() {
478                 return "create user " + name;
479             }
480         });
481     }
482 
483     @Override
484     public User changePassword(final User user, final String newPassword) {
485         return MgnlContext.doInSystemContext(new SilentSessionOp<User>(getRepositoryName()) {
486 
487             @Override
488             public User doExec(Session session) throws RepositoryException {
489                 Node userNode = session.getNode("/" + getRealmName() + "/" + user.getName());
490                 setPasswordProperty(userNode, newPassword);
491 
492                 session.save();
493                 return newUserInstance(userNode);
494             }
495 
496             @Override
497             public String toString() {
498                 return "change password of user " + user.getName();
499             }
500         });
501     }
502 
503     /**
504      * @deprecated since 4.5 use {@link #setPasswordProperty(Node, String)}
505      *             instead
506      */
507     @Deprecated
508     protected void setPasswordProperty(Content userNode, String clearPassword) throws RepositoryException {
509         setPasswordProperty(userNode.getJCRNode(), clearPassword);
510     }
511 
512     protected void setPasswordProperty(Node userNode, String clearPassword) throws RepositoryException {
513         userNode.setProperty(PROPERTY_PASSWORD, encodePassword(clearPassword));
514     }
515 
516     protected String encodePassword(String clearPassword) {
517         return SecurityUtil.getBCrypt(clearPassword);
518     }
519 
520     protected void validateUsername(String name) {
521         if (StringUtils.isBlank(name)) {
522             throw new IllegalArgumentException(name + " is not a valid username.");
523         }
524         User user;
525         if(isAllowCrossRealmDuplicateNames()){
526             user = this.getUser(name);
527         } else {
528             user = Security.getUserManager().getUser(name);
529         }
530         if (user != null) {
531             throw new IllegalArgumentException("User with name " + name + " already exists.");
532         }
533     }
534 
535     protected Content createUserNode(String name) throws RepositoryException {
536         final String path = "/" + getRealmName();
537         return getHierarchyManager().createContent(path, name, ItemType.USER.getSystemName());
538     }
539 
540     /**
541      * Return the HierarchyManager for the user workspace (through the system
542      * context).
543      */
544     protected HierarchyManager getHierarchyManager() {
545         return MgnlContext.getSystemContext().getHierarchyManager(RepositoryConstants.USERS);
546     }
547 
548     /**
549      * @deprecated since 4.3.1 - use {@link #newUserInstance(javax.jcr.Node)}
550      */
551     @Deprecated
552     protected MgnlUser userInstance(Content node) {
553         try {
554             return (MgnlUser) newUserInstance(node.getJCRNode());
555         } catch (RepositoryException e) {
556             log.error(e.getMessage(), e);
557             return null;
558         }
559     }
560 
561     /**
562      * Creates a {@link MgnlUser} out of a jcr node. Can be overridden in order
563      * to provide a different implementation.
564      * 
565      * @since 4.3.1
566      * @deprecated since 4.5 use newUSerInstance(javax.jcr.Node) instead
567      */
568     @Deprecated
569     protected User newUserInstance(Content node) {
570         try {
571             return newUserInstance(node.getJCRNode());
572         } catch (RepositoryException e) {
573             log.error(e.getMessage(), e);
574             return null;
575         }
576     }
577 
578     private Node addWrite(String parentPath, String property, Node acls) throws PathNotFoundException, RepositoryException, AccessDeniedException {
579         Node acl = acls.addNode(Path.getUniqueLabel(acls.getSession(), acls.getPath(), "0"), ItemType.CONTENTNODE.getSystemName());
580         acl.setProperty("path", parentPath + "/" + property);
581         acl.setProperty("permissions", Permission.ALL);
582         return acl;
583     }
584 
585     @Override
586     public void updateLastAccessTimestamp(final User user) throws UnsupportedOperationException {
587         MgnlContext.doInSystemContext(new SilentSessionOp<Void>(getRepositoryName()) {
588 
589             @Override
590             public Void doExec(Session session) throws RepositoryException {
591                 String path = ((MgnlUser) user).getPath();
592                 log.debug("update access timestamp for {}", user.getName());
593                 try {
594                     Node userNode = session.getNode(path);
595                     PropertyUtil.updateOrCreate(userNode, "lastaccess", new GregorianCalendar());
596                     session.save();
597                 } catch (RepositoryException e) {
598                     session.refresh(false);
599                 }
600                 return null;
601             }
602 
603             @Override
604             public String toString() {
605                 return "update user " + user.getName() + " last access time stamp";
606             }
607         });
608     }
609 
610     protected User newUserInstance(Node privilegedUserNode) throws ValueFormatException, PathNotFoundException, RepositoryException {
611         if (privilegedUserNode == null) {
612             return null;
613         }
614         Set<String> roles = collectUniquePropertyNames(privilegedUserNode, "roles", RepositoryConstants.USER_ROLES, false);
615         Set<String> groups = collectUniquePropertyNames(privilegedUserNode, "groups", RepositoryConstants.USER_GROUPS, false);
616 
617         Map<String, String> properties = new HashMap<String, String>();
618         for (PropertyIterator iter = privilegedUserNode.getProperties(); iter.hasNext();) {
619             Property prop = iter.nextProperty();
620             if (prop.getName().startsWith(MgnlNodeType.JCR_PREFIX) || prop.getName().startsWith(MgnlNodeType.MGNL_PREFIX)) {
621                 // skip special props
622                 continue;
623             }
624             // TODO: should we check and skip binary props in case someone adds
625             // image to the user?
626             properties.put(prop.getName(), prop.getString());
627         }
628 
629         MgnlUser user = new MgnlUser(privilegedUserNode.getName(), getRealmName(), groups, roles, properties,
630                 NodeUtil.getPathIfPossible(privilegedUserNode), NodeUtil.getNodeIdentifierIfPossible(privilegedUserNode));
631         return user;
632     }
633 
634     @Override
635     protected String getRepositoryName() {
636         return RepositoryConstants.USERS;
637     }
638 
639     /**
640      * Sets access control list from a list of roles under the provided content
641      * object.
642      */
643     @Override
644     public Map<String, ACL> getACLs(final User user) {
645         if (!(user instanceof MgnlUser)) {
646             return null;
647         }
648         return super.getACLs(user.getName());
649     }
650 
651     @Override
652     public User addRole(User user, String roleName) {
653         try {
654             super.add(user.getName(), roleName, NODE_ROLES);
655         } catch (PrincipalNotFoundException e) {
656             // user doesn't exist in this UM
657             return null;
658         }
659         return getUser(user.getName());
660     }
661 
662     /**
663      * Collects all property names of given type, sorting them (case
664      * insensitive) and removing duplicates in the process.
665      */
666     private Set<String> collectUniquePropertyNames(Node rootNode, String subnodeName, String repositoryName, boolean isDeep) {
667         final SortedSet<String> set = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
668         String path = null;
669         try {
670             path = rootNode.getPath();
671             final Node node = rootNode.getNode(subnodeName);
672             collectUniquePropertyNames(node, repositoryName, subnodeName, set, isDeep);
673             collectUniquePropertyNames(rootNode.getNode(subnodeName), repositoryName, subnodeName, set, isDeep);
674         } catch (PathNotFoundException e) {
675             log.debug("{} does not have any {}", path, repositoryName);
676         } catch (Throwable t) {
677             log.error("Failed to read " + path + " or sub node " + subnodeName + " in repository " + repositoryName, t);
678         }
679         return set;
680     }
681 
682     private void collectUniquePropertyNames(final Node node, final String repositoryName, final String subnodeName, final Collection<String> set, final boolean isDeep) throws RepositoryException {
683         MgnlContext.doInSystemContext(new JCRSessionOp<Void>(repositoryName) {
684 
685             @Override
686             public Void exec(Session session) throws RepositoryException {
687                 for (PropertyIterator props = node.getProperties(); props.hasNext();) {
688                     Property property = props.nextProperty();
689                     if (property.getName().startsWith(MgnlNodeType.JCR_PREFIX)) {
690                         continue;
691                     }
692                     final String uuid = property.getString();
693                     try {
694                         final Node targetNode = session.getNodeByIdentifier(uuid);
695                         set.add(targetNode.getName());
696                         if (isDeep && targetNode.hasNode(subnodeName)) {
697                             collectUniquePropertyNames(targetNode.getNode(subnodeName), repositoryName, subnodeName, set, true);
698                         }
699                     } catch (ItemNotFoundException t) {
700                         final String path = property.getPath();
701                         // TODO: why we are using UUIDs here? shouldn't be
702                         // better to use group names, since uuids can change???
703                         log.warn("Can't find {} node by UUID {} referred by node {}", new Object[] { repositoryName, t.getMessage(), path });
704                         log.debug("Failed while reading node by UUID", t);
705                         // we continue since it can happen that target node is
706                         // removed
707                         // - UUID's are kept as simple strings thus have no
708                         // referential integrity
709                     }
710                 }
711                 return null;
712             }
713         });
714     }
715 
716     @Override
717     public User addGroup(User user, String groupName) {
718         try {
719             super.add(user.getName(), groupName, NODE_GROUPS);
720         } catch (PrincipalNotFoundException e) {
721             // user doesn't exist in this UM
722             return null;
723         }
724         return getUser(user.getName());
725     }
726 
727     @Override
728     public User removeGroup(User user, String groupName) {
729         try {
730             super.remove(user.getName(), groupName, NODE_GROUPS);
731         } catch (PrincipalNotFoundException e) {
732             // user doesn't exist in this UM
733             return null;
734         }
735         return getUser(user.getName());
736     }
737 
738     @Override
739     public User removeRole(User user, String roleName) {
740         try {
741             super.remove(user.getName(), roleName, NODE_ROLES);
742         } catch (PrincipalNotFoundException e) {
743             // user doesn't exist in this UM
744             return null;
745         }
746         return getUser(user.getName());
747     }
748 }