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