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