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