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