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 52871 2011-12-20 08:52:14Z sschmitt $
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      */
153     @Override
154     public void setEnabled(boolean enabled) {
155         log.debug("setEnabled({})", enabled);
156         this.enabled = enabled;
157     }
158 
159     /**
160      * Is this user in a specified role?
161      * @param roleName the name of the role
162      * @return true if in role
163      */
164     @Override
165     public boolean hasRole(String roleName) {
166         return SecuritySupport.Factory.getInstance().getUserManager(getRealm()).hasAny(getName(), roleName, NODE_ROLES);
167     }
168 
169     @Override
170     public void removeRole(String roleName) throws UnsupportedOperationException {
171         log.debug("removeRole({})", roleName);
172         throw new UnsupportedOperationException("use manager to remove roles!");
173     }
174 
175     @Override
176     public void addRole(String roleName) throws UnsupportedOperationException {
177         log.debug("addRole({})", roleName);
178         throw new UnsupportedOperationException("use manager to add roles!");
179     }
180 
181     // TODO: methods like the ones below should not be in the object but rather in the manager, making object reusable with different managers.
182     private boolean hasAny(final String name, final String nodeName) {
183         long start = System.currentTimeMillis();
184         try {
185             String sessionName;
186             if (StringUtils.equalsIgnoreCase(nodeName, NODE_ROLES)) {
187                 sessionName = RepositoryConstants.USER_ROLES;
188             } else {
189                 sessionName = RepositoryConstants.USER_GROUPS;
190             }
191 
192             // 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.
193             final Collection<String> groupsOrRoles = MgnlContext.doInSystemContext(new SilentSessionOp<Collection<String>>(RepositoryConstants.USERS) {
194 
195                 @Override
196                 public Collection<String> doExec(Session session) throws RepositoryException {
197                     Node groupsOrRoles = session.getNode(getName()).getNode(nodeName);
198                     List<String> list = new ArrayList<String>();
199                     for (PropertyIterator props = groupsOrRoles.getProperties(); props.hasNext();) {
200                         // check for the existence of this ID
201                         Property property = props.nextProperty();
202                         try {
203                             list.add(property.getString());
204                         } catch (ItemNotFoundException e) {
205                             log.debug("Role [{}] does not exist in the ROLES repository", name);
206                         } catch (IllegalArgumentException e) {
207                             log.debug("{} has invalid value", property.getPath());
208                         }
209                     }
210                     return list;
211                 }});
212 
213 
214             return MgnlContext.doInSystemContext(new JCRSessionOp<Boolean>(sessionName) {
215 
216                 @Override
217                 public Boolean exec(Session session) throws RepositoryException {
218                     for (String groupOrRole : groupsOrRoles) {
219                         // check for the existence of this ID
220                         try {
221                             if (session.getNodeByIdentifier(groupOrRole).getName().equalsIgnoreCase(name)) {
222                                 return true;
223                             }
224                         } catch (ItemNotFoundException e) {
225                             log.debug("Role [{}] does not exist in the ROLES repository", name);
226                         }
227                     }
228                     return false;
229                 }});
230 
231         } catch (RepositoryException e) {
232             log.debug(e.getMessage(), e);
233             //TODO: why are we swallowing exceptions silently here?
234         } finally {
235             log.debug("checked {} for {} in {}ms.", new Object[] {name, nodeName, (System.currentTimeMillis() - start)});
236         }
237         return false;
238     }
239 
240     public int getFailedLoginAttempts(){
241         return MgnlContext.doInSystemContext(new SilentSessionOp<Integer>(RepositoryConstants.USERS) {
242             @Override
243             public Integer doExec(Session session) throws RepositoryException {
244                 Node userNode = session.getNode("/" + getRealm() + "/" + getName());
245                 if (!userNode.hasProperty("failedLoginAttempts")){
246                     userNode.setProperty("failedLoginAttempts", 0);
247                     session.save();
248                 }
249                 return (int)userNode.getProperty("failedLoginAttempts").getLong();
250             }});
251     }
252 
253     public Calendar getReleaseTime(){
254         return MgnlContext.doInSystemContext(new SilentSessionOp<Calendar>(RepositoryConstants.USERS) {
255             @Override
256             public Calendar doExec(Session session) throws RepositoryException {
257                 Node userNode = session.getNode("/" + getRealm() + "/" + getName());
258                 if (!userNode.hasProperty("releaseTime")){
259                     userNode.setProperty("releaseTime", 0);
260                     session.save();
261                 }
262                 return userNode.getProperty("releaseTime").getDate();
263             }});
264     }
265 
266     @Override
267     public String getName() {
268         log.debug("getName()=>{}", name);
269         return name;
270     }
271 
272     @Override
273     public String getPassword() {
274         return encodedPassword;
275     }
276 
277     protected String decodePassword(String encodedPassword) {
278         return new String(Base64.decodeBase64(encodedPassword.getBytes()));
279     }
280 
281     @Override
282     public String getLanguage() {
283         log.debug("getLang()=>{}", language);
284         return this.language;
285     }
286 
287     @Override
288     public String getProperty(String propertyName) {
289         log.debug("getProperty({})", propertyName);
290         return properties.get(propertyName);
291     }
292 
293     @Override
294     public Collection<String> getGroups() {
295         log.debug("getGroups()");
296         return groups;
297     }
298 
299     @Override
300     public Collection<String> getAllGroups() {
301         // TODO: if the user is just a simple bean, then this method doesn't belong here anymore!!!!
302         // should be moved to user manager or to group manager???
303         log.debug("get groups for {}", getName());
304 
305         final Set<String> allGroups = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
306         final Collection<String> groups = getGroups();
307 
308         // FYI: can't initialize upfront as the instance of the user class needs to be created BEFORE repo is ready
309         GroupManager man = SecuritySupport.Factory.getInstance().getGroupManager();
310 
311         // add all subbroups
312         addSubgroups(allGroups, man, groups);
313 
314         // and only now add all direct user groups
315         allGroups.addAll(groups);
316 
317         return allGroups;
318     }
319 
320     @Override
321     public Collection<String> getRoles() {
322         log.debug("getRoles()");
323         return roles;
324     }
325 
326     @Override
327     public Collection<String> getAllRoles() {
328         // TODO: if the user is just a simple bean, then this method doesn't belong here anymore!!!!
329         log.debug("get roles for {}", getName());
330 
331         final Set<String> allRoles = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
332         final Collection<String> roles = getRoles();
333 
334         // add all direct user groups
335         allRoles.addAll(roles);
336 
337         Collection<String> allGroups = getAllGroups();
338 
339         // FYI: can't initialize upfront as the instance of the user class needs to be created BEFORE repo is ready
340         GroupManager man = SecuritySupport.Factory.getInstance().getGroupManager();
341 
342         // add roles from all groups
343         for (String group : allGroups) {
344             try {
345                 allRoles.addAll(man.getGroup(group).getRoles());
346             } catch (AccessDeniedException e) {
347                 log.debug("Skipping denied group " + group + " for user " + getName(), e);
348             } catch (UnsupportedOperationException e) {
349                 log.debug("Skipping unsupported  getGroup() for group " + group + " and user " + getName(), e);
350             }
351         }
352         return allRoles;
353     }
354 
355     public String getPath() {
356         return this.path;
357     }
358 
359     @Deprecated
360     public void setPath(String path) {
361         this.path = path;
362     }
363 
364     /**
365      * 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.
366      */
367     private void addSubgroups(final Set<String> allGroups, GroupManager man, Collection<String> groups) {
368         for (String groupName : groups) {
369             // check if this group was not already added to prevent infinite loops
370             if (!allGroups.contains(groupName)) {
371                 try {
372                     Group group = man.getGroup(groupName);
373                     if (group == null) {
374                         log.error("Failed to resolve group {} for user {}.", groupName, name);
375                         continue;
376                     }
377                     Collection<String> subgroups = group.getGroups();
378                     // and recursively add more subgroups
379                     addSubgroups(allGroups, man, subgroups);
380                     allGroups.addAll(subgroups);
381                 } catch (AccessDeniedException e) {
382                     log.debug("Skipping denied group " + groupName + " for user " + getName(), e);
383                 } catch (UnsupportedOperationException e) {
384                     log.debug("Skipping unsupported  getGroup() for group " + groupName + " and user " + getName(), e);
385                 }
386 
387             }
388         }
389     }
390 
391     public String getRealm() {
392         return realm;
393     }
394 
395     /**
396      * Update the "last access" timestamp.
397      * @deprecated since 4.5, use {@link UserManager#updateLastAccessTimestamp(User)} instead
398      */
399     @Deprecated
400     public void setLastAccess() {
401         throw new UnsupportedOperationException("Use manager to update user details.");
402     }
403 
404     /**
405      * Not every user needs to have a node behind. Use manager to obtain nodes
406      * @deprecated since 4.5, use {@link UserManager#updateLastAccessTimestamp(User)} instead
407      */
408     @Deprecated
409     public Content getUserNode() {
410         throw new UnsupportedOperationException("Underlying storage node is no longer exposed nor required for custom user stores.");
411     }
412 
413     /**
414      * @deprecated since 4.5, use {@link UserManager} instead
415      */
416     @Override
417     @Deprecated
418     public void setProperty(String propertyName, String value) {
419         throw new UnsupportedOperationException("Use manager to modify properties of the user.");
420     }
421 
422     public String getUuid() {
423         return uuid;
424     }
425 }