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