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
323 @Override
324 protected Node findPrincipalNode(String name, Session session) throws RepositoryException {
325 String realmName = getRealmName();
326 final String where;
327
328 if (Realm.REALM_ALL.getName().equals(realmName)) {
329 where = "where name() = '" + name + "'";
330 } else {
331
332
333 where = "where name() = '" + name + "' and isdescendantnode(['/" + realmName + "'])";
334
335
336 }
337
338 final String statement = "select * from [" + MgnlNodeType.USER + "] " + where;
339
340 Query query = session.getWorkspace().getQueryManager().createQuery(statement, Query.JCR_SQL2);
341 NodeIterator iter = query.execute().getNodes();
342 Node user = null;
343 while (iter.hasNext()) {
344 Node node = iter.nextNode();
345 if (node.isNodeType(ItemType.USER.getSystemName())) {
346 user = node;
347 break;
348 }
349 }
350 if (iter.hasNext()) {
351 log.error("More than one user found with name [{}] in realm [{}]", name, realmName);
352 }
353 return user;
354 }
355
356 protected User getFromRepository(String name) throws RepositoryException {
357 final Content node = findUserNode(this.realmName, name);
358 if (node == null) {
359 log.debug("User not found: [{}]", name);
360 return null;
361 }
362
363 return newUserInstance(node);
364 }
365
366
367
368
369 @Override
370 public User getSystemUser() throws UnsupportedOperationException {
371 throw new UnsupportedOperationException();
372 }
373
374
375
376
377 @Override
378 public User getAnonymousUser() throws UnsupportedOperationException {
379 throw new UnsupportedOperationException();
380 }
381
382
383
384
385 @Override
386 public Collection<User> getAllUsers() {
387 return MgnlContext.doInSystemContext(new SilentSessionOp<Collection<User>>(getRepositoryName()) {
388
389 @Override
390 public Collection<User> doExec(Session session) throws RepositoryException {
391 List<User> users = new ArrayList<User>();
392 Node node = session.getNode("/" + realmName);
393 updateUserListWithAllChildren(node, users);
394 return users;
395 }
396
397 @Override
398 public String toString() {
399 return "get all users";
400 }
401
402 });
403 }
404
405
406
407
408
409
410 public void updateUserListWithAllChildren(Node node, Collection<User> users) throws RepositoryException {
411 NodeIterator nodesIter = node.getNodes();
412 Collection<Node> nodes = new HashSet<Node>();
413 Collection<Node> folders = new HashSet<Node>();
414 while (nodesIter.hasNext()) {
415 Node newNode = (Node) nodesIter.next();
416 if (newNode.isNodeType(MgnlNodeType.USER)) {
417 nodes.add(newNode);
418 } else if (newNode.isNodeType(MgnlNodeType.NT_FOLDER)) {
419 folders.add(newNode);
420 }
421 }
422
423 if (!nodes.isEmpty()) {
424 for (Node userNode : nodes) {
425 users.add(newUserInstance(userNode));
426 }
427 }
428 if (!folders.isEmpty()) {
429 Iterator<Node> it = folders.iterator();
430 while (it.hasNext()) {
431 updateUserListWithAllChildren(it.next(), users);
432 }
433 }
434 }
435
436 @Override
437 public User createUser(final String name, final String pw) {
438 return this.createUser(null, name, pw);
439 }
440
441 @Override
442 public User createUser(final String path, final String name, final String pw) throws UnsupportedOperationException {
443 validateUsername(name);
444 return MgnlContext.doInSystemContext(new SilentSessionOp<MgnlUser>(getRepositoryName()) {
445
446 @Override
447 public MgnlUser doExec(Session session) throws RepositoryException {
448 String uPath = path == null ? "/" + getRealmName() : path;
449 Node userNode = session.getNode(uPath).addNode(name, MgnlNodeType.USER);
450 userNode.addMixin(MgnlNodeType.MIX_LOCKABLE);
451 userNode.setProperty("name", name);
452 setPasswordProperty(userNode, pw);
453 userNode.setProperty("language", "en");
454
455 final String handle = userNode.getPath();
456 final Node acls = userNode.addNode(NODE_ACLUSERS, MgnlNodeType.NT_CONTENTNODE);
457
458 Node acl = acls.addNode(Path.getUniqueLabel(session, acls.getPath(), "0"), MgnlNodeType.NT_CONTENTNODE);
459 acl.setProperty("path", handle);
460 acl.setProperty("permissions", Permission.READ);
461
462
463 addWrite(handle, PROPERTY_EMAIL, acls);
464 addWrite(handle, PROPERTY_LANGUAGE, acls);
465 addWrite(handle, PROPERTY_LASTACCESS, acls);
466 addWrite(handle, PROPERTY_PASSWORD, acls);
467 addWrite(handle, PROPERTY_TITLE, acls);
468
469 addWrite(handle, MetaData.DEFAULT_META_NODE + "/*", acls);
470
471 session.save();
472 return new MgnlUser(userNode.getName(), getRealmName(), Collections.EMPTY_LIST, Collections.EMPTY_LIST, Collections.EMPTY_MAP,
473 NodeUtil.getPathIfPossible(userNode), NodeUtil.getNodeIdentifierIfPossible(userNode));
474 }
475
476 @Override
477 public String toString() {
478 return "create user " + name;
479 }
480 });
481 }
482
483 @Override
484 public User changePassword(final User user, final String newPassword) {
485 return MgnlContext.doInSystemContext(new SilentSessionOp<User>(getRepositoryName()) {
486
487 @Override
488 public User doExec(Session session) throws RepositoryException {
489 Node userNode = session.getNode("/" + getRealmName() + "/" + user.getName());
490 setPasswordProperty(userNode, newPassword);
491
492 session.save();
493 return newUserInstance(userNode);
494 }
495
496 @Override
497 public String toString() {
498 return "change password of user " + user.getName();
499 }
500 });
501 }
502
503
504
505
506
507 @Deprecated
508 protected void setPasswordProperty(Content userNode, String clearPassword) throws RepositoryException {
509 setPasswordProperty(userNode.getJCRNode(), clearPassword);
510 }
511
512 protected void setPasswordProperty(Node userNode, String clearPassword) throws RepositoryException {
513 userNode.setProperty(PROPERTY_PASSWORD, encodePassword(clearPassword));
514 }
515
516 protected String encodePassword(String clearPassword) {
517 return SecurityUtil.getBCrypt(clearPassword);
518 }
519
520 protected void validateUsername(String name) {
521 if (StringUtils.isBlank(name)) {
522 throw new IllegalArgumentException(name + " is not a valid username.");
523 }
524 User user;
525 if(isAllowCrossRealmDuplicateNames()){
526 user = this.getUser(name);
527 } else {
528 user = Security.getUserManager().getUser(name);
529 }
530 if (user != null) {
531 throw new IllegalArgumentException("User with name " + name + " already exists.");
532 }
533 }
534
535 protected Content createUserNode(String name) throws RepositoryException {
536 final String path = "/" + getRealmName();
537 return getHierarchyManager().createContent(path, name, ItemType.USER.getSystemName());
538 }
539
540
541
542
543
544 protected HierarchyManager getHierarchyManager() {
545 return MgnlContext.getSystemContext().getHierarchyManager(RepositoryConstants.USERS);
546 }
547
548
549
550
551 @Deprecated
552 protected MgnlUser userInstance(Content node) {
553 try {
554 return (MgnlUser) newUserInstance(node.getJCRNode());
555 } catch (RepositoryException e) {
556 log.error(e.getMessage(), e);
557 return null;
558 }
559 }
560
561
562
563
564
565
566
567
568 @Deprecated
569 protected User newUserInstance(Content node) {
570 try {
571 return newUserInstance(node.getJCRNode());
572 } catch (RepositoryException e) {
573 log.error(e.getMessage(), e);
574 return null;
575 }
576 }
577
578 private Node addWrite(String parentPath, String property, Node acls) throws PathNotFoundException, RepositoryException, AccessDeniedException {
579 Node acl = acls.addNode(Path.getUniqueLabel(acls.getSession(), acls.getPath(), "0"), ItemType.CONTENTNODE.getSystemName());
580 acl.setProperty("path", parentPath + "/" + property);
581 acl.setProperty("permissions", Permission.ALL);
582 return acl;
583 }
584
585 @Override
586 public void updateLastAccessTimestamp(final User user) throws UnsupportedOperationException {
587 MgnlContext.doInSystemContext(new SilentSessionOp<Void>(getRepositoryName()) {
588
589 @Override
590 public Void doExec(Session session) throws RepositoryException {
591 String path = ((MgnlUser) user).getPath();
592 log.debug("update access timestamp for {}", user.getName());
593 try {
594 Node userNode = session.getNode(path);
595 PropertyUtil.updateOrCreate(userNode, "lastaccess", new GregorianCalendar());
596 session.save();
597 } catch (RepositoryException e) {
598 session.refresh(false);
599 }
600 return null;
601 }
602
603 @Override
604 public String toString() {
605 return "update user " + user.getName() + " last access time stamp";
606 }
607 });
608 }
609
610 protected User newUserInstance(Node privilegedUserNode) throws ValueFormatException, PathNotFoundException, RepositoryException {
611 if (privilegedUserNode == null) {
612 return null;
613 }
614 Set<String> roles = collectUniquePropertyNames(privilegedUserNode, "roles", RepositoryConstants.USER_ROLES, false);
615 Set<String> groups = collectUniquePropertyNames(privilegedUserNode, "groups", RepositoryConstants.USER_GROUPS, false);
616
617 Map<String, String> properties = new HashMap<String, String>();
618 for (PropertyIterator iter = privilegedUserNode.getProperties(); iter.hasNext();) {
619 Property prop = iter.nextProperty();
620 if (prop.getName().startsWith(MgnlNodeType.JCR_PREFIX) || prop.getName().startsWith(MgnlNodeType.MGNL_PREFIX)) {
621
622 continue;
623 }
624
625
626 properties.put(prop.getName(), prop.getString());
627 }
628
629 MgnlUser user = new MgnlUser(privilegedUserNode.getName(), getRealmName(), groups, roles, properties,
630 NodeUtil.getPathIfPossible(privilegedUserNode), NodeUtil.getNodeIdentifierIfPossible(privilegedUserNode));
631 return user;
632 }
633
634 @Override
635 protected String getRepositoryName() {
636 return RepositoryConstants.USERS;
637 }
638
639
640
641
642
643 @Override
644 public Map<String, ACL> getACLs(final User user) {
645 if (!(user instanceof MgnlUser)) {
646 return null;
647 }
648 return super.getACLs(user.getName());
649 }
650
651 @Override
652 public User addRole(User user, String roleName) {
653 try {
654 super.add(user.getName(), roleName, NODE_ROLES);
655 } catch (PrincipalNotFoundException e) {
656
657 return null;
658 }
659 return getUser(user.getName());
660 }
661
662
663
664
665
666 private Set<String> collectUniquePropertyNames(Node rootNode, String subnodeName, String repositoryName, boolean isDeep) {
667 final SortedSet<String> set = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
668 String path = null;
669 try {
670 path = rootNode.getPath();
671 final Node node = rootNode.getNode(subnodeName);
672 collectUniquePropertyNames(node, repositoryName, subnodeName, set, isDeep);
673 collectUniquePropertyNames(rootNode.getNode(subnodeName), repositoryName, subnodeName, set, isDeep);
674 } catch (PathNotFoundException e) {
675 log.debug("{} does not have any {}", path, repositoryName);
676 } catch (Throwable t) {
677 log.error("Failed to read " + path + " or sub node " + subnodeName + " in repository " + repositoryName, t);
678 }
679 return set;
680 }
681
682 private void collectUniquePropertyNames(final Node node, final String repositoryName, final String subnodeName, final Collection<String> set, final boolean isDeep) throws RepositoryException {
683 MgnlContext.doInSystemContext(new JCRSessionOp<Void>(repositoryName) {
684
685 @Override
686 public Void exec(Session session) throws RepositoryException {
687 for (PropertyIterator props = node.getProperties(); props.hasNext();) {
688 Property property = props.nextProperty();
689 if (property.getName().startsWith(MgnlNodeType.JCR_PREFIX)) {
690 continue;
691 }
692 final String uuid = property.getString();
693 try {
694 final Node targetNode = session.getNodeByIdentifier(uuid);
695 set.add(targetNode.getName());
696 if (isDeep && targetNode.hasNode(subnodeName)) {
697 collectUniquePropertyNames(targetNode.getNode(subnodeName), repositoryName, subnodeName, set, true);
698 }
699 } catch (ItemNotFoundException t) {
700 final String path = property.getPath();
701
702
703 log.warn("Can't find {} node by UUID {} referred by node {}", new Object[] { repositoryName, t.getMessage(), path });
704 log.debug("Failed while reading node by UUID", t);
705
706
707
708
709 }
710 }
711 return null;
712 }
713 });
714 }
715
716 @Override
717 public User addGroup(User user, String groupName) {
718 try {
719 super.add(user.getName(), groupName, NODE_GROUPS);
720 } catch (PrincipalNotFoundException e) {
721
722 return null;
723 }
724 return getUser(user.getName());
725 }
726
727 @Override
728 public User removeGroup(User user, String groupName) {
729 try {
730 super.remove(user.getName(), groupName, NODE_GROUPS);
731 } catch (PrincipalNotFoundException e) {
732
733 return null;
734 }
735 return getUser(user.getName());
736 }
737
738 @Override
739 public User removeRole(User user, String roleName) {
740 try {
741 super.remove(user.getName(), roleName, NODE_ROLES);
742 } catch (PrincipalNotFoundException e) {
743
744 return null;
745 }
746 return getUser(user.getName());
747 }
748 }