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.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.query.Query;
73  import javax.security.auth.Subject;
74  
75  import org.apache.commons.codec.binary.Base64;
76  import org.apache.commons.lang.StringUtils;
77  import org.slf4j.Logger;
78  import org.slf4j.LoggerFactory;
79  
80  
81  /**
82   * Manages the users stored in Magnolia itself.
83   * @version $Revision: 52456 $ ($Author: ehechinger $)
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 int maxFailedLoginAttempts;
101 
102     private int lockTimePeriod;
103 
104     /**
105      * There should be no need to instantiate this class except maybe for testing. Manual instantiation might cause manager not to be initialized properly.
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         MgnlContext.doInSystemContext(new SilentSessionOp<Void>(getRepositoryName()) {
133 
134             @Override
135             public Void doExec(Session session) throws RepositoryException {
136                 String path = ((MgnlUser) user).getPath();
137                 try {
138                     Node userNode = session.getNode(path);
139                     if(propertyValue == null && PropertyUtil.getProperty(userNode, propertyName) != null){
140                         userNode.getProperty(propertyName).remove();
141                     }else if(propertyValue != null){
142                         userNode.setProperty(propertyName, propertyValue);
143                     }
144                     session.save();
145                 }
146                 catch (RepositoryException e) {
147                     session.refresh(false);
148                 }
149                 return null;
150             }
151         });
152         return user;
153     }
154 
155     /**
156      * TODO : rename to getRealmName and setRealmName (and make sure Content2Bean still sets realmName using the parent's node name).
157      * @deprecated since 4.5 use realmName instead
158      */
159     @Deprecated
160     public String getName() {
161         return getRealmName();
162     }
163 
164     /**
165      * @deprecated since 4.5 use realmName instead
166      */
167     @Deprecated
168     public void setName(String name) {
169         setRealmName(name);
170     }
171 
172     public void setRealmName(String name) {
173         this.realmName = name;
174     }
175 
176     public String getRealmName() {
177         return realmName;
178     }
179 
180     /**
181      * Get the user object. Uses a search
182      * @param name
183      * @return the user object
184      */
185     @Override
186     public User getUser(final String name) {
187         try {
188             return MgnlContext.doInSystemContext(new JCRSessionOp<User>(getRepositoryName()) {
189                 @Override
190                 public User exec(Session session) throws RepositoryException {
191                     Node priviledgedUserNode = findPrincipalNode(name, session);
192                     return newUserInstance(priviledgedUserNode);
193                 }
194                 @Override
195                 public String toString() {
196                     return "retrieve user " + name;
197                 }
198             });
199         } catch (RepositoryException e) {
200             e.printStackTrace();
201         }
202         return null;
203     }
204 
205     @Override
206     public User getUser(Subject subject) throws UnsupportedOperationException {
207         // this could be the case if no one is logged in yet
208         if (subject == null) {
209             log.debug("subject not set.");
210             return new DummyUser();
211         }
212 
213         Set<User> principalSet = subject.getPrincipals(User.class);
214         Iterator<User> entityIterator = principalSet.iterator();
215         if (!entityIterator.hasNext()) {
216             // happens when JCR authentication module set to optional and user doesn't exist in magnolia
217             log.debug("user name not contained in principal set.");
218             return new DummyUser();
219         }
220         return entityIterator.next();
221     }
222 
223     /**
224      * Helper method to find a user in a certain realm. Uses JCR Query.
225      * @deprecated since 4.5 use findPrincipalNode(java.lang.String, javax.jcr.Session) instead
226      */
227     @Deprecated
228     protected Content findUserNode(String realm, String name) throws RepositoryException {
229         // 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
230         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.");
231     }
232 
233     /**
234      * Helper method to find a user in a certain realm. Uses JCR Query.
235      */
236     @Override
237     protected Node findPrincipalNode(String name, Session session) throws RepositoryException {
238         String realmName = getRealmName();
239         final String where;
240         // the all realm searches the repository
241         if (Realm.REALM_ALL.getName().equals(realmName)) {
242             where = "where name() = '" + name + "'";
243         } else {
244             // FIXME: DOUBLE CHECK THE QUERY FOR REALMS ... ISDESCENDANTNODE and NAME ....
245             where = "where name() = '" + name + "' and isdescendantnode(['/" + realmName + "'])";
246             //            where = "where [jcr:path] = '/" + realm + "/" + name + "'"
247             //            + " or [jcr:path] like '/" + realm + "/%/" + name + "'";
248         }
249 
250         final String statement = "select * from [" + ItemType.USER + "] " + where;
251 
252         Query query = session.getWorkspace().getQueryManager().createQuery(statement, Query.JCR_SQL2);
253         NodeIterator iter = query.execute().getNodes();
254         Node user = null;
255         while (iter.hasNext()) {
256             Node node = iter.nextNode();
257             if (node.isNodeType(ItemType.USER.getSystemName())) {
258                 user = node;
259                 break;
260             }
261         }
262         if (iter.hasNext()) {
263             log.error("More than one user found with name [{}] in realm [{}]");
264         }
265         return user;
266     }
267 
268     protected User getFromRepository(String name) throws RepositoryException {
269         final Content node = findUserNode(this.realmName, name);
270         if (node == null) {
271             log.debug("User not found: [{}]", name);
272             return null;
273         }
274 
275         return newUserInstance(node);
276     }
277 
278     /**
279      * SystemUserManager does this.
280      */
281     @Override
282     public User getSystemUser() throws UnsupportedOperationException {
283         throw new UnsupportedOperationException();
284     }
285 
286     /**
287      * SystemUserManager does this.
288      */
289     @Override
290     public User getAnonymousUser() throws UnsupportedOperationException {
291         throw new UnsupportedOperationException();
292     }
293 
294     /**
295      * Get all users managed by this user manager.
296      */
297     @Override
298     public Collection<User> getAllUsers() {
299         return MgnlContext.doInSystemContext(new SilentSessionOp<Collection<User>>(getRepositoryName()) {
300 
301             @Override
302             public Collection<User> doExec(Session session) throws RepositoryException {
303                 List<User> users = new ArrayList<User>();
304                 Node node = session.getNode("/" + realmName);
305                 updateUserListWithAllChildren(node, users);
306                 return users;
307             }
308 
309             @Override
310             public String toString() {
311                 return "get all users";
312             }
313 
314         });
315     }
316 
317     /**
318      * Updates collection with all users located under provided node.
319      * @throws RepositoryException
320      */
321     public void updateUserListWithAllChildren(Node node, Collection<User> users) throws RepositoryException{
322         NodeIterator nodesIter = node.getNodes();
323         Collection<Node> nodes = new HashSet<Node>();
324         Collection<Node> folders = new HashSet<Node>();
325         while(nodesIter.hasNext()){
326             Node newNode = (Node) nodesIter.next();
327             if(newNode.isNodeType(MgnlNodeType.USER)){
328                 nodes.add(newNode);
329             }else if(newNode.isNodeType(MgnlNodeType.NT_FOLDER)){
330                 folders.add(newNode);
331             }
332         }
333 
334         if(!nodes.isEmpty()){
335             for (Node userNode : nodes) {
336                 users.add(newUserInstance(userNode));
337             }
338         }
339         if(!folders.isEmpty()){
340             Iterator<Node> it = folders.iterator();
341             while(it.hasNext()){
342                 updateUserListWithAllChildren(it.next(), users);
343             }
344         }
345     }
346 
347     @Override
348     public User createUser(final String name, final String pw) {
349         validateUsername(name);
350         return MgnlContext.doInSystemContext(new SilentSessionOp<MgnlUser>(getRepositoryName()) {
351 
352             @Override
353             public MgnlUser doExec(Session session) throws RepositoryException {
354                 Node userNode = session.getNode("/" + getRealmName()).addNode(name,ItemType.USER.getSystemName());
355                 userNode.setProperty("name", name);
356                 setPasswordProperty(userNode, pw);
357                 userNode.setProperty("language", "en");
358 
359                 final String handle = userNode.getPath();
360                 final Node acls = userNode.addNode(NODE_ACLUSERS, ItemType.CONTENTNODE.getSystemName());
361                 // read only access to the node itself
362                 Node acl = acls.addNode(Path.getUniqueLabel(session, acls.getPath(), "0"), ItemType.CONTENTNODE.getSystemName());
363                 acl.setProperty("path", handle);
364                 acl.setProperty("permissions", Permission.READ);
365                 // those who had access to their nodes should get access to their own props
366                 addWrite(handle, PROPERTY_EMAIL, acls);
367                 addWrite(handle, PROPERTY_LANGUAGE, acls);
368                 addWrite(handle, PROPERTY_LASTACCESS, acls);
369                 addWrite(handle, PROPERTY_PASSWORD, acls);
370                 addWrite(handle, PROPERTY_TITLE, acls);
371                 // and of course the meta data
372                 addWrite(handle, MetaData.DEFAULT_META_NODE, acls);
373                 session.save();
374                 return new MgnlUser(userNode.getName(), getRealmName(), Collections.EMPTY_LIST, Collections.EMPTY_LIST, Collections.EMPTY_MAP);
375             }
376 
377             @Override
378             public String toString() {
379                 return "create user " + name;
380             }
381         });
382     }
383 
384     @Override
385     public User changePassword(final User user, final String newPassword) {
386         return MgnlContext.doInSystemContext(new SilentSessionOp<User>(getRepositoryName()) {
387 
388             @Override
389             public User doExec(Session session) throws RepositoryException {
390                 Node userNode = session.getNode("/" + getRealmName() + "/" + user.getName());
391                 setPasswordProperty(userNode, newPassword);
392 
393                 session.save();
394                 return newUserInstance(userNode);
395             }
396 
397             @Override
398             public String toString() {
399                 return "change password of user " + user.getName();
400             }
401         });
402     }
403 
404     /**
405      * @deprecated since 4.5 use {@link #setPasswordProperty(Node, String)} instead
406      */
407     @Deprecated
408     protected void setPasswordProperty(Content userNode, String clearPassword) throws RepositoryException {
409         setPasswordProperty(userNode.getJCRNode(), clearPassword);
410     }
411 
412 
413     protected void setPasswordProperty(Node userNode, String clearPassword) throws RepositoryException {
414         userNode.setProperty(PROPERTY_PASSWORD, encodePassword(clearPassword));
415     }
416 
417     protected String encodePassword(String clearPassword) {
418         return new String(Base64.encodeBase64(clearPassword.getBytes()));
419     }
420 
421     protected void validateUsername(String name) {
422         if (StringUtils.isBlank(name)) {
423             throw new IllegalArgumentException(name + " is not a valid username.");
424         }
425     }
426 
427     protected Content createUserNode(String name) throws RepositoryException {
428         final String path = "/" + getRealmName();
429         return getHierarchyManager().createContent(path, name, ItemType.USER.getSystemName());
430     }
431 
432     /**
433      * Return the HierarchyManager for the user workspace (through the system context).
434      */
435     protected HierarchyManager getHierarchyManager() {
436         return MgnlContext.getSystemContext().getHierarchyManager(RepositoryConstants.USERS);
437     }
438 
439     /**
440      * @deprecated since 4.3.1 - use {@link #newUserInstance(javax.jcr.Node)}
441      */
442     @Deprecated
443     protected MgnlUser userInstance(Content node) {
444         try {
445             return (MgnlUser) newUserInstance(node.getJCRNode());
446         } catch (RepositoryException e) {
447             log.error(e.getMessage(), e);
448             return null;
449         }
450     }
451 
452     /**
453      * Creates a {@link MgnlUser} out of a jcr node. Can be overridden in order to provide a different implementation.
454      * @since 4.3.1
455      * @deprecated since 4.5 use newUSerInstance(javax.jcr.Node) instead
456      */
457     @Deprecated
458     protected User newUserInstance(Content node) {
459         try {
460             return newUserInstance(node.getJCRNode());
461         } catch (RepositoryException e) {
462             log.error(e.getMessage(), e);
463             return null;
464         }
465     }
466 
467     private Node addWrite(String parentPath, String property, Node acls) throws PathNotFoundException, RepositoryException, AccessDeniedException {
468         Node acl = acls.addNode(Path.getUniqueLabel(acls.getSession(), acls.getPath(), "0"), ItemType.CONTENTNODE.getSystemName());
469         acl.setProperty("path", parentPath + "/" + property);
470         acl.setProperty("permissions", Permission.ALL);
471         return acl;
472     }
473 
474     @Override
475     public void updateLastAccessTimestamp(final User user) throws UnsupportedOperationException {
476         MgnlContext.doInSystemContext(new SilentSessionOp<Void>(getRepositoryName()) {
477 
478             @Override
479             public Void doExec(Session session) throws RepositoryException {
480                 String path = ((MgnlUser) user).getPath();
481                 log.debug("update access timestamp for {}", user.getName());
482                 try {
483                     Node userNode = session.getNode(path);
484                     PropertyUtil.updateOrCreate(userNode, "lastaccess", new GregorianCalendar());
485                     session.save();
486                 }
487                 catch (RepositoryException e) {
488                     session.refresh(false);
489                 }
490                 return null;
491             }
492             @Override
493             public String toString() {
494                 return "update user "+user.getName()+" last access time stamp";
495             }
496         });
497     }
498 
499     protected User newUserInstance(Node privilegedUserNode) throws ValueFormatException, PathNotFoundException, RepositoryException {
500         if (privilegedUserNode == null) {
501             return null;
502         }
503         Set<String> roles = collectUniquePropertyNames(privilegedUserNode, "roles", RepositoryConstants.USER_ROLES, false);
504         Set<String> groups = collectUniquePropertyNames(privilegedUserNode, "groups", RepositoryConstants.USER_GROUPS, false);
505 
506         Map<String, String> properties = new HashMap<String, String>();
507         for (PropertyIterator iter = privilegedUserNode.getProperties(); iter.hasNext(); ) {
508             Property prop = iter.nextProperty();
509             if (prop.getName().startsWith(MgnlNodeType.JCR_PREFIX) || prop.getName().startsWith(MgnlNodeType.MGNL_PREFIX)) {
510                 // skip special props
511                 continue;
512             }
513             //TODO: should we check and skip binary props in case someone adds image to the user?
514             properties.put(prop.getName(), prop.getString());
515         }
516 
517         MgnlUser user = new MgnlUser(privilegedUserNode.getName(), getRealmName(), groups, roles, properties);
518         // keep just a token to user, not the whole node
519         // TODO: would it be better to keep around UUID?
520         user.setPath(privilegedUserNode.getPath());
521 
522         return user;
523     }
524 
525     @Override
526     protected String getRepositoryName() {
527         return RepositoryConstants.USERS;
528     }
529 
530     /**
531      * Sets access control list from a list of roles under the provided content object.
532      */
533     @Override
534     public Map<String, ACL> getACLs(final User user) {
535         if (!(user instanceof MgnlUser)) {
536             return null;
537         }
538         return super.getACLs(user.getName());
539     }
540 
541     @Override
542     public User addRole(User user, String roleName) {
543         try {
544             super.add(user.getName(), roleName, NODE_ROLES);
545         } catch (PrincipalNotFoundException e) {
546             // user doesn't exist in this UM
547             return null;
548         }
549         return getUser(user.getName());
550     }
551 
552     /**
553      * Collects all property names of given type, sorting them (case insensitive) and removing duplicates in the process.
554      */
555     private Set<String> collectUniquePropertyNames(Node rootNode, String subnodeName, String repositoryName, boolean isDeep) {
556         final SortedSet<String> set = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
557         String path = null;
558         try {
559             path = rootNode.getPath();
560             final Node node = rootNode.getNode(subnodeName);
561             collectUniquePropertyNames(node, repositoryName, subnodeName, set, isDeep);
562             collectUniquePropertyNames(rootNode.getNode(subnodeName), repositoryName, subnodeName, set, isDeep);
563         } catch (PathNotFoundException e) {
564             log.debug("{} does not have any {}", path, repositoryName);
565         } catch (Throwable t) {
566             log.error("Failed to read " + path + " or sub node " + subnodeName + " in repository " + repositoryName, t);
567         }
568         return set;
569     }
570 
571     private void collectUniquePropertyNames(final Node node, final String repositoryName, final String subnodeName, final Collection<String> set, final boolean isDeep) throws RepositoryException {
572         MgnlContext.doInSystemContext(new JCRSessionOp<Void>(repositoryName) {
573 
574             @Override
575             public Void exec(Session session) throws RepositoryException {
576                 for (PropertyIterator props = node.getProperties(); props.hasNext();) {
577                     Property property = props.nextProperty();
578                     if (property.getName().startsWith(MgnlNodeType.JCR_PREFIX)) {
579                         continue;
580                     }
581                     final String uuid = property.getString();
582                     try {
583                         final Node targetNode = session.getNodeByIdentifier(uuid);
584                         set.add(targetNode.getName());
585                         if (isDeep && targetNode.hasNode(subnodeName)) {
586                             collectUniquePropertyNames(targetNode.getNode(subnodeName), repositoryName, subnodeName, set, true);
587                         }
588                     } catch (ItemNotFoundException t) {
589                         final String path = property.getPath();
590                         // TODO: why we are using UUIDs here? shouldn't be better to use group names, since uuids can change???
591                         log.warn("Can't find {} node by UUID {} referred by node {}", new Object[]{repositoryName, t.getMessage(), path});
592                         log.debug("Failed while reading node by UUID", t);
593                         // we continue since it can happen that target node is removed
594                         // - UUID's are kept as simple strings thus have no referential integrity
595                     }
596                 }
597                 return null;
598             }
599         });
600     }
601 
602     @Override
603     public User addGroup(User user, String groupName) {
604         try {
605             super.add(user.getName(), groupName, NODE_GROUPS);
606         } catch (PrincipalNotFoundException e) {
607             // user doesn't exist in this UM
608             return null;
609         }
610         return getUser(user.getName());
611     }
612 
613     @Override
614     public User removeGroup(User user, String groupName) {
615         super.remove(user.getName(), groupName, NODE_GROUPS);
616         return getUser(user.getName());
617     }
618 
619     @Override
620     public User removeRole(User user, String roleName) {
621         super.remove(user.getName(), roleName, NODE_ROLES);
622         return getUser(user.getName());
623     }
624 }