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