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.context.MgnlContext;
40  import info.magnolia.repository.RepositoryConstants;
41  
42  import java.io.Serializable;
43  import java.util.ArrayList;
44  import java.util.Calendar;
45  import java.util.Collection;
46  import java.util.Collections;
47  import java.util.List;
48  import java.util.Map;
49  import java.util.Set;
50  import java.util.TreeSet;
51  
52  import javax.jcr.ItemNotFoundException;
53  import javax.jcr.Node;
54  import javax.jcr.Property;
55  import javax.jcr.PropertyIterator;
56  import javax.jcr.RepositoryException;
57  import javax.jcr.Session;
58  
59  import org.apache.commons.codec.binary.Base64;
60  import org.apache.commons.lang.StringUtils;
61  import org.slf4j.Logger;
62  import org.slf4j.LoggerFactory;
63  
64  /**
65   * User for 4.5 instance
66   * In difference from old MgnlUser, this class operates directly on JCR session and with JCR nodes/properties as our hierarchy managers are not
67   * available at the login time.
68   * Also in difference from MgnlUser, this class doesn't keep around instance of the user node! TODO: Test performance impact of such change.
69   * @author had
70   * @version $Id: MgnlUser.java 51822 2011-11-22 13:37:32Z ochytil $
71   */
72  public class MgnlUser extends AbstractUser implements User, Serializable {
73  
74      private static final long serialVersionUID = 222L;
75  
76      private static final Logger log = LoggerFactory.getLogger(MgnlUser.class);
77  
78      private final Map<String, String> properties;
79      private final Collection<String> groups;
80      private final Collection<String> roles;
81  
82      private final String name;
83      private final String language;
84      private final String encodedPassword;
85      private boolean enabled = true;
86      private String path;
87  
88      private final String realm;
89  
90      public MgnlUser(String name, String realm, Collection<String> groups, Collection<String> roles, Map<String, String> properties) {
91          this.name = name;
92          this.roles = Collections.unmodifiableCollection(roles);
93          this.groups = Collections.unmodifiableCollection(groups);
94          this.properties = Collections.unmodifiableMap(properties);
95          this.realm = realm;
96  
97          //shortcut some often accessed props so we don't have to search hashmap for them.
98          language = properties.get(MgnlUserManager.PROPERTY_LANGUAGE);
99          String enbld = properties.get(MgnlUserManager.PROPERTY_ENABLED);
100         // all accounts are enabled by default and prop doesn't exist if the account was not disabled before
101         enabled = enbld == null ? true : Boolean.parseBoolean(properties.get(MgnlUserManager.PROPERTY_ENABLED));
102         encodedPassword = properties.get(MgnlUserManager.PROPERTY_PASSWORD);
103     }
104 
105     /**
106      * Is this user in a specified group?
107      * @param groupName the name of the group
108      * @return true if in group
109      */
110     @Override
111     public boolean inGroup(String groupName) {
112         log.debug("inGroup({})", groupName);
113         return this.hasAny(groupName, NODE_GROUPS);
114     }
115 
116     /**
117      * Remove a group. Implementation is optional
118      * @param groupName
119      */
120     @Override
121     public void removeGroup(String groupName) throws UnsupportedOperationException {
122         log.debug("removeGroup({})", groupName);
123         throw new UnsupportedOperationException("use manager to remove groups!");
124     }
125 
126     /**
127      * Adds this user to a group. Implementation is optional
128      * @param groupName
129      */
130     @Override
131     public void addGroup(String groupName) throws UnsupportedOperationException {
132         log.debug("addGroup({})", groupName);
133         throw new UnsupportedOperationException("use manager to add groups!");
134     }
135 
136     @Override
137     public boolean isEnabled() {
138         log.debug("isEnabled()");
139         return enabled ;
140     }
141 
142     /**
143      * This methods sets flag just on the bean. It does not update persisted user data. Use manager to update user data.
144      */
145     @Override
146     public void setEnabled(boolean enabled) {
147         log.debug("setEnabled({})", enabled);
148         this.enabled = enabled;
149     }
150 
151     /**
152      * Is this user in a specified role?
153      * @param roleName the name of the role
154      * @return true if in role
155      */
156     @Override
157     public boolean hasRole(String roleName) {
158         return SecuritySupport.Factory.getInstance().getUserManager(getRealm()).hasAny(getName(), roleName, NODE_ROLES);
159     }
160 
161     @Override
162     public void removeRole(String roleName) throws UnsupportedOperationException {
163         log.debug("removeRole({})", roleName);
164         throw new UnsupportedOperationException("use manager to remove roles!");
165     }
166 
167     @Override
168     public void addRole(String roleName) throws UnsupportedOperationException {
169         log.debug("addRole({})", roleName);
170         throw new UnsupportedOperationException("use manager to add roles!");
171     }
172 
173     // TODO: methods like the ones below should not be in the object but rather in the manager, making object reusable with different managers.
174     private boolean hasAny(final String name, final String nodeName) {
175         long start = System.currentTimeMillis();
176         try {
177             String sessionName;
178             if (StringUtils.equalsIgnoreCase(nodeName, NODE_ROLES)) {
179                 sessionName = RepositoryConstants.USER_ROLES;
180             } else {
181                 sessionName = RepositoryConstants.USER_GROUPS;
182             }
183 
184             // TODO: this is an original code. If you ever need to speed it up, turn it around - retrieve group or role by its name and read its ID, then loop through IDs this user has assigned to find out if he has that one or not.
185             final Collection<String> groupsOrRoles = MgnlContext.doInSystemContext(new SilentSessionOp<Collection<String>>(RepositoryConstants.USERS) {
186 
187                 @Override
188                 public Collection<String> doExec(Session session) throws RepositoryException {
189                     Node groupsOrRoles = session.getNode(getName()).getNode(nodeName);
190                     List<String> list = new ArrayList<String>();
191                     for (PropertyIterator props = groupsOrRoles.getProperties(); props.hasNext();) {
192                         // check for the existence of this ID
193                         Property property = props.nextProperty();
194                         try {
195                             list.add(property.getString());
196                         } catch (ItemNotFoundException e) {
197                             log.debug("Role [{}] does not exist in the ROLES repository", name);
198                         } catch (IllegalArgumentException e) {
199                             log.debug("{} has invalid value", property.getPath());
200                         }
201                     }
202                     return list;
203                 }});
204 
205 
206             return MgnlContext.doInSystemContext(new JCRSessionOp<Boolean>(sessionName) {
207 
208                 @Override
209                 public Boolean exec(Session session) throws RepositoryException {
210                     for (String groupOrRole : groupsOrRoles) {
211                         // check for the existence of this ID
212                         try {
213                             if (session.getNodeByIdentifier(groupOrRole).getName().equalsIgnoreCase(name)) {
214                                 return true;
215                             }
216                         } catch (ItemNotFoundException e) {
217                             log.debug("Role [{}] does not exist in the ROLES repository", name);
218                         }
219                     }
220                     return false;
221                 }});
222 
223         } catch (RepositoryException e) {
224             log.debug(e.getMessage(), e);
225             //TODO: why are we swallowing exceptions silently here?
226         } finally {
227             log.debug("checked {} for {} in {}ms.", new Object[] {name, nodeName, (System.currentTimeMillis() - start)});
228         }
229         return false;
230     }
231 
232     public int getFailedLoginAttempts(){
233         return MgnlContext.doInSystemContext(new SilentSessionOp<Integer>(RepositoryConstants.USERS) {
234             @Override
235             public Integer doExec(Session session) throws RepositoryException {
236                 Node userNode = session.getNode("/" + getRealm() + "/" + getName());
237                 if (!userNode.hasProperty("failedLoginAttempts")){
238                     userNode.setProperty("failedLoginAttempts", 0);
239                     session.save();
240                 }
241                 return (int)userNode.getProperty("failedLoginAttempts").getLong();
242             }});
243     }
244 
245     public Calendar getReleaseTime(){
246         return MgnlContext.doInSystemContext(new SilentSessionOp<Calendar>(RepositoryConstants.USERS) {
247             @Override
248             public Calendar doExec(Session session) throws RepositoryException {
249                 Node userNode = session.getNode("/" + getRealm() + "/" + getName());
250                 if (!userNode.hasProperty("releaseTime")){
251                     userNode.setProperty("releaseTime", 0);
252                     session.save();
253                 }
254                 return userNode.getProperty("releaseTime").getDate();
255             }});
256     }
257 
258     @Override
259     public String getName() {
260         log.debug("getName()=>{}", name);
261         return name;
262     }
263 
264     @Override
265     public String getPassword() {
266         // TODO: should we really decode pwd here? Encoding is UM implementation specific
267         return decodePassword(encodedPassword);
268     }
269 
270     protected String decodePassword(String encodedPassword) {
271         return new String(Base64.decodeBase64(encodedPassword.getBytes()));
272     }
273 
274     @Override
275     public String getLanguage() {
276         log.debug("getLang()=>{}", language);
277         return this.language;
278     }
279 
280     @Override
281     public String getProperty(String propertyName) {
282         log.debug("getProperty({})", propertyName);
283         return properties.get(propertyName);
284     }
285 
286     @Override
287     public Collection<String> getGroups() {
288         log.debug("getGroups()");
289         return groups;
290     }
291 
292     @Override
293     public Collection<String> getAllGroups() {
294         // TODO: if the user is just a simple bean, then this method doesn't belong here anymore!!!!
295         // should be moved to user manager or to group manager???
296         log.debug("get groups for {}", getName());
297 
298         final Set<String> allGroups = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
299         final Collection<String> groups = getGroups();
300 
301         // FYI: can't initialize upfront as the instance of the user class needs to be created BEFORE repo is ready
302         GroupManager man = SecuritySupport.Factory.getInstance().getGroupManager();
303 
304         // add all subbroups
305         addSubgroups(allGroups, man, groups);
306 
307         // and only now add all direct user groups
308         allGroups.addAll(groups);
309 
310         return allGroups;
311     }
312 
313     @Override
314     public Collection<String> getRoles() {
315         log.debug("getRoles()");
316         return roles;
317     }
318 
319     @Override
320     public Collection<String> getAllRoles() {
321         // TODO: if the user is just a simple bean, then this method doesn't belong here anymore!!!!
322         log.debug("get roles for {}", getName());
323 
324         final Set<String> allRoles = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
325         final Collection<String> roles = getRoles();
326 
327         // add all direct user groups
328         allRoles.addAll(roles);
329 
330         Collection<String> allGroups = getAllGroups();
331 
332         // FYI: can't initialize upfront as the instance of the user class needs to be created BEFORE repo is ready
333         GroupManager man = SecuritySupport.Factory.getInstance().getGroupManager();
334 
335         // add roles from all groups
336         for (String group : allGroups) {
337             try {
338                 allRoles.addAll(man.getGroup(group).getRoles());
339             } catch (AccessDeniedException e) {
340                 log.debug("Skipping denied group " + group + " for user " + getName(), e);
341             } catch (UnsupportedOperationException e) {
342                 log.debug("Skipping unsupported  getGroup() for group " + group + " and user " + getName(), e);
343             }
344         }
345         return allRoles;
346     }
347 
348     public String getPath() {
349         return this.path;
350     }
351 
352     @Deprecated
353     public void setPath(String path) {
354         this.path = path;
355     }
356 
357     /**
358      * Any group from the groups is checked for the subgroups only if it is not in the allGroups yet. This is to prevent infinite loops in case of cyclic group assignment.
359      */
360     private void addSubgroups(final Set<String> allGroups, GroupManager man, Collection<String> groups) {
361         for (String groupName : groups) {
362             // check if this group was not already added to prevent infinite loops
363             if (!allGroups.contains(groupName)) {
364                 try {
365                     Group group = man.getGroup(groupName);
366                     if (group == null) {
367                         log.error("Failed to resolve group {} for user {}.", groupName, name);
368                         continue;
369                     }
370                     Collection<String> subgroups = group.getGroups();
371                     // and recursively add more subgroups
372                     addSubgroups(allGroups, man, subgroups);
373                     allGroups.addAll(subgroups);
374                 } catch (AccessDeniedException e) {
375                     log.debug("Skipping denied group " + groupName + " for user " + getName(), e);
376                 } catch (UnsupportedOperationException e) {
377                     log.debug("Skipping unsupported  getGroup() for group " + groupName + " and user " + getName(), e);
378                 }
379 
380             }
381         }
382     }
383 
384     public String getRealm() {
385         return realm;
386     }
387 
388     /**
389      * Update the "last access" timestamp.
390      * @deprecated since 4.5, use {@link UserManager#updateLastAccessTimestamp(User)} instead
391      */
392     @Deprecated
393     public void setLastAccess() {
394         throw new UnsupportedOperationException("Use manager to update user details.");
395     }
396 
397     /**
398      * Not every user needs to have a node behind. Use manager to obtain nodes
399      * @deprecated since 4.5, use {@link UserManager#updateLastAccessTimestamp(User)} instead
400      */
401     @Deprecated
402     public Content getUserNode() {
403         throw new UnsupportedOperationException("Underlying storage node is no longer exposed nor required for custom user stores.");
404     }
405 
406     /**
407      * @deprecated since 4.5, use {@link UserManager} instead
408      */
409     @Override
410     @Deprecated
411     public void setProperty(String propertyName, String value) {
412         throw new UnsupportedOperationException("Use manager to modify properties of the user.");
413     }
414 }