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 public void findAllUsersInFolder(Node node, Collection<User> users) throws RepositoryException {
391 NodeIterator nodesIter = node.getNodes();
392 Collection<Node> nodes = new HashSet<Node>();
393 Collection<Node> folders = new HashSet<Node>();
394 while (nodesIter.hasNext()) {
395 Node newNode = (Node) nodesIter.next();
396 if (newNode.isNodeType(NodeTypes.User.NAME)) {
397 nodes.add(newNode);
398 } else if (newNode.isNodeType(NodeTypes.Folder.NAME)) {
399 folders.add(newNode);
400 }
401 }
402
403 if (!nodes.isEmpty()) {
404 for (Node userNode : nodes) {
405 users.add(newUserInstance(userNode));
406 }
407 }
408 if (!folders.isEmpty()) {
409 for (Node folder : folders) {
410 findAllUsersInFolder(folder, users);
411 }
412 }
413 }
414
415 @Override
416 public User createUser(final String name, final String pw) {
417 return this.createUser(null, name, pw);
418 }
419
420 @Override
421 public User createUser(final String path, final String name, final String pw) throws UnsupportedOperationException {
422 validateUsername(name);
423 return MgnlContext.doInSystemContext(new SilentSessionOp<MgnlUser>(getRepositoryName()) {
424
425 @Override
426 public MgnlUser doExec(Session session) throws RepositoryException {
427 String uPath = path == null ? "/" + getRealmName() : path;
428 Node userNode = session.getNode(uPath).addNode(name, NodeTypes.User.NAME);
429 userNode.addMixin(JcrConstants.MIX_LOCKABLE);
430 userNode.setProperty("name", name);
431 setPasswordProperty(userNode, pw);
432 userNode.setProperty("language", "en");
433
434 final String handle = userNode.getPath();
435 final Node acls = userNode.addNode(NODE_ACLUSERS, NodeTypes.ContentNode.NAME);
436
437 Node acl = acls.addNode(Path.getUniqueLabel(session, acls.getPath(), "0"), NodeTypes.ContentNode.NAME);
438 acl.setProperty("path", handle);
439 acl.setProperty("permissions", Permission.READ);
440
441 addWrite(handle, PROPERTY_EMAIL, acls);
442 addWrite(handle, PROPERTY_LANGUAGE, acls);
443 addWrite(handle, PROPERTY_LASTACCESS, acls);
444 addWrite(handle, PROPERTY_PASSWORD, acls);
445 addWrite(handle, PROPERTY_TITLE, acls);
446 session.save();
447 return new MgnlUser(userNode.getName(), getRealmName(), Collections.EMPTY_LIST, Collections.EMPTY_LIST, Collections.EMPTY_MAP, userNode.getPath(), userNode.getIdentifier());
448 }
449
450 @Override
451 public String toString() {
452 return "create user " + name;
453 }
454 });
455 }
456
457 @Override
458 public User changePassword(final User user, final String newPassword) {
459 return MgnlContext.doInSystemContext(new SilentSessionOp<User>(getRepositoryName()) {
460
461 @Override
462 public User doExec(Session session) throws RepositoryException {
463 Node userNode = session.getNode("/" + getRealmName() + "/" + user.getName());
464 setPasswordProperty(userNode, newPassword);
465
466 session.save();
467 return newUserInstance(userNode);
468 }
469
470 @Override
471 public String toString() {
472 return "change password of user " + user.getName();
473 }
474 });
475 }
476
477
478
479
480 @Deprecated
481 protected void setPasswordProperty(Content userNode, String clearPassword) throws RepositoryException {
482 setPasswordProperty(userNode.getJCRNode(), clearPassword);
483 }
484
485 protected void setPasswordProperty(Node userNode, String clearPassword) throws RepositoryException {
486 userNode.setProperty(PROPERTY_PASSWORD, encodePassword(clearPassword));
487 }
488
489 protected String encodePassword(String clearPassword) {
490 return SecurityUtil.getBCrypt(clearPassword);
491 }
492
493 protected void validateUsername(String name) {
494 if (StringUtils.isBlank(name)) {
495 throw new IllegalArgumentException(name + " is not a valid username.");
496 }
497
498 User user;
499 if (isAllowCrossRealmDuplicateNames()) {
500 user = this.getUser(name);
501 } else {
502 user = Security.getUserManager().getUser(name);
503 }
504 if (user != null) {
505 throw new IllegalArgumentException("User with name " + name + " already exists.");
506 }
507 }
508
509 protected Content createUserNode(String name) throws RepositoryException {
510 final String path = "/" + getRealmName();
511 return getHierarchyManager().createContent(path, name, NodeTypes.User.NAME);
512 }
513
514
515
516
517 protected HierarchyManager getHierarchyManager() {
518 return MgnlContext.getSystemContext().getHierarchyManager(RepositoryConstants.USERS);
519 }
520
521
522
523
524
525
526
527 @Deprecated
528 protected User newUserInstance(Content node) {
529 try {
530 return newUserInstance(node.getJCRNode());
531 } catch (RepositoryException e) {
532 log.error(e.getMessage(), e);
533 return null;
534 }
535 }
536
537 private Node addWrite(String parentPath, String property, Node acls) throws PathNotFoundException, RepositoryException, AccessDeniedException {
538 Node acl = acls.addNode(Path.getUniqueLabel(acls.getSession(), acls.getPath(), "0"), NodeTypes.ContentNode.NAME);
539 acl.setProperty("path", parentPath + "/" + property);
540 acl.setProperty("permissions", Permission.ALL);
541 return acl;
542 }
543
544 @Override
545 public void updateLastAccessTimestamp(final User user) throws UnsupportedOperationException {
546 final String workspaceName = getRepositoryName();
547 try {
548 MgnlContext.doInSystemContext(new MgnlContext.LockingOp(workspaceName, ((MgnlUser) user).getPath()) {
549
550 @Override
551 public void doExec() throws RepositoryException {
552 Session session = MgnlContext.getJCRSession(workspaceName);
553 String path = ((MgnlUser) user).getPath();
554 log.debug("update access timestamp for {}", user.getName());
555 try {
556 Node userNode = session.getNode(path);
557 PropertyUtil.updateOrCreate(userNode, "lastaccess", new GregorianCalendar());
558 session.save();
559 }
560 catch (RepositoryException e) {
561 session.refresh(false);
562 }
563 return;
564 }
565 });
566 } catch (LockException e) {
567 log.debug("Failed to lock node for last access timestamp update for user " + user.getName() + " with " + e.getMessage(), e);
568 } catch (RepositoryException e) {
569 log.error("Failed to update user " + user.getName() + " last access time stamp with " + e.getMessage(), e);
570 }
571 }
572
573 protected User newUserInstance(Node privilegedUserNode) throws ValueFormatException, PathNotFoundException, RepositoryException {
574 if (privilegedUserNode == null) {
575 return null;
576 }
577 Set<String> roles = collectUniquePropertyNames(privilegedUserNode, "roles", RepositoryConstants.USER_ROLES, false);
578 Set<String> groups = collectUniquePropertyNames(privilegedUserNode, "groups", RepositoryConstants.USER_GROUPS, false);
579
580 Map<String, String> properties = new HashMap<String, String>();
581 for (PropertyIterator iter = new FilteringPropertyIterator(privilegedUserNode.getProperties(), NodeUtil.ALL_PROPERTIES_EXCEPT_JCR_AND_MGNL_FILTER); iter.hasNext();) {
582 Property prop = iter.nextProperty();
583
584 properties.put(prop.getName(), prop.getString());
585 }
586
587 MgnlUser user = new MgnlUser(privilegedUserNode.getName(), getRealmName(), groups, roles, properties, privilegedUserNode.getPath(), privilegedUserNode.getIdentifier());
588 return user;
589 }
590
591 @Override
592 protected String getRepositoryName() {
593 return RepositoryConstants.USERS;
594 }
595
596
597
598
599 @Override
600 public Map<String, ACL> getACLs(final User user) {
601 if (!(user instanceof MgnlUser)) {
602 return null;
603 }
604 return super.getACLs(user.getName());
605 }
606
607 @Override
608 public User addRole(User user, String roleName) {
609 try {
610 super.add(user.getName(), roleName, NODE_ROLES);
611 } catch (PrincipalNotFoundException e) {
612
613 return null;
614 }
615 return getUser(user.getName());
616 }
617
618
619
620
621 private Set<String> collectUniquePropertyNames(Node rootNode, String subnodeName, String repositoryName, boolean isDeep) {
622 final SortedSet<String> set = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
623 String path = null;
624 try {
625 path = rootNode.getPath();
626 final Node node = rootNode.getNode(subnodeName);
627 collectUniquePropertyNames(node, repositoryName, subnodeName, set, isDeep);
628 collectUniquePropertyNames(rootNode.getNode(subnodeName), repositoryName, subnodeName, set, isDeep);
629 } catch (PathNotFoundException e) {
630 log.debug("{} does not have any {}", path, repositoryName);
631 } catch (Throwable t) {
632 log.error("Failed to read " + path + " or sub node " + subnodeName + " in repository " + repositoryName, t);
633 }
634 return set;
635 }
636
637 private void collectUniquePropertyNames(final Node node, final String repositoryName, final String subnodeName, final Collection<String> set, final boolean isDeep) throws RepositoryException {
638 MgnlContext.doInSystemContext(new JCRSessionOp<Void>(repositoryName) {
639
640 @Override
641 public Void exec(Session session) throws RepositoryException {
642 for (PropertyIterator iter = new FilteringPropertyIterator(node.getProperties(), NodeUtil.ALL_PROPERTIES_EXCEPT_JCR_AND_MGNL_FILTER); iter.hasNext();) {
643 Property property = iter.nextProperty();
644 final String uuid = property.getString();
645 try {
646 final Node targetNode = session.getNodeByIdentifier(uuid);
647 set.add(targetNode.getName());
648 if (isDeep && targetNode.hasNode(subnodeName)) {
649 collectUniquePropertyNames(targetNode.getNode(subnodeName), repositoryName, subnodeName, set, true);
650 }
651 } catch (ItemNotFoundException t) {
652 final String path = property.getPath();
653
654 log.warn("Can't find {} node by UUID {} referred by node {}", new Object[] { repositoryName, t.getMessage(), path });
655 log.debug("Failed while reading node by UUID", t);
656
657
658 }
659 }
660 return null;
661 }
662 });
663 }
664
665 @Override
666 public User addGroup(User user, String groupName) {
667 try {
668 super.add(user.getName(), groupName, NODE_GROUPS);
669 } catch (PrincipalNotFoundException e) {
670
671 return null;
672 }
673 return getUser(user.getName());
674 }
675
676 @Override
677 public User removeGroup(User user, String groupName) {
678 try {
679 super.remove(user.getName(), groupName, NODE_GROUPS);
680 } catch (PrincipalNotFoundException e) {
681
682 return null;
683 }
684 return getUser(user.getName());
685 }
686
687 @Override
688 public User removeRole(User user, String roleName) {
689 try {
690 super.remove(user.getName(), roleName, NODE_ROLES);
691 } catch (PrincipalNotFoundException e) {
692
693 return null;
694 }
695 return getUser(user.getName());
696 }
697 }