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