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