View Javadoc

1   /**
2    * This file Copyright (c) 2003-2011 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
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  import info.magnolia.cms.core.Content;
39  import info.magnolia.cms.core.HierarchyManager;
40  import info.magnolia.cms.core.ItemType;
41  import info.magnolia.cms.core.MetaData;
42  import info.magnolia.cms.core.MgnlNodeType;
43  import info.magnolia.cms.core.Path;
44  import info.magnolia.cms.security.auth.ACL;
45  import info.magnolia.context.MgnlContext;
46  import info.magnolia.jcr.util.NodeUtil;
47  import info.magnolia.jcr.util.PropertyUtil;
48  import info.magnolia.repository.RepositoryConstants;
49  
50  import java.util.ArrayList;
51  import java.util.Collection;
52  import java.util.Collections;
53  import java.util.GregorianCalendar;
54  import java.util.HashMap;
55  import java.util.HashSet;
56  import java.util.Iterator;
57  import java.util.List;
58  import java.util.Map;
59  import java.util.Set;
60  import java.util.SortedSet;
61  import java.util.TreeSet;
62  
63  import javax.jcr.ItemNotFoundException;
64  import javax.jcr.Node;
65  import javax.jcr.NodeIterator;
66  import javax.jcr.PathNotFoundException;
67  import javax.jcr.Property;
68  import javax.jcr.PropertyIterator;
69  import javax.jcr.RepositoryException;
70  import javax.jcr.Session;
71  import javax.jcr.Value;
72  import javax.jcr.ValueFormatException;
73  import javax.jcr.query.Query;
74  import javax.security.auth.Subject;
75  
76  import org.apache.commons.codec.binary.Base64;
77  import org.apache.commons.lang.StringUtils;
78  import org.slf4j.Logger;
79  import org.slf4j.LoggerFactory;
80  
81  
82  /**
83   * Manages the users stored in Magnolia itself.
84   * @version $Revision$ ($Author$)
85   */
86  public class MgnlUserManager extends RepositoryBackedSecurityManager implements UserManager {
87  
88      private static final Logger log = LoggerFactory.getLogger(MgnlUserManager.class);
89  
90      public static final String PROPERTY_EMAIL = "email";
91      public static final String PROPERTY_LANGUAGE = "language";
92      public static final String PROPERTY_LASTACCESS = "lastaccess";
93      public static final String PROPERTY_PASSWORD = "pswd";
94      public static final String PROPERTY_TITLE = "title";
95      public static final String PROPERTY_ENABLED = "enabled";
96  
97      public static final String NODE_ACLUSERS = "acl_users";
98  
99      private String realmName;
100 
101     private int maxFailedLoginAttempts;
102 
103     private int lockTimePeriod;
104 
105     /**
106      * There should be no need to instantiate this class except maybe for testing. Manual instantiation might cause manager not to be initialized properly.
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                     if(propertyValue == null && PropertyUtil.getProperty(userNode, propertyName) != null){
142                         userNode.getProperty(propertyName).remove();
143                     }else if(propertyValue != null){
144                         userNode.setProperty(propertyName, propertyValue);
145                     }
146                     session.save();
147                 }
148                 catch (RepositoryException e) {
149                     session.refresh(false);
150                     log.error("Property {} can't be changed. " + e.getMessage(), propertyName);
151                     return user;
152                 }
153                 return newUserInstance(userNode);
154             }
155         });
156     }
157 
158     /**
159      * TODO : rename to getRealmName and setRealmName (and make sure Content2Bean still sets realmName using the parent's node name).
160      * @deprecated since 4.5 use realmName instead
161      */
162     @Deprecated
163     public String getName() {
164         return getRealmName();
165     }
166 
167     /**
168      * @deprecated since 4.5 use realmName instead
169      */
170     @Deprecated
171     public void setName(String name) {
172         setRealmName(name);
173     }
174 
175     public void setRealmName(String name) {
176         this.realmName = name;
177     }
178 
179     public String getRealmName() {
180         return realmName;
181     }
182 
183     /**
184      * Get the user object. Uses a search
185      * @param name
186      * @return the user object
187      */
188     @Override
189     public User getUser(final String name) {
190         try {
191             return MgnlContext.doInSystemContext(new JCRSessionOp<User>(getRepositoryName()) {
192                 @Override
193                 public User exec(Session session) throws RepositoryException {
194                     Node priviledgedUserNode = findPrincipalNode(name, session);
195                     return newUserInstance(priviledgedUserNode);
196                 }
197                 @Override
198                 public String toString() {
199                     return "retrieve user " + name;
200                 }
201             });
202         } catch (RepositoryException e) {
203             e.printStackTrace();
204         }
205         return null;
206     }
207 
208     /**
209      * Get the user object. Uses a search
210      * @param id user identifier
211      * @return the user object
212      */
213     @Override
214     public User getUserById(final String id){  
215         try {
216             return MgnlContext.doInSystemContext(new JCRSessionOp<User>(getRepositoryName()) {
217                 @Override
218                 public User exec(Session session) throws RepositoryException {
219                     Node priviledgedUserNode = session.getNodeByIdentifier(id);
220                     return newUserInstance(priviledgedUserNode);
221                 }
222                 @Override
223                 public String toString() {
224                     return "retrieve user with id " + id;
225                 }
226             });
227         } catch (RepositoryException e) {
228             e.printStackTrace();
229         }
230         return null;
231     }
232 
233     @Override
234     public User getUser(Subject subject) throws UnsupportedOperationException {
235         // this could be the case if no one is logged in yet
236         if (subject == null) {
237             log.debug("subject not set.");
238             return new DummyUser();
239         }
240 
241         Set<User> principalSet = subject.getPrincipals(User.class);
242         Iterator<User> entityIterator = principalSet.iterator();
243         if (!entityIterator.hasNext()) {
244             // happens when JCR authentication module set to optional and user doesn't exist in magnolia
245             log.debug("user name not contained in principal set.");
246             return new DummyUser();
247         }
248         return entityIterator.next();
249     }
250 
251     /**
252      * Helper method to find a user in a certain realm. Uses JCR Query.
253      * @deprecated since 4.5 use findPrincipalNode(java.lang.String, javax.jcr.Session) instead
254      */
255     @Deprecated
256     protected Content findUserNode(String realm, String name) throws RepositoryException {
257         // while we could call the other findUserNode method and wrap the output it would be inappropriate as session is not valid outside of the call
258         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.");
259     }
260 
261     /**
262      * Helper method to find a user in a certain realm. Uses JCR Query.
263      */
264     @Override
265     protected Node findPrincipalNode(String name, Session session) throws RepositoryException {
266         String realmName = getRealmName();
267         final String where;
268         // the all realm searches the repository
269         if (Realm.REALM_ALL.getName().equals(realmName)) {
270             where = "where name() = '" + name + "'";
271         } else {
272             // FIXME: DOUBLE CHECK THE QUERY FOR REALMS ... ISDESCENDANTNODE and NAME ....
273             where = "where name() = '" + name + "' and isdescendantnode(['/" + realmName + "'])";
274             //            where = "where [jcr:path] = '/" + realm + "/" + name + "'"
275             //            + " or [jcr:path] like '/" + realm + "/%/" + name + "'";
276         }
277 
278         final String statement = "select * from [" + MgnlNodeType.USER + "] " + where;
279 
280         Query query = session.getWorkspace().getQueryManager().createQuery(statement, Query.JCR_SQL2);
281         NodeIterator iter = query.execute().getNodes();
282         Node user = null;
283         while (iter.hasNext()) {
284             Node node = iter.nextNode();
285             if (node.isNodeType(ItemType.USER.getSystemName())) {
286                 user = node;
287                 break;
288             }
289         }
290         if (iter.hasNext()) {
291             log.error("More than one user found with name [{}] in realm [{}]");
292         }
293         return user;
294     }
295 
296     protected User getFromRepository(String name) throws RepositoryException {
297         final Content node = findUserNode(this.realmName, name);
298         if (node == null) {
299             log.debug("User not found: [{}]", name);
300             return null;
301         }
302 
303         return newUserInstance(node);
304     }
305 
306     /**
307      * SystemUserManager does this.
308      */
309     @Override
310     public User getSystemUser() throws UnsupportedOperationException {
311         throw new UnsupportedOperationException();
312     }
313 
314     /**
315      * SystemUserManager does this.
316      */
317     @Override
318     public User getAnonymousUser() throws UnsupportedOperationException {
319         throw new UnsupportedOperationException();
320     }
321 
322     /**
323      * Get all users managed by this user manager.
324      */
325     @Override
326     public Collection<User> getAllUsers() {
327         return MgnlContext.doInSystemContext(new SilentSessionOp<Collection<User>>(getRepositoryName()) {
328 
329             @Override
330             public Collection<User> doExec(Session session) throws RepositoryException {
331                 List<User> users = new ArrayList<User>();
332                 Node node = session.getNode("/" + realmName);
333                 updateUserListWithAllChildren(node, users);
334                 return users;
335             }
336 
337             @Override
338             public String toString() {
339                 return "get all users";
340             }
341 
342         });
343     }
344 
345     /**
346      * Updates collection with all users located under provided node.
347      * @throws RepositoryException
348      */
349     public void updateUserListWithAllChildren(Node node, Collection<User> users) throws RepositoryException{
350         NodeIterator nodesIter = node.getNodes();
351         Collection<Node> nodes = new HashSet<Node>();
352         Collection<Node> folders = new HashSet<Node>();
353         while(nodesIter.hasNext()){
354             Node newNode = (Node) nodesIter.next();
355             if(newNode.isNodeType(MgnlNodeType.USER)){
356                 nodes.add(newNode);
357             }else if(newNode.isNodeType(MgnlNodeType.NT_FOLDER)){
358                 folders.add(newNode);
359             }
360         }
361 
362         if(!nodes.isEmpty()){
363             for (Node userNode : nodes) {
364                 users.add(newUserInstance(userNode));
365             }
366         }
367         if(!folders.isEmpty()){
368             Iterator<Node> it = folders.iterator();
369             while(it.hasNext()){
370                 updateUserListWithAllChildren(it.next(), users);
371             }
372         }
373     }
374 
375     @Override
376     public User createUser(final String name, final String pw) {
377         validateUsername(name);
378         return MgnlContext.doInSystemContext(new SilentSessionOp<MgnlUser>(getRepositoryName()) {
379 
380             @Override
381             public MgnlUser doExec(Session session) throws RepositoryException {
382                 Node userNode = session.getNode("/" + getRealmName()).addNode(name,ItemType.USER.getSystemName());
383                 userNode.setProperty("name", name);
384                 setPasswordProperty(userNode, pw);
385                 userNode.setProperty("language", "en");
386 
387                 final String handle = userNode.getPath();
388                 final Node acls = userNode.addNode(NODE_ACLUSERS, ItemType.CONTENTNODE.getSystemName());
389                 // read only access to the node itself
390                 Node acl = acls.addNode(Path.getUniqueLabel(session, acls.getPath(), "0"), ItemType.CONTENTNODE.getSystemName());
391                 acl.setProperty("path", handle);
392                 acl.setProperty("permissions", Permission.READ);
393                 // those who had access to their nodes should get access to their own props
394                 addWrite(handle, PROPERTY_EMAIL, acls);
395                 addWrite(handle, PROPERTY_LANGUAGE, acls);
396                 addWrite(handle, PROPERTY_LASTACCESS, acls);
397                 addWrite(handle, PROPERTY_PASSWORD, acls);
398                 addWrite(handle, PROPERTY_TITLE, acls);
399                 // and of course the meta data
400                 addWrite(handle, MetaData.DEFAULT_META_NODE, acls);
401                 session.save();
402                 return new MgnlUser(userNode.getName(), getRealmName(), Collections.EMPTY_LIST, Collections.EMPTY_LIST, Collections.EMPTY_MAP,  NodeUtil.getPathIfPossible(userNode), NodeUtil.getNodeIdentifierIfPossible(userNode));
403             }
404 
405             @Override
406             public String toString() {
407                 return "create user " + name;
408             }
409         });
410     }
411 
412     @Override
413     public User changePassword(final User user, final String newPassword) {
414         return MgnlContext.doInSystemContext(new SilentSessionOp<User>(getRepositoryName()) {
415 
416             @Override
417             public User doExec(Session session) throws RepositoryException {
418                 Node userNode = session.getNode("/" + getRealmName() + "/" + user.getName());
419                 setPasswordProperty(userNode, newPassword);
420 
421                 session.save();
422                 return newUserInstance(userNode);
423             }
424 
425             @Override
426             public String toString() {
427                 return "change password of user " + user.getName();
428             }
429         });
430     }
431 
432     /**
433      * @deprecated since 4.5 use {@link #setPasswordProperty(Node, String)} instead
434      */
435     @Deprecated
436     protected void setPasswordProperty(Content userNode, String clearPassword) throws RepositoryException {
437         setPasswordProperty(userNode.getJCRNode(), clearPassword);
438     }
439 
440 
441     protected void setPasswordProperty(Node userNode, String clearPassword) throws RepositoryException {
442         userNode.setProperty(PROPERTY_PASSWORD, encodePassword(clearPassword));
443     }
444 
445     protected String encodePassword(String clearPassword) {
446         return new String(Base64.encodeBase64(clearPassword.getBytes()));
447     }
448 
449     protected void validateUsername(String name) {
450         if (StringUtils.isBlank(name)) {
451             throw new IllegalArgumentException(name + " is not a valid username.");
452         }
453     }
454 
455     protected Content createUserNode(String name) throws RepositoryException {
456         final String path = "/" + getRealmName();
457         return getHierarchyManager().createContent(path, name, ItemType.USER.getSystemName());
458     }
459 
460     /**
461      * Return the HierarchyManager for the user workspace (through the system context).
462      */
463     protected HierarchyManager getHierarchyManager() {
464         return MgnlContext.getSystemContext().getHierarchyManager(RepositoryConstants.USERS);
465     }
466 
467     /**
468      * @deprecated since 4.3.1 - use {@link #newUserInstance(javax.jcr.Node)}
469      */
470     @Deprecated
471     protected MgnlUser userInstance(Content node) {
472         try {
473             return (MgnlUser) newUserInstance(node.getJCRNode());
474         } catch (RepositoryException e) {
475             log.error(e.getMessage(), e);
476             return null;
477         }
478     }
479 
480     /**
481      * Creates a {@link MgnlUser} out of a jcr node. Can be overridden in order to provide a different implementation.
482      * @since 4.3.1
483      * @deprecated since 4.5 use newUSerInstance(javax.jcr.Node) instead
484      */
485     @Deprecated
486     protected User newUserInstance(Content node) {
487         try {
488             return newUserInstance(node.getJCRNode());
489         } catch (RepositoryException e) {
490             log.error(e.getMessage(), e);
491             return null;
492         }
493     }
494 
495     private Node addWrite(String parentPath, String property, Node acls) throws PathNotFoundException, RepositoryException, AccessDeniedException {
496         Node acl = acls.addNode(Path.getUniqueLabel(acls.getSession(), acls.getPath(), "0"), ItemType.CONTENTNODE.getSystemName());
497         acl.setProperty("path", parentPath + "/" + property);
498         acl.setProperty("permissions", Permission.ALL);
499         return acl;
500     }
501 
502     @Override
503     public void updateLastAccessTimestamp(final User user) throws UnsupportedOperationException {
504         MgnlContext.doInSystemContext(new SilentSessionOp<Void>(getRepositoryName()) {
505 
506             @Override
507             public Void doExec(Session session) throws RepositoryException {
508                 String path = ((MgnlUser) user).getPath();
509                 log.debug("update access timestamp for {}", user.getName());
510                 try {
511                     Node userNode = session.getNode(path);
512                     PropertyUtil.updateOrCreate(userNode, "lastaccess", new GregorianCalendar());
513                     session.save();
514                 }
515                 catch (RepositoryException e) {
516                     session.refresh(false);
517                 }
518                 return null;
519             }
520             @Override
521             public String toString() {
522                 return "update user "+user.getName()+" last access time stamp";
523             }
524         });
525     }
526 
527     protected User newUserInstance(Node privilegedUserNode) throws ValueFormatException, PathNotFoundException, RepositoryException {
528         if (privilegedUserNode == null) {
529             return null;
530         }
531         Set<String> roles = collectUniquePropertyNames(privilegedUserNode, "roles", RepositoryConstants.USER_ROLES, false);
532         Set<String> groups = collectUniquePropertyNames(privilegedUserNode, "groups", RepositoryConstants.USER_GROUPS, false);
533 
534         Map<String, String> properties = new HashMap<String, String>();
535         for (PropertyIterator iter = privilegedUserNode.getProperties(); iter.hasNext(); ) {
536             Property prop = iter.nextProperty();
537             if (prop.getName().startsWith(MgnlNodeType.JCR_PREFIX) || prop.getName().startsWith(MgnlNodeType.MGNL_PREFIX)) {
538                 // skip special props
539                 continue;
540             }
541             //TODO: should we check and skip binary props in case someone adds image to the user?
542             properties.put(prop.getName(), prop.getString());
543         }
544 
545         MgnlUser user = new MgnlUser(privilegedUserNode.getName(), getRealmName(), groups, roles, properties, NodeUtil.getPathIfPossible(privilegedUserNode), NodeUtil.getNodeIdentifierIfPossible(privilegedUserNode));
546         return user;
547     }
548 
549     @Override
550     protected String getRepositoryName() {
551         return RepositoryConstants.USERS;
552     }
553 
554     /**
555      * Sets access control list from a list of roles under the provided content object.
556      */
557     @Override
558     public Map<String, ACL> getACLs(final User user) {
559         if (!(user instanceof MgnlUser)) {
560             return null;
561         }
562         return super.getACLs(user.getName());
563     }
564 
565     @Override
566     public User addRole(User user, String roleName) {
567         try {
568             super.add(user.getName(), roleName, NODE_ROLES);
569         } catch (PrincipalNotFoundException e) {
570             // user doesn't exist in this UM
571             return null;
572         }
573         return getUser(user.getName());
574     }
575 
576     /**
577      * Collects all property names of given type, sorting them (case insensitive) and removing duplicates in the process.
578      */
579     private Set<String> collectUniquePropertyNames(Node rootNode, String subnodeName, String repositoryName, boolean isDeep) {
580         final SortedSet<String> set = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
581         String path = null;
582         try {
583             path = rootNode.getPath();
584             final Node node = rootNode.getNode(subnodeName);
585             collectUniquePropertyNames(node, repositoryName, subnodeName, set, isDeep);
586             collectUniquePropertyNames(rootNode.getNode(subnodeName), repositoryName, subnodeName, set, isDeep);
587         } catch (PathNotFoundException e) {
588             log.debug("{} does not have any {}", path, repositoryName);
589         } catch (Throwable t) {
590             log.error("Failed to read " + path + " or sub node " + subnodeName + " in repository " + repositoryName, t);
591         }
592         return set;
593     }
594 
595     private void collectUniquePropertyNames(final Node node, final String repositoryName, final String subnodeName, final Collection<String> set, final boolean isDeep) throws RepositoryException {
596         MgnlContext.doInSystemContext(new JCRSessionOp<Void>(repositoryName) {
597 
598             @Override
599             public Void exec(Session session) throws RepositoryException {
600                 for (PropertyIterator props = node.getProperties(); props.hasNext();) {
601                     Property property = props.nextProperty();
602                     if (property.getName().startsWith(MgnlNodeType.JCR_PREFIX)) {
603                         continue;
604                     }
605                     final String uuid = property.getString();
606                     try {
607                         final Node targetNode = session.getNodeByIdentifier(uuid);
608                         set.add(targetNode.getName());
609                         if (isDeep && targetNode.hasNode(subnodeName)) {
610                             collectUniquePropertyNames(targetNode.getNode(subnodeName), repositoryName, subnodeName, set, true);
611                         }
612                     } catch (ItemNotFoundException t) {
613                         final String path = property.getPath();
614                         // TODO: why we are using UUIDs here? shouldn't be better to use group names, since uuids can change???
615                         log.warn("Can't find {} node by UUID {} referred by node {}", new Object[]{repositoryName, t.getMessage(), path});
616                         log.debug("Failed while reading node by UUID", t);
617                         // we continue since it can happen that target node is removed
618                         // - UUID's are kept as simple strings thus have no referential integrity
619                     }
620                 }
621                 return null;
622             }
623         });
624     }
625 
626     @Override
627     public User addGroup(User user, String groupName) {
628         try {
629             super.add(user.getName(), groupName, NODE_GROUPS);
630         } catch (PrincipalNotFoundException e) {
631             // user doesn't exist in this UM
632             return null;
633         }
634         return getUser(user.getName());
635     }
636 
637     @Override
638     public User removeGroup(User user, String groupName) {
639         super.remove(user.getName(), groupName, NODE_GROUPS);
640         return getUser(user.getName());
641     }
642 
643     @Override
644     public User removeRole(User user, String roleName) {
645         super.remove(user.getName(), roleName, NODE_ROLES);
646         return getUser(user.getName());
647     }
648 }