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 55879 2012-03-10 13:03:59Z mdivilek $
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      private String uuid;
88  
89      private final String realm;
90  
91  
92      public MgnlUser(String name, String realm, Collection<String> groups, Collection<String> roles, Map<String, String> properties) {
93          this.name = name;
94          this.roles = Collections.unmodifiableCollection(roles);
95          this.groups = Collections.unmodifiableCollection(groups);
96          this.properties = Collections.unmodifiableMap(properties);
97          this.realm = realm;
98  
99          //shortcut some often accessed props so we don't have to search hashmap for them.
100         language = properties.get(MgnlUserManager.PROPERTY_LANGUAGE);
101         String enbld = properties.get(MgnlUserManager.PROPERTY_ENABLED);
102         // all accounts are enabled by default and prop doesn't exist if the account was not disabled before
103         enabled = enbld == null ? true : Boolean.parseBoolean(properties.get(MgnlUserManager.PROPERTY_ENABLED));
104         encodedPassword = properties.get(MgnlUserManager.PROPERTY_PASSWORD);
105     }
106 
107     public MgnlUser(String name, String realm, Collection<String> groups, Collection<String> roles, Map<String, String> properties, String path, String uuid) {
108         this(name,realm,groups,roles,properties);
109         this.path = path;
110         this.uuid = uuid;
111     }
112 
113     /**
114      * Is this user in a specified group?
115      * @param groupName the name of the group
116      * @return true if in group
117      */
118     @Override
119     public boolean inGroup(String groupName) {
120         log.debug("inGroup({})", groupName);
121         return this.hasAny(groupName, NODE_GROUPS);
122     }
123 
124     /**
125      * Remove a group. Implementation is optional
126      * @param groupName
127      */
128     @Override
129     public void removeGroup(String groupName) throws UnsupportedOperationException {
130         log.debug("removeGroup({})", groupName);
131         throw new UnsupportedOperationException("use manager to remove groups!");
132     }
133 
134     /**
135      * Adds this user to a group. Implementation is optional
136      * @param groupName
137      */
138     @Override
139     public void addGroup(String groupName) throws UnsupportedOperationException {
140         log.debug("addGroup({})", groupName);
141         throw new UnsupportedOperationException("use manager to add groups!");
142     }
143 
144     @Override
145     public boolean isEnabled() {
146         log.debug("isEnabled()");
147         return enabled ;
148     }
149 
150     /**
151      * This methods sets flag just on the bean. It does not update persisted user data. Use manager to update user data.
152      * @deprecated since 4.5, use {@link UserManager#setProperty(User, String, Value)} instead
153      */
154     @Override
155     @Deprecated
156     public void setEnabled(boolean enabled) {
157         log.debug("setEnabled({})", enabled);
158         throw new UnsupportedOperationException("use manager to enable user!");
159     }
160 
161     /**
162      * Is this user in a specified role?
163      * @param roleName the name of the role
164      * @return true if in role
165      */
166     @Override
167     public boolean hasRole(String roleName) {
168         return SecuritySupport.Factory.getInstance().getUserManager(getRealm()).hasAny(getName(), roleName, NODE_ROLES);
169     }
170 
171     @Override
172     public void removeRole(String roleName) throws UnsupportedOperationException {
173         log.debug("removeRole({})", roleName);
174         throw new UnsupportedOperationException("use manager to remove roles!");
175     }
176 
177     @Override
178     public void addRole(String roleName) throws UnsupportedOperationException {
179         log.debug("addRole({})", roleName);
180         throw new UnsupportedOperationException("use manager to add roles!");
181     }
182 
183     // TODO: methods like the ones below should not be in the object but rather in the manager, making object reusable with different managers.
184     private boolean hasAny(final String name, final String nodeName) {
185         long start = System.currentTimeMillis();
186         try {
187             String sessionName;
188             if (StringUtils.equalsIgnoreCase(nodeName, NODE_ROLES)) {
189                 sessionName = RepositoryConstants.USER_ROLES;
190             } else {
191                 sessionName = RepositoryConstants.USER_GROUPS;
192             }
193 
194             // 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.
195             final Collection<String> groupsOrRoles = MgnlContext.doInSystemContext(new SilentSessionOp<Collection<String>>(RepositoryConstants.USERS) {
196 
197                 @Override
198                 public Collection<String> doExec(Session session) throws RepositoryException {
199                     Node groupsOrRoles = session.getNode(getName()).getNode(nodeName);
200                     List<String> list = new ArrayList<String>();
201                     for (PropertyIterator props = groupsOrRoles.getProperties(); props.hasNext();) {
202                         // check for the existence of this ID
203                         Property property = props.nextProperty();
204                         try {
205                             list.add(property.getString());
206                         } catch (ItemNotFoundException e) {
207                             log.debug("Role [{}] does not exist in the ROLES repository", name);
208                         } catch (IllegalArgumentException e) {
209                             log.debug("{} has invalid value", property.getPath());
210                         }
211                     }
212                     return list;
213                 }});
214 
215 
216             return MgnlContext.doInSystemContext(new JCRSessionOp<Boolean>(sessionName) {
217 
218                 @Override
219                 public Boolean exec(Session session) throws RepositoryException {
220                     for (String groupOrRole : groupsOrRoles) {
221                         // check for the existence of this ID
222                         try {
223                             if (session.getNodeByIdentifier(groupOrRole).getName().equalsIgnoreCase(name)) {
224                                 return true;
225                             }
226                         } catch (ItemNotFoundException e) {
227                             log.debug("Role [{}] does not exist in the ROLES repository", name);
228                         }
229                     }
230                     return false;
231                 }});
232 
233         } catch (RepositoryException e) {
234             log.debug(e.getMessage(), e);
235             //TODO: why are we swallowing exceptions silently here?
236         } finally {
237             log.debug("checked {} for {} in {}ms.", new Object[] {name, nodeName, (System.currentTimeMillis() - start)});
238         }
239         return false;
240     }
241 
242     public int getFailedLoginAttempts(){
243         return MgnlContext.doInSystemContext(new SilentSessionOp<Integer>(RepositoryConstants.USERS) {
244             @Override
245             public Integer doExec(Session session) throws RepositoryException {
246                 Node userNode = session.getNode("/" + getRealm() + "/" + getName());
247                 if (!userNode.hasProperty("failedLoginAttempts")){
248                     userNode.setProperty("failedLoginAttempts", 0);
249                     session.save();
250                 }
251                 return (int)userNode.getProperty("failedLoginAttempts").getLong();
252             }});
253     }
254 
255     public Calendar getReleaseTime(){
256         return MgnlContext.doInSystemContext(new SilentSessionOp<Calendar>(RepositoryConstants.USERS) {
257             @Override
258             public Calendar doExec(Session session) throws RepositoryException {
259                 Node userNode = session.getNode("/" + getRealm() + "/" + getName());
260                 if (!userNode.hasProperty("releaseTime")){
261                     userNode.setProperty("releaseTime", 0);
262                     session.save();
263                 }
264                 return userNode.getProperty("releaseTime").getDate();
265             }});
266     }
267 
268     @Override
269     public String getName() {
270         log.debug("getName()=>{}", name);
271         return name;
272     }
273 
274     @Override
275     public String getPassword() {
276         return encodedPassword;
277     }
278 
279     protected String decodePassword(String encodedPassword) {
280         return new String(Base64.decodeBase64(encodedPassword.getBytes()));
281     }
282 
283     @Override
284     public String getLanguage() {
285         log.debug("getLang()=>{}", language);
286         return this.language;
287     }
288 
289     @Override
290     public String getProperty(String propertyName) {
291         log.debug("getProperty({})", propertyName);
292         return properties.get(propertyName);
293     }
294 
295     @Override
296     public Collection<String> getGroups() {
297         log.debug("getGroups()");
298         return groups;
299     }
300 
301     @Override
302     public Collection<String> getAllGroups() {
303         // TODO: if the user is just a simple bean, then this method doesn't belong here anymore!!!!
304         // should be moved to user manager or to group manager???
305         log.debug("get groups for {}", getName());
306 
307         final Set<String> allGroups = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
308         final Collection<String> groups = getGroups();
309 
310         // FYI: can't initialize upfront as the instance of the user class needs to be created BEFORE repo is ready
311         GroupManager man = SecuritySupport.Factory.getInstance().getGroupManager();
312 
313         // add all subbroups
314         addSubgroups(allGroups, man, groups);
315 
316         // and only now add all direct user groups
317         allGroups.addAll(groups);
318 
319         return allGroups;
320     }
321 
322     @Override
323     public Collection<String> getRoles() {
324         log.debug("getRoles()");
325         return roles;
326     }
327 
328     @Override
329     public Collection<String> getAllRoles() {
330         // TODO: if the user is just a simple bean, then this method doesn't belong here anymore!!!!
331         log.debug("get roles for {}", getName());
332 
333         final Set<String> allRoles = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
334         final Collection<String> roles = getRoles();
335 
336         // add all direct user groups
337         allRoles.addAll(roles);
338 
339         Collection<String> allGroups = getAllGroups();
340 
341         // FYI: can't initialize upfront as the instance of the user class needs to be created BEFORE repo is ready
342         GroupManager man = SecuritySupport.Factory.getInstance().getGroupManager();
343 
344         // add roles from all groups
345         for (String group : allGroups) {
346             try {
347                 allRoles.addAll(man.getGroup(group).getRoles());
348             } catch (AccessDeniedException e) {
349                 log.debug("Skipping denied group " + group + " for user " + getName(), e);
350             } catch (UnsupportedOperationException e) {
351                 log.debug("Skipping unsupported  getGroup() for group " + group + " and user " + getName(), e);
352             }
353         }
354         return allRoles;
355     }
356 
357     public String getPath() {
358         return this.path;
359     }
360 
361     @Deprecated
362     public void setPath(String path) {
363         this.path = path;
364     }
365 
366     /**
367      * 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.
368      */
369     private void addSubgroups(final Set<String> allGroups, GroupManager man, Collection<String> groups) {
370         for (String groupName : groups) {
371             // check if this group was not already added to prevent infinite loops
372             if (!allGroups.contains(groupName)) {
373                 try {
374                     Group group = man.getGroup(groupName);
375                     if (group == null) {
376                         log.error("Failed to resolve group {} for user {}.", groupName, name);
377                         continue;
378                     }
379                     Collection<String> subgroups = group.getGroups();
380                     // and recursively add more subgroups
381                     addSubgroups(allGroups, man, subgroups);
382                     allGroups.addAll(subgroups);
383                 } catch (AccessDeniedException e) {
384                     log.debug("Skipping denied group " + groupName + " for user " + getName(), e);
385                 } catch (UnsupportedOperationException e) {
386                     log.debug("Skipping unsupported  getGroup() for group " + groupName + " and user " + getName(), e);
387                 }
388 
389             }
390         }
391     }
392 
393     public String getRealm() {
394         return realm;
395     }
396 
397     /**
398      * Update the "last access" timestamp.
399      * @deprecated since 4.5, use {@link UserManager#updateLastAccessTimestamp(User)} instead
400      */
401     @Deprecated
402     public void setLastAccess() {
403         throw new UnsupportedOperationException("Use manager to update user details.");
404     }
405 
406     /**
407      * Not every user needs to have a node behind. Use manager to obtain nodes
408      * @deprecated since 4.5, use {@link UserManager#updateLastAccessTimestamp(User)} instead
409      */
410     @Deprecated
411     public Content getUserNode() {
412         throw new UnsupportedOperationException("Underlying storage node is no longer exposed nor required for custom user stores.");
413     }
414 
415     /**
416      * @deprecated since 4.5, use {@link UserManager} instead
417      */
418     @Override
419     @Deprecated
420     public void setProperty(String propertyName, String value) {
421         throw new UnsupportedOperationException("Use manager to modify properties of the user.");
422     }
423 
424     @Override
425     public String getIdentifier() {
426         return uuid;
427     }
428     
429     /**
430      * @deprecated since 4.5.1, use {@link MgnlUser#getIdentifier()} instead
431      */
432     @Deprecated
433     public String getUuid() {
434         return uuid;
435     }
436 
437     @Override
438     public String toString() {
439         return "MgnlUser - " + name + " [" + uuid + "]";
440     }
441 }