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