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