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.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.lock.LockException;
74 import javax.security.auth.Subject;
75
76 import org.apache.commons.lang.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. " + e.getMessage(), propertyName);
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. " + e.getMessage(), propertyName);
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 public void findAllUsersInFolder(Node node, Collection<User> users) throws RepositoryException {
386 NodeIterator nodesIter = node.getNodes();
387 Collection<Node> nodes = new HashSet<Node>();
388 Collection<Node> folders = new HashSet<Node>();
389 while (nodesIter.hasNext()) {
390 Node newNode = (Node) nodesIter.next();
391 if (newNode.isNodeType(NodeTypes.User.NAME)) {
392 nodes.add(newNode);
393 } else if (newNode.isNodeType(NodeTypes.Folder.NAME)) {
394 folders.add(newNode);
395 }
396 }
397
398 if (!nodes.isEmpty()) {
399 for (Node userNode : nodes) {
400 users.add(newUserInstance(userNode));
401 }
402 }
403 if (!folders.isEmpty()) {
404 for (Node folder : folders) {
405 findAllUsersInFolder(folder, users);
406 }
407 }
408 }
409
410 @Override
411 public User createUser(final String name, final String pw) {
412 return this.createUser(null, name, pw);
413 }
414 @Override
415 public User createUser(final String path, final String name, final String pw) throws UnsupportedOperationException {
416 validateUsername(name);
417 return MgnlContext.doInSystemContext(new SilentSessionOp<MgnlUser>(getRepositoryName()) {
418
419 @Override
420 public MgnlUser doExec(Session session) throws RepositoryException {
421 String uPath = path == null ? "/" + getRealmName() : path;
422 Node userNode = createUserNode(uPath, name, session);
423 userNode.addMixin(JcrConstants.MIX_LOCKABLE);
424 userNode.setProperty("name", name);
425 setPasswordProperty(userNode, pw);
426 userNode.setProperty("language", "en");
427
428 final String handle = userNode.getPath();
429 final Node acls = userNode.addNode(NODE_ACLUSERS, NodeTypes.ContentNode.NAME);
430
431 Node acl = acls.addNode(Path.getUniqueLabel(session, acls.getPath(), "0"), NodeTypes.ContentNode.NAME);
432 acl.setProperty("path", handle);
433 acl.setProperty("permissions", Permission.READ);
434
435 addWrite(handle, PROPERTY_EMAIL, acls);
436 addWrite(handle, PROPERTY_LANGUAGE, acls);
437 addWrite(handle, PROPERTY_LASTACCESS, acls);
438 addWrite(handle, PROPERTY_PASSWORD, acls);
439 addWrite(handle, PROPERTY_TITLE, acls);
440 session.save();
441 return new MgnlUser(userNode.getName(), getRealmName(), Collections.EMPTY_LIST, Collections.EMPTY_LIST, Collections.EMPTY_MAP, userNode.getPath(), userNode.getIdentifier());
442 }
443
444 @Override
445 public String toString() {
446 return "create user " + name;
447 }
448 });
449 }
450
451 @Override
452 public User changePassword(final User user, final String newPassword) {
453 return MgnlContext.doInSystemContext(new SilentSessionOp<User>(getRepositoryName()) {
454
455 @Override
456 public User doExec(Session session) throws RepositoryException {
457 Node userNode = findPrincipalNode(user.getName(), session);
458 setPasswordProperty(userNode, newPassword);
459
460 session.save();
461 return newUserInstance(userNode);
462 }
463
464 @Override
465 public String toString() {
466 return "change password of user " + user.getName();
467 }
468 });
469 }
470
471
472
473
474 @Deprecated
475 protected void setPasswordProperty(Content userNode, String clearPassword) throws RepositoryException {
476 setPasswordProperty(userNode.getJCRNode(), clearPassword);
477 }
478
479 protected void setPasswordProperty(Node userNode, String clearPassword) throws RepositoryException {
480 userNode.setProperty(PROPERTY_PASSWORD, encodePassword(clearPassword));
481 }
482
483 protected String encodePassword(String clearPassword) {
484 return SecurityUtil.getBCrypt(clearPassword);
485 }
486
487 protected void validateUsername(String name) {
488 if (StringUtils.isBlank(name)) {
489 throw new IllegalArgumentException(name + " is not a valid username.");
490 }
491
492 User user;
493 if (isAllowCrossRealmDuplicateNames()) {
494 user = this.getUser(name);
495 } else {
496 user = Security.getUserManager().getUser(name);
497 }
498 if (user != null) {
499 throw new IllegalArgumentException("User with name " + name + " already exists.");
500 }
501 }
502
503
504
505
506 protected Content createUserNode(String name) throws RepositoryException {
507 final String path = "/" + getRealmName();
508 final String userName = name;
509 Node userNode = MgnlContext.doInSystemContext(new SilentSessionOp<Node>(getRepositoryName()) {
510 @Override
511 public Node doExec(Session session) throws RepositoryException {
512 return createUserNode(path, userName, session);
513 }
514 });
515 return ContentUtil.asContent(userNode);
516
517 }
518
519 protected Node createUserNode(String path, String userName, Session session) throws RepositoryException {
520 return session.getNode(path).addNode(userName, NodeTypes.User.NAME);
521 }
522
523
524
525
526
527 protected HierarchyManager getHierarchyManager() {
528 return MgnlContext.getSystemContext().getHierarchyManager(RepositoryConstants.USERS);
529 }
530
531
532
533
534
535
536
537 @Deprecated
538 protected User newUserInstance(Content node) {
539 try {
540 return newUserInstance(node.getJCRNode());
541 } catch (RepositoryException e) {
542 log.error(e.getMessage(), e);
543 return null;
544 }
545 }
546
547 private Node addWrite(String parentPath, String property, Node acls) throws PathNotFoundException, RepositoryException, AccessDeniedException {
548 Node acl = acls.addNode(Path.getUniqueLabel(acls.getSession(), acls.getPath(), "0"), NodeTypes.ContentNode.NAME);
549 acl.setProperty("path", parentPath + "/" + property);
550 acl.setProperty("permissions", Permission.ALL);
551 return acl;
552 }
553
554 @Override
555 public void updateLastAccessTimestamp(final User user) throws UnsupportedOperationException {
556 final String workspaceName = getRepositoryName();
557 try {
558 MgnlContext.doInSystemContext(new MgnlContext.LockingOp(workspaceName, ((MgnlUser) user).getPath()) {
559
560 @Override
561 public void doExec() throws RepositoryException {
562 Session session = MgnlContext.getJCRSession(workspaceName);
563 String path = ((MgnlUser) user).getPath();
564 log.debug("update access timestamp for {}", user.getName());
565 try {
566 Node userNode = session.getNode(path);
567 PropertyUtil.updateOrCreate(userNode, "lastaccess", new GregorianCalendar());
568 session.save();
569 }
570 catch (RepositoryException e) {
571 session.refresh(false);
572 }
573 return;
574 }
575 });
576 } catch (LockException e) {
577 log.debug("Failed to lock node for last access timestamp update for user " + user.getName() + " with " + e.getMessage(), e);
578 } catch (RepositoryException e) {
579 log.error("Failed to update user " + user.getName() + " last access time stamp with " + e.getMessage(), e);
580 }
581 }
582
583 protected User newUserInstance(Node privilegedUserNode) throws ValueFormatException, PathNotFoundException, RepositoryException {
584 if (privilegedUserNode == null) {
585 return null;
586 }
587 Set<String> roles = collectUniquePropertyNames(privilegedUserNode, "roles", RepositoryConstants.USER_ROLES, false);
588 Set<String> groups = collectUniquePropertyNames(privilegedUserNode, "groups", RepositoryConstants.USER_GROUPS, false);
589
590 Map<String, String> properties = new HashMap<String, String>();
591 for (PropertyIterator iter = new FilteringPropertyIterator(privilegedUserNode.getProperties(), NodeUtil.ALL_PROPERTIES_EXCEPT_JCR_AND_MGNL_FILTER); iter.hasNext();) {
592 Property prop = iter.nextProperty();
593
594 properties.put(prop.getName(), prop.getString());
595 }
596
597 MgnlUser user = new MgnlUser(privilegedUserNode.getName(), getRealmName(), groups, roles, properties, privilegedUserNode.getPath(), privilegedUserNode.getIdentifier());
598 return user;
599 }
600
601 @Override
602 protected String getRepositoryName() {
603 return RepositoryConstants.USERS;
604 }
605
606
607
608
609 @Override
610 public Map<String, ACL> getACLs(final User user) {
611 if (!(user instanceof MgnlUser)) {
612 return null;
613 }
614 return super.getACLs(user.getName());
615 }
616
617 @Override
618 public User addRole(User user, String roleName) {
619 try {
620 super.add(user.getName(), roleName, NODE_ROLES);
621 } catch (PrincipalNotFoundException e) {
622
623 return null;
624 }
625 return getUser(user.getName());
626 }
627
628
629
630
631 private Set<String> collectUniquePropertyNames(Node rootNode, String subnodeName, String repositoryName, boolean isDeep) {
632 final SortedSet<String> set = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
633 String path = null;
634 try {
635 path = rootNode.getPath();
636 final Node node = rootNode.getNode(subnodeName);
637 collectUniquePropertyNames(node, repositoryName, subnodeName, set, isDeep);
638 collectUniquePropertyNames(rootNode.getNode(subnodeName), repositoryName, subnodeName, set, isDeep);
639 } catch (PathNotFoundException e) {
640 log.debug("{} does not have any {}", path, repositoryName);
641 } catch (Throwable t) {
642 log.error("Failed to read " + path + " or sub node " + subnodeName + " in repository " + repositoryName, t);
643 }
644 return set;
645 }
646
647 private void collectUniquePropertyNames(final Node node, final String repositoryName, final String subnodeName, final Collection<String> set, final boolean isDeep) throws RepositoryException {
648 MgnlContext.doInSystemContext(new JCRSessionOp<Void>(repositoryName) {
649
650 @Override
651 public Void exec(Session session) throws RepositoryException {
652 for (PropertyIterator iter = new FilteringPropertyIterator(node.getProperties(), NodeUtil.ALL_PROPERTIES_EXCEPT_JCR_AND_MGNL_FILTER); iter.hasNext();) {
653 Property property = iter.nextProperty();
654 final String uuid = property.getString();
655 try {
656 final Node targetNode = session.getNodeByIdentifier(uuid);
657 set.add(targetNode.getName());
658 if (isDeep && targetNode.hasNode(subnodeName)) {
659 collectUniquePropertyNames(targetNode.getNode(subnodeName), repositoryName, subnodeName, set, true);
660 }
661 } catch (ItemNotFoundException t) {
662 final String path = property.getPath();
663
664 log.warn("Can't find {} node by UUID {} referred by node {}", new Object[] { repositoryName, t.getMessage(), path });
665 log.debug("Failed while reading node by UUID", t);
666
667
668 }
669 }
670 return null;
671 }
672 });
673 }
674
675 @Override
676 public User addGroup(User user, String groupName) {
677 try {
678 super.add(user.getName(), groupName, NODE_GROUPS);
679 } catch (PrincipalNotFoundException e) {
680
681 return null;
682 }
683 return getUser(user.getName());
684 }
685
686 @Override
687 public User removeGroup(User user, String groupName) {
688 try {
689 super.remove(user.getName(), groupName, NODE_GROUPS);
690 } catch (PrincipalNotFoundException e) {
691
692 return null;
693 }
694 return getUser(user.getName());
695 }
696
697 @Override
698 public User removeRole(User user, String roleName) {
699 try {
700 super.remove(user.getName(), roleName, NODE_ROLES);
701 } catch (PrincipalNotFoundException e) {
702
703 return null;
704 }
705 return getUser(user.getName());
706 }
707 }