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