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