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