1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34 package info.magnolia.cms.security;
35
36 import static info.magnolia.cms.security.SecurityConstants.*;
37
38 import info.magnolia.cms.security.auth.ACL;
39 import info.magnolia.context.MgnlContext;
40 import info.magnolia.jcr.iterator.FilteringPropertyIterator;
41 import info.magnolia.jcr.predicate.JCRMgnlPropertyHidingPredicate;
42 import info.magnolia.jcr.util.NodeNameHelper;
43 import info.magnolia.jcr.util.NodeTypes;
44 import info.magnolia.jcr.util.NodeUtil;
45 import info.magnolia.jcr.util.PropertyUtil;
46 import info.magnolia.jcr.wrapper.MgnlPropertySettingNodeWrapper;
47 import info.magnolia.objectfactory.Components;
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 import java.util.stream.Collectors;
63
64 import javax.inject.Inject;
65 import javax.jcr.ItemNotFoundException;
66 import javax.jcr.Node;
67 import javax.jcr.NodeIterator;
68 import javax.jcr.PathNotFoundException;
69 import javax.jcr.Property;
70 import javax.jcr.PropertyIterator;
71 import javax.jcr.RepositoryException;
72 import javax.jcr.Session;
73 import javax.jcr.Value;
74 import javax.jcr.ValueFormatException;
75 import javax.jcr.lock.LockException;
76 import javax.security.auth.Subject;
77
78 import org.apache.commons.lang3.StringUtils;
79 import org.apache.jackrabbit.JcrConstants;
80 import org.slf4j.Logger;
81 import org.slf4j.LoggerFactory;
82
83
84
85
86 public class MgnlUserManager extends RepositoryBackedSecurityManager implements UserManager {
87
88 private static final Logger log = LoggerFactory.getLogger(MgnlUserManager.class);
89
90 public static final String PROPERTY_EMAIL = "email";
91 public static final String PROPERTY_LANGUAGE = "language";
92 public static final String PROPERTY_LASTACCESS = "lastaccess";
93 public static final String PROPERTY_PASSWORD = "pswd";
94 public static final String PROPERTY_TITLE = "title";
95 public static final String PROPERTY_ENABLED = "enabled";
96 public static final String PROPERTY_TIMEZONE = "timezone";
97
98 public static final String NODE_ACLUSERS = "acl_users";
99
100 private String realmName;
101
102 private boolean allowCrossRealmDuplicateNames = false;
103
104 private int maxFailedLoginAttempts;
105
106 private int lockTimePeriod;
107
108
109
110
111 @Inject
112 public MgnlUserManager(NodeNameHelper nodeNameHelper) {
113 super(nodeNameHelper);
114 }
115
116
117
118
119 @Deprecated
120 public MgnlUserManager() {
121 this(Components.getComponent(NodeNameHelper.class));
122 }
123
124 @Override
125 public void setMaxFailedLoginAttempts(int maxFailedLoginAttempts) {
126 this.maxFailedLoginAttempts = maxFailedLoginAttempts;
127 }
128
129 @Override
130 public int getMaxFailedLoginAttempts() {
131 return maxFailedLoginAttempts;
132 }
133
134 @Override
135 public int getLockTimePeriod() {
136 return lockTimePeriod;
137 }
138
139 @Override
140 public void setLockTimePeriod(int lockTimePeriod) {
141 this.lockTimePeriod = lockTimePeriod;
142 }
143
144 @Override
145 public User setProperty(final User user, final String propertyName, final Value propertyValue) {
146 return MgnlContext.doInSystemContext(new SilentSessionOp<User>(getRepositoryName()) {
147
148 @Override
149 public User doExec(Session session) throws RepositoryException {
150 String path = ((MgnlUser) user).getPath();
151 Node userNode;
152 try {
153 userNode = session.getNode(path);
154
155
156 if (propertyValue != null || PropertyUtil.getPropertyOrNull(userNode, propertyName) != null) {
157 if (StringUtils.equals(propertyName, PROPERTY_PASSWORD)) {
158 setPasswordProperty(userNode, propertyValue.getString());
159 } else {
160 userNode.setProperty(propertyName, propertyValue);
161 session.save();
162 }
163 }
164 } catch (RepositoryException e) {
165 session.refresh(false);
166 log.error("Property {} can't be changed. {}", propertyName, e.getMessage());
167 return user;
168 }
169 return newUserInstance(userNode);
170 }
171
172 @Override
173 public String toString() {
174 return getClass().getName() + " setProperty(user, propertyName, Value propertyValue)";
175 }
176 });
177 }
178
179 @Override
180 public User setProperty(final User user, final String propertyName, final String propertyValue) {
181 return MgnlContext.doInSystemContext(new SilentSessionOp<User>(getRepositoryName()) {
182
183 @Override
184 public User doExec(Session session) throws RepositoryException {
185 String path = ((MgnlUser) user).getPath();
186 Node userNode;
187 try {
188 userNode = session.getNode(path);
189
190
191 if (propertyName != null) {
192 if (StringUtils.equals(propertyName, PROPERTY_PASSWORD)) {
193 setPasswordProperty(userNode, propertyValue);
194 } else {
195 userNode.setProperty(propertyName, propertyValue);
196 session.save();
197 }
198 }
199 } catch (RepositoryException e) {
200 session.refresh(false);
201 log.error("Property {} can't be changed. {}", propertyName, e.getMessage());
202 return user;
203 }
204 return newUserInstance(userNode);
205 }
206
207 @Override
208 public String toString() {
209 return getClass().getName() + " setProperty(user, propertyName, String propertyValue)";
210 }
211 });
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
232
233
234
235
236 @Override
237 public User getUser(final String name) {
238 try {
239
240 if (MgnlContext.isSystemInstance()) {
241 return getUser(name, MgnlContext.getJCRSession(getRepositoryName()));
242 } else {
243 return MgnlContext.doInSystemContext(new JCRSessionOp<User>(getRepositoryName()) {
244 @Override
245 public User exec(Session session) throws RepositoryException {
246 return getUser(name, session);
247 }
248
249 @Override
250 public String toString() {
251 return "retrieve user " + name;
252 }
253 });
254 }
255 } catch (RepositoryException e) {
256 log.error("Could not retrieve user with name: {}", name, e);
257 }
258 return null;
259 }
260
261 private User getUser(String name, Session session) throws RepositoryException {
262 Node priviledgedUserNode = findPrincipalNode(name, session);
263 return newUserInstance(priviledgedUserNode);
264 }
265
266
267
268
269
270
271
272 @Override
273 public User getUserById(final String id) {
274 try {
275 return MgnlContext.doInSystemContext(new JCRSessionOp<User>(getRepositoryName()) {
276 @Override
277 public User exec(Session session) throws RepositoryException {
278 Node priviledgedUserNode = session.getNodeByIdentifier(id);
279 return newUserInstance(priviledgedUserNode);
280 }
281
282 @Override
283 public String toString() {
284 return "retrieve user with id " + id;
285 }
286 });
287 } catch (RepositoryException e) {
288 log.error("Could not retrieve user with id: {}", id, e);
289 }
290 return null;
291 }
292
293 @Override
294 public User getUser(Subject subject) throws UnsupportedOperationException {
295
296 if (subject == null) {
297 log.debug("subject not set.");
298 return new DummyUser();
299 }
300
301 Set<User> principalSet = subject.getPrincipals(User.class);
302 Iterator<User> entityIterator = principalSet.iterator();
303 if (!entityIterator.hasNext()) {
304
305 log.debug("user name not contained in principal set.");
306 return new DummyUser();
307 }
308 return entityIterator.next();
309 }
310
311
312
313
314
315 @Override
316 protected Node findPrincipalNode(String name, Session session) throws RepositoryException {
317 final String realmName = getRealmName();
318
319 final Node startNode = (Realm.REALM_ALL.getName().equals(realmName)) ? session.getRootNode() : session.getNode("/" + realmName);
320
321 return findPrincipalNode(name, session, NodeTypes.User.NAME, startNode);
322 }
323
324 protected User getFromRepository(final String name) throws RepositoryException {
325 return MgnlContext.doInSystemContext(new SilentSessionOp<User>(getRepositoryName()) {
326
327 @Override
328 public User doExec(Session session) throws RepositoryException {
329 Node userNode = findPrincipalNode(name, session);
330 return newUserInstance(userNode);
331 }
332
333 @Override
334 public String toString() {
335 return "Retrieve user [" + name + "] from repository.";
336 }
337 });
338 }
339
340
341
342
343 @Override
344 public User getSystemUser() throws UnsupportedOperationException {
345 throw new UnsupportedOperationException();
346 }
347
348
349
350
351 @Override
352 public User getAnonymousUser() throws UnsupportedOperationException {
353 throw new UnsupportedOperationException();
354 }
355
356
357
358
359 @Override
360 public Collection<User> getAllUsers() {
361 return MgnlContext.doInSystemContext(new SilentSessionOp<Collection<User>>(getRepositoryName()) {
362
363 @Override
364 public Collection<User> doExec(Session session) throws RepositoryException {
365 List<User> users = new ArrayList<User>();
366 Node node = session.getNode("/" + realmName);
367 findAllUsersInFolder(node, users);
368 return users;
369 }
370
371 @Override
372 public String toString() {
373 return "get all users";
374 }
375
376 });
377 }
378
379
380
381
382
383 public void findAllUsersInFolder(Node node, Collection<User> addTo) throws RepositoryException {
384 final NodeIterator nodesIter = findPrincipalNodes(node, NodeTypes.User.NAME);
385 while (nodesIter.hasNext()) {
386 addTo.add(newUserInstance(nodesIter.nextNode()));
387 }
388 }
389
390 @Override
391 public User createUser(final String name, final String pw) {
392 return this.createUser(null, name, pw);
393 }
394
395 @Override
396 public User createUser(final String path, final String name, final String pw) throws UnsupportedOperationException {
397 validateUsername(name);
398 return MgnlContext.doInSystemContext(new SilentSessionOp<MgnlUser>(getRepositoryName()) {
399
400 @Override
401 public MgnlUser doExec(Session session) throws RepositoryException {
402 String uPath = path == null ? "/" + getRealmName() : path;
403 Node userNode = createUserNode(uPath, name, session);
404 userNode.addMixin(JcrConstants.MIX_LOCKABLE);
405 userNode.setProperty("name", name);
406 setPasswordProperty(userNode, pw);
407 userNode.setProperty("language", "en");
408
409 final String handle = userNode.getPath();
410 final Node acls = userNode.addNode(NODE_ACLUSERS, NodeTypes.ContentNode.NAME);
411
412 Node acl = acls.addNode(nodeNameHelper.getUniqueName(session, acls.getPath(), "0"), NodeTypes.ContentNode.NAME);
413 acl.setProperty("path", handle);
414 acl.setProperty("permissions", Permission.READ);
415
416 addWrite(handle, PROPERTY_EMAIL, acls);
417 addWrite(handle, PROPERTY_LANGUAGE, acls);
418 addWrite(handle, PROPERTY_LASTACCESS, acls);
419 addWrite(handle, PROPERTY_PASSWORD, acls);
420 addWrite(handle, PROPERTY_TITLE, acls);
421
422 if (Realm.REALM_ADMIN.getName().equals(getRealmName())) {
423 addWrite(handle, PROPERTY_TIMEZONE, acls);
424 }
425 session.save();
426 return new MgnlUser(userNode.getName(), getRealmName(), Collections.emptySet(), Collections.emptySet(), Collections.emptyMap(), userNode.getPath(), userNode.getIdentifier(), Collections.emptySet(), Collections.emptySet());
427 }
428
429 @Override
430 public String toString() {
431 return "create user " + name;
432 }
433 });
434 }
435
436 @Override
437 public User changePassword(final User user, final String newPassword) {
438 return MgnlContext.doInSystemContext(new SilentSessionOp<User>(getRepositoryName()) {
439
440 @Override
441 public User doExec(Session session) throws RepositoryException {
442 Node userNode = findPrincipalNode(user.getName(), session);
443 if (userNode != null) {
444 setPasswordProperty(userNode, newPassword);
445
446 return newUserInstance(userNode);
447 }
448 return null;
449 }
450
451 @Override
452 public String toString() {
453 return "change password of user " + user.getName();
454 }
455 });
456 }
457
458 protected void setPasswordProperty(Node userNode, String clearPassword) throws RepositoryException {
459 userNode.setProperty(PROPERTY_PASSWORD, encodePassword(clearPassword));
460 userNode.getSession().save();
461 }
462
463 protected String encodePassword(String clearPassword) {
464 return SecurityUtil.getBCrypt(clearPassword);
465 }
466
467 protected void validateUsername(String name) {
468 if (StringUtils.isBlank(name)) {
469 throw new IllegalArgumentException(name + " is not a valid username.");
470 }
471
472 User user;
473 if (isAllowCrossRealmDuplicateNames()) {
474 user = this.getUser(name);
475 } else {
476 user = Security.getUserManager().getUser(name);
477 }
478 if (user != null) {
479 throw new IllegalArgumentException("User with name " + name + " already exists.");
480 }
481 }
482
483 protected Node createUserNode(String path, String userName, Session session) throws RepositoryException {
484 return session.getNode(path).addNode(userName, NodeTypes.User.NAME);
485 }
486
487 private Node addWrite(String parentPath, String property, Node acls) throws PathNotFoundException, RepositoryException, AccessDeniedException {
488 Node acl = acls.addNode(nodeNameHelper.getUniqueName(acls.getSession(), acls.getPath(), "0"), NodeTypes.ContentNode.NAME);
489 acl.setProperty("path", parentPath + "/" + property);
490 acl.setProperty("permissions", Permission.ALL);
491 return acl;
492 }
493
494 @Override
495 public void updateLastAccessTimestamp(final User user) throws UnsupportedOperationException {
496 final String workspaceName = getRepositoryName();
497 try {
498 MgnlContext.doInSystemContext(new MgnlContext.LockingOp(workspaceName, ((MgnlUser) user).getPath()) {
499
500 @Override
501 public void doExec() throws RepositoryException {
502 Session session = MgnlContext.getJCRSession(workspaceName);
503 String path = ((MgnlUser) user).getPath();
504 log.debug("update access timestamp for {}", user.getName());
505 try {
506 Node userNode = session.getNode(path);
507 userNode = NodeUtil.deepUnwrap(userNode, MgnlPropertySettingNodeWrapper.class);
508 PropertyUtil.updateOrCreate(userNode, "lastaccess", new GregorianCalendar());
509 session.save();
510 } catch (RepositoryException e) {
511 session.refresh(false);
512 }
513 return;
514 }
515
516 @Override
517 public String toString() {
518 return getClass().getName() + " updateLastAccessTimestamp(user)";
519 }
520 });
521 } catch (LockException e) {
522 log.debug("Failed to lock node for last access timestamp update for user {} with {}", user.getName(), e.getMessage(), e);
523 } catch (RepositoryException e) {
524 log.error("Failed to update user {} last access time stamp with {}", user.getName(), e.getMessage(), e);
525 }
526 }
527
528 protected User newUserInstance(Node privilegedUserNode) throws ValueFormatException, PathNotFoundException, RepositoryException {
529 if (privilegedUserNode == null) {
530 return null;
531 }
532 Set<String> roles = collectUniquePropertyNames(privilegedUserNode, ROLES_NODE_NAME, RepositoryConstants.USER_ROLES, false);
533 Set<String> groups = collectUniquePropertyNames(privilegedUserNode, GROUPS_NODE_NAME, RepositoryConstants.USER_GROUPS, false);
534
535 final String userName = privilegedUserNode.getName();
536
537 final GroupManager groupManager = Components.getComponent(SecuritySupport.class).getGroupManager();
538
539 Set<String> allGroups = aggregateDirectAndTransitiveGroups(groups, userName, groupManager);
540 Set<String> allRoles = aggregateDirectAndTransitiveRoles(roles, allGroups, userName, groupManager);
541
542
543 Map<String, String> properties = new HashMap<>();
544 for (PropertyIterator iter = new FilteringPropertyIterator(privilegedUserNode.getProperties(), new JCRMgnlPropertyHidingPredicate()); iter.hasNext();) {
545 Property prop = iter.nextProperty();
546
547 properties.put(prop.getName(), prop.getString());
548 }
549 return new MgnlUser(userName, getRealmName(), groups, roles, properties, privilegedUserNode.getPath(), privilegedUserNode.getIdentifier(), allGroups, allRoles);
550 }
551
552 @Override
553 protected String getRepositoryName() {
554 return RepositoryConstants.USERS;
555 }
556
557
558
559
560 @Override
561 public Map<String, ACL> getACLs(final User user) {
562 if (!(user instanceof MgnlUser)) {
563 return null;
564 }
565 return super.getACLs(user.getName());
566 }
567
568 @Override
569 public User addRole(User user, String roleName) {
570 try {
571 super.add(user.getName(), roleName, NODE_ROLES);
572 } catch (PrincipalNotFoundException e) {
573
574 return null;
575 }
576 return getUser(user.getName());
577 }
578
579
580
581
582 protected Set<String> collectUniquePropertyNames(Node rootNode, String subnodeName, String repositoryName, boolean isDeep) {
583 final SortedSet<String> set = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
584 String path = null;
585 try {
586 path = rootNode.getPath();
587 final Node node = rootNode.getNode(subnodeName);
588 collectUniquePropertyNames(node, repositoryName, subnodeName, set, isDeep);
589 collectUniquePropertyNames(rootNode.getNode(subnodeName), repositoryName, subnodeName, set, isDeep);
590 } catch (PathNotFoundException e) {
591 log.debug("{} does not have any {}", path, repositoryName);
592 } catch (Throwable t) {
593 log.error("Failed to read {} or sub node {} in repository {}", path, subnodeName, repositoryName, t);
594 }
595 return set;
596 }
597
598 private void collectUniquePropertyNames(final Node node, final String repositoryName, final String subnodeName, final Collection<String> set, final boolean isDeep) throws RepositoryException {
599 if (!MgnlContext.isSystemInstance()) {
600 if (log.isDebugEnabled()) {
601 log.debug("Collecting user properties in user context. List might not include all properties. Please check the calling code (see stacktrace)", new Exception());
602 } else {
603 log.warn("Collecting user properties in user context. List might not include all properties. Please check the calling code (stacktrace will be printed for this call when debug logging is enabled)");
604 }
605 }
606 Session session = MgnlContext.getJCRSession(repositoryName);
607 for (PropertyIterator iter = new FilteringPropertyIterator(node.getProperties(), new JCRMgnlPropertyHidingPredicate()); iter.hasNext();) {
608 Property property = iter.nextProperty();
609 final String uuid = property.getString();
610 try {
611 final Node targetNode = session.getNodeByIdentifier(uuid);
612 set.add(targetNode.getName());
613 if (isDeep && targetNode.hasNode(subnodeName)) {
614 collectUniquePropertyNames(targetNode.getNode(subnodeName), repositoryName, subnodeName, set, true);
615 }
616 } catch (ItemNotFoundException t) {
617 final String path = property.getPath();
618
619 log.warn("Can't find {} node by UUID {} referred by node {}", repositoryName, t.getMessage(), path);
620 log.debug("Failed while reading node by UUID", t);
621
622
623 }
624 }
625 }
626
627 @Override
628 public User addGroup(User user, String groupName) {
629 try {
630 super.add(user.getName(), groupName, NODE_GROUPS);
631 } catch (PrincipalNotFoundException e) {
632
633 return null;
634 }
635 return getUser(user.getName());
636 }
637
638 @Override
639 public User removeGroup(User user, String groupName) {
640 try {
641 super.remove(user.getName(), groupName, NODE_GROUPS);
642 } catch (PrincipalNotFoundException e) {
643
644 return null;
645 }
646 return getUser(user.getName());
647 }
648
649 @Override
650 public User removeRole(User user, String roleName) {
651 try {
652 super.remove(user.getName(), roleName, NODE_ROLES);
653 } catch (PrincipalNotFoundException e) {
654
655 return null;
656 }
657 return getUser(user.getName());
658 }
659
660 @Override
661 public Collection<String> getUsersWithGroup(final String groupName) {
662 return MgnlContext.doInSystemContext(new SilentSessionOp<Collection<String>>(getRepositoryName()) {
663
664 @Override
665 public Collection<String> doExec(Session session) throws RepositoryException {
666 final Node groupNode = findPrincipalNode(groupName, MgnlContext.getJCRSession(RepositoryConstants.USER_GROUPS), NodeTypes.Group.NAME);
667 return findUsersOrGroupsHavingAssignedGroupOrRoleWithUid(session, groupNode, GROUPS_NODE_NAME);
668 }
669
670 @Override
671 public String toString() {
672 return "get group " + groupName;
673 }
674 });
675 }
676
677 @Override
678 public Collection<String> getUsersWithRole(final String roleName) {
679 return MgnlContext.doInSystemContext(new SilentSessionOp<Collection<String>>(getRepositoryName()) {
680
681 @Override
682 public Collection<String> doExec(Session session) throws RepositoryException {
683 final Node roleNode = findPrincipalNode(roleName, MgnlContext.getJCRSession(RepositoryConstants.USER_ROLES), NodeTypes.Role.NAME);
684 return findUsersOrGroupsHavingAssignedGroupOrRoleWithUid(session, roleNode, ROLES_NODE_NAME);
685 }
686
687 @Override
688 public String toString() {
689 return "get role " + roleName;
690 }
691 });
692 }
693
694 @Override
695 public Collection<String> getUsersWithGroup(String groupName, boolean transitive) {
696 if (!transitive) {
697 return getUsersWithGroup(groupName);
698 }
699
700 Set<String> users = new HashSet<>();
701
702 GroupManager man = SecuritySupport.Factory.getInstance().getGroupManager();
703 Collection<String> groupNames = man.getAllSubGroups(groupName);
704 groupNames.add(groupName);
705 for (String transitiveGroup : groupNames) {
706 Collection<String> userNames = getUsersWithGroup(transitiveGroup);
707 users.addAll(userNames);
708 }
709 return users;
710 }
711
712
713
714
715
716 protected Set<String> aggregateDirectAndTransitiveRoles(Collection<String> roles, Collection<String> allGroups, String name, GroupManager groupManager) {
717
718 final Set<String> set = new HashSet<>(roles);
719
720
721 allGroups.forEach(group -> {
722 try {
723 set.addAll(groupManager.getGroup(group).getRoles());
724 } catch (javax.jcr.AccessDeniedException e) {
725 log.debug("Skipping denied group {} for user {}", group, name, e);
726 } catch (UnsupportedOperationException e) {
727 log.debug("Skipping unsupported getGroup() for group {} and user {}", group, name, e);
728 }
729 });
730
731 return set.stream()
732 .sorted(String::compareToIgnoreCase)
733 .collect(Collectors.toSet());
734 }
735
736
737
738
739
740 protected Set<String> aggregateDirectAndTransitiveGroups(Collection<String> groups, String name, GroupManager groupManager) {
741 final Set<String> set = new HashSet<>();
742
743
744 addSubgroups(set, groupManager, groups, name);
745
746 return set.stream()
747 .sorted(String::compareToIgnoreCase)
748 .collect(Collectors.toSet());
749 }
750
751
752
753
754 private void addSubgroups(final Set<String> allGroups, GroupManager groupManager, Collection<String> groups, String name) {
755 groups.forEach(groupName -> {
756
757 if (!allGroups.contains(groupName)) {
758 allGroups.add(groupName);
759 try {
760 Group group = groupManager.getGroup(groupName);
761 if (group == null) {
762 log.error("Failed to resolve group {} for user {}.", groupName, name);
763 return;
764 }
765 Collection<String> subgroups = group.getGroups();
766
767 addSubgroups(allGroups, groupManager, subgroups, name);
768 } catch (javax.jcr.AccessDeniedException e) {
769 log.debug("Skipping denied group {} for user {}", groupName, name, e);
770 } catch (UnsupportedOperationException e) {
771 log.debug("Skipping unsupported getGroup() for group {} and user {}", groupName, name, e);
772 }
773 }
774 });
775 }
776 }