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 if (userNode != null) {
432 setPasswordProperty(userNode, newPassword);
433
434 return newUserInstance(userNode);
435 }
436 return null;
437 }
438
439 @Override
440 public String toString() {
441 return "change password of user " + user.getName();
442 }
443 });
444 }
445
446 protected void setPasswordProperty(Node userNode, String clearPassword) throws RepositoryException {
447 userNode.setProperty(PROPERTY_PASSWORD, encodePassword(clearPassword));
448 userNode.getSession().save();
449 }
450
451 protected String encodePassword(String clearPassword) {
452 return SecurityUtil.getBCrypt(clearPassword);
453 }
454
455 protected void validateUsername(String name) {
456 if (StringUtils.isBlank(name)) {
457 throw new IllegalArgumentException(name + " is not a valid username.");
458 }
459
460 User user;
461 if (isAllowCrossRealmDuplicateNames()) {
462 user = this.getUser(name);
463 } else {
464 user = Security.getUserManager().getUser(name);
465 }
466 if (user != null) {
467 throw new IllegalArgumentException("User with name " + name + " already exists.");
468 }
469 }
470
471
472
473
474 @Deprecated
475 protected Content createUserNode(String name) throws RepositoryException {
476 final String path = "/" + getRealmName();
477 final String userName = name;
478 Node userNode = MgnlContext.doInSystemContext(new SilentSessionOp<Node>(getRepositoryName()) {
479 @Override
480 public Node doExec(Session session) throws RepositoryException {
481 return createUserNode(path, userName, session);
482 }
483
484 @Override
485 public String toString() {
486 return getClass().getName() + " createUSerNode(name)";
487 }
488 });
489 return ContentUtil.asContent(userNode);
490
491 }
492
493 protected Node createUserNode(String path, String userName, Session session) throws RepositoryException {
494 return session.getNode(path).addNode(userName, NodeTypes.User.NAME);
495 }
496
497
498
499
500
501
502 @Deprecated
503 protected HierarchyManager getHierarchyManager() {
504 return MgnlContext.getSystemContext().getHierarchyManager(RepositoryConstants.USERS);
505 }
506
507 private Node addWrite(String parentPath, String property, Node acls) throws PathNotFoundException, RepositoryException, AccessDeniedException {
508 Node acl = acls.addNode(Path.getUniqueLabel(acls.getSession(), acls.getPath(), "0"), NodeTypes.ContentNode.NAME);
509 acl.setProperty("path", parentPath + "/" + property);
510 acl.setProperty("permissions", Permission.ALL);
511 return acl;
512 }
513
514 @Override
515 public void updateLastAccessTimestamp(final User user) throws UnsupportedOperationException {
516 final String workspaceName = getRepositoryName();
517 try {
518 MgnlContext.doInSystemContext(new MgnlContext.LockingOp(workspaceName, ((MgnlUser) user).getPath()) {
519
520 @Override
521 public void doExec() throws RepositoryException {
522 Session session = MgnlContext.getJCRSession(workspaceName);
523 String path = ((MgnlUser) user).getPath();
524 log.debug("update access timestamp for {}", user.getName());
525 try {
526 Node userNode = session.getNode(path);
527 userNode = NodeUtil.deepUnwrap(userNode, MgnlPropertySettingNodeWrapper.class);
528 PropertyUtil.updateOrCreate(userNode, "lastaccess", new GregorianCalendar());
529 session.save();
530 } catch (RepositoryException e) {
531 session.refresh(false);
532 }
533 return;
534 }
535
536 @Override
537 public String toString() {
538 return getClass().getName() + " updateLastAccessTimestamp(user)";
539 }
540 });
541 } catch (LockException e) {
542 log.debug("Failed to lock node for last access timestamp update for user {} with {}", user.getName(), e.getMessage(), e);
543 } catch (RepositoryException e) {
544 log.error("Failed to update user {} last access time stamp with {}", user.getName(), e.getMessage(), e);
545 }
546 }
547
548 protected User newUserInstance(Node privilegedUserNode) throws ValueFormatException, PathNotFoundException, RepositoryException {
549 if (privilegedUserNode == null) {
550 return null;
551 }
552 Set<String> roles = collectUniquePropertyNames(privilegedUserNode, ROLES_NODE_NAME, RepositoryConstants.USER_ROLES, false);
553 Set<String> groups = collectUniquePropertyNames(privilegedUserNode, GROUPS_NODE_NAME, RepositoryConstants.USER_GROUPS, false);
554
555 Map<String, String> properties = new HashMap<String, String>();
556 for (PropertyIterator iter = new FilteringPropertyIterator(privilegedUserNode.getProperties(), new JCRMgnlPropertyHidingPredicate()); iter.hasNext();) {
557 Property prop = iter.nextProperty();
558
559 properties.put(prop.getName(), prop.getString());
560 }
561
562 MgnlUser user = new MgnlUser(privilegedUserNode.getName(), getRealmName(), groups, roles, properties, privilegedUserNode.getPath(), privilegedUserNode.getIdentifier());
563 return user;
564 }
565
566 @Override
567 protected String getRepositoryName() {
568 return RepositoryConstants.USERS;
569 }
570
571
572
573
574 @Override
575 public Map<String, ACL> getACLs(final User user) {
576 if (!(user instanceof MgnlUser)) {
577 return null;
578 }
579 return super.getACLs(user.getName());
580 }
581
582 @Override
583 public User addRole(User user, String roleName) {
584 try {
585 super.add(user.getName(), roleName, NODE_ROLES);
586 } catch (PrincipalNotFoundException e) {
587
588 return null;
589 }
590 return getUser(user.getName());
591 }
592
593
594
595
596 private Set<String> collectUniquePropertyNames(Node rootNode, String subnodeName, String repositoryName, boolean isDeep) {
597 final SortedSet<String> set = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
598 String path = null;
599 try {
600 path = rootNode.getPath();
601 final Node node = rootNode.getNode(subnodeName);
602 collectUniquePropertyNames(node, repositoryName, subnodeName, set, isDeep);
603 collectUniquePropertyNames(rootNode.getNode(subnodeName), repositoryName, subnodeName, set, isDeep);
604 } catch (PathNotFoundException e) {
605 log.debug("{} does not have any {}", path, repositoryName);
606 } catch (Throwable t) {
607 log.error("Failed to read {} or sub node {} in repository {}", path, subnodeName, repositoryName, t);
608 }
609 return set;
610 }
611
612 private void collectUniquePropertyNames(final Node node, final String repositoryName, final String subnodeName, final Collection<String> set, final boolean isDeep) throws RepositoryException {
613 if (!MgnlContext.isSystemInstance()) {
614 if (log.isDebugEnabled()) {
615 log.debug("Collecting user properties in user context. List might not include all properties. Please check the calling code (see stacktrace)", new Exception());
616 } else {
617 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)");
618 }
619 }
620 Session session = MgnlContext.getJCRSession(repositoryName);
621 for (PropertyIterator iter = new FilteringPropertyIterator(node.getProperties(), new JCRMgnlPropertyHidingPredicate()); iter.hasNext();) {
622 Property property = iter.nextProperty();
623 final String uuid = property.getString();
624 try {
625 final Node targetNode = session.getNodeByIdentifier(uuid);
626 set.add(targetNode.getName());
627 if (isDeep && targetNode.hasNode(subnodeName)) {
628 collectUniquePropertyNames(targetNode.getNode(subnodeName), repositoryName, subnodeName, set, true);
629 }
630 } catch (ItemNotFoundException t) {
631 final String path = property.getPath();
632
633 log.warn("Can't find {} node by UUID {} referred by node {}", repositoryName, t.getMessage(), path);
634 log.debug("Failed while reading node by UUID", t);
635
636
637 }
638 }
639 }
640
641 @Override
642 public User addGroup(User user, String groupName) {
643 try {
644 super.add(user.getName(), groupName, NODE_GROUPS);
645 } catch (PrincipalNotFoundException e) {
646
647 return null;
648 }
649 return getUser(user.getName());
650 }
651
652 @Override
653 public User removeGroup(User user, String groupName) {
654 try {
655 super.remove(user.getName(), groupName, NODE_GROUPS);
656 } catch (PrincipalNotFoundException e) {
657
658 return null;
659 }
660 return getUser(user.getName());
661 }
662
663 @Override
664 public User removeRole(User user, String roleName) {
665 try {
666 super.remove(user.getName(), roleName, NODE_ROLES);
667 } catch (PrincipalNotFoundException e) {
668
669 return null;
670 }
671 return getUser(user.getName());
672 }
673
674 @Override
675 public Collection<String> getUsersWithGroup(final String groupName) {
676 return MgnlContext.doInSystemContext(new SilentSessionOp<Collection<String>>(getRepositoryName()) {
677
678 @Override
679 public Collection<String> doExec(Session session) throws RepositoryException {
680 final Node groupNode = findPrincipalNode(groupName, MgnlContext.getJCRSession(RepositoryConstants.USER_GROUPS), NodeTypes.Group.NAME);
681 return findUsersOrGroupsHavingAssignedGroupOrRoleWithUid(session, groupNode, GROUPS_NODE_NAME);
682 }
683
684 @Override
685 public String toString() {
686 return "get group " + groupName;
687 }
688 });
689 }
690
691 @Override
692 public Collection<String> getUsersWithRole(final String roleName) {
693 return MgnlContext.doInSystemContext(new SilentSessionOp<Collection<String>>(getRepositoryName()) {
694
695 @Override
696 public Collection<String> doExec(Session session) throws RepositoryException {
697 final Node roleNode = findPrincipalNode(roleName, MgnlContext.getJCRSession(RepositoryConstants.USER_ROLES), NodeTypes.Role.NAME);
698 return findUsersOrGroupsHavingAssignedGroupOrRoleWithUid(session, roleNode, ROLES_NODE_NAME);
699 }
700
701 @Override
702 public String toString() {
703 return "get role " + roleName;
704 }
705 });
706 }
707
708 @Override
709 public Collection<String> getUsersWithGroup(String groupName, boolean transitive) {
710 if (!transitive) {
711 return getUsersWithGroup(groupName);
712 }
713
714 Set<String> users = new HashSet<>();
715
716 GroupManager man = SecuritySupport.Factory.getInstance().getGroupManager();
717 Collection<String> groupNames = man.getAllSubGroups(groupName);
718 groupNames.add(groupName);
719 for (String transitiveGroup : groupNames) {
720 Collection<String> userNames = getUsersWithGroup(transitiveGroup);
721 users.addAll(userNames);
722 }
723 return users;
724 }
725 }