View Javadoc

1   /**
2    * This file Copyright (c) 2003-2012 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.jcr.iterator.FilteringPropertyIterator;
41  import info.magnolia.jcr.predicate.JCRPropertyHidingPredicate;
42  import info.magnolia.repository.RepositoryConstants;
43  
44  import java.io.Serializable;
45  import java.util.ArrayList;
46  import java.util.Calendar;
47  import java.util.Collection;
48  import java.util.Collections;
49  import java.util.List;
50  import java.util.Map;
51  import java.util.Set;
52  import java.util.TreeSet;
53  
54  import javax.jcr.ItemNotFoundException;
55  import javax.jcr.Node;
56  import javax.jcr.Property;
57  import javax.jcr.PropertyIterator;
58  import javax.jcr.RepositoryException;
59  import javax.jcr.Session;
60  
61  import org.apache.commons.lang.StringUtils;
62  import org.apache.jackrabbit.util.ISO8601;
63  import org.slf4j.Logger;
64  import org.slf4j.LoggerFactory;
65  
66  /**
67   * User for 4.5 instance
68   * In difference from old MgnlUser, this class operates directly on JCR session and with JCR nodes/properties as our hierarchy managers are not
69   * available at the login time.
70   * Also in difference from MgnlUser, this class doesn't keep around instance of the user node! TODO: Test performance impact of such change.
71   * @author had
72   * @version $Id$
73   */
74  public class MgnlUser extends AbstractUser implements User, Serializable {
75  
76      private static final long serialVersionUID = 222L;
77  
78      private static final Logger log = LoggerFactory.getLogger(MgnlUser.class);
79  
80      private final Map<String, String> properties;
81      private final Collection<String> groups;
82      private final Collection<String> roles;
83  
84      private final String name;
85      private final String language;
86      private final String encodedPassword;
87      private boolean enabled = true;
88      private String path;
89      private String uuid;
90  
91      private final String realm;
92  
93  
94      public MgnlUser(String name, String realm, Collection<String> groups, Collection<String> roles, Map<String, String> properties) {
95          this.name = name;
96          this.roles = Collections.unmodifiableCollection(roles);
97          this.groups = Collections.unmodifiableCollection(groups);
98          this.properties = Collections.unmodifiableMap(properties);
99          this.realm = realm;
100 
101         //shortcut some often accessed props so we don't have to search hashmap for them.
102         language = properties.get(MgnlUserManager.PROPERTY_LANGUAGE);
103         String enbld = properties.get(MgnlUserManager.PROPERTY_ENABLED);
104         // all accounts are enabled by default and prop doesn't exist if the account was not disabled before
105         enabled = enbld == null ? true : Boolean.parseBoolean(properties.get(MgnlUserManager.PROPERTY_ENABLED));
106         encodedPassword = properties.get(MgnlUserManager.PROPERTY_PASSWORD);
107     }
108 
109     public MgnlUser(String name, String realm, Collection<String> groups, Collection<String> roles, Map<String, String> properties, String path, String uuid) {
110         this(name,realm,groups,roles,properties);
111         this.path = path;
112         this.uuid = uuid;
113     }
114 
115     /**
116      * Is this user in a specified group?
117      * @param groupName the name of the group
118      * @return true if in group
119      */
120     @Override
121     public boolean inGroup(String groupName) {
122         log.debug("inGroup({})", groupName);
123         return this.hasAny(groupName, NODE_GROUPS);
124     }
125 
126     /**
127      * Remove a group. Implementation is optional
128      * @param groupName
129      */
130     @Override
131     public void removeGroup(String groupName) throws UnsupportedOperationException {
132         log.debug("removeGroup({})", groupName);
133         throw new UnsupportedOperationException("use manager to remove groups!");
134     }
135 
136     /**
137      * Adds this user to a group. Implementation is optional
138      * @param groupName
139      */
140     @Override
141     public void addGroup(String groupName) throws UnsupportedOperationException {
142         log.debug("addGroup({})", groupName);
143         throw new UnsupportedOperationException("use manager to add groups!");
144     }
145 
146     @Override
147     public boolean isEnabled() {
148         log.debug("isEnabled()");
149         return enabled ;
150     }
151 
152     /**
153      * This methods sets flag just on the bean. It does not update persisted user data. Use manager to update user data.
154      * @deprecated since 4.5, use {@link UserManager#setProperty(User, String, Value)} instead
155      */
156     @Override
157     @Deprecated
158     public void setEnabled(boolean enabled) {
159         log.debug("setEnabled({})", enabled);
160         throw new UnsupportedOperationException("use manager to enable user!");
161     }
162 
163     /**
164      * Is this user in a specified role?
165      * @param roleName the name of the role
166      * @return true if in role
167      */
168     @Override
169     public boolean hasRole(String roleName) {
170         return SecuritySupport.Factory.getInstance().getUserManager(getRealm()).hasAny(getName(), roleName, NODE_ROLES);
171     }
172 
173     @Override
174     public void removeRole(String roleName) throws UnsupportedOperationException {
175         log.debug("removeRole({})", roleName);
176         throw new UnsupportedOperationException("use manager to remove roles!");
177     }
178 
179     @Override
180     public void addRole(String roleName) throws UnsupportedOperationException {
181         log.debug("addRole({})", roleName);
182         throw new UnsupportedOperationException("use manager to add roles!");
183     }
184 
185     // TODO: methods like the ones below should not be in the object but rather in the manager, making object reusable with different managers.
186     private boolean hasAny(final String name, final String nodeName) {
187         long start = System.currentTimeMillis();
188         try {
189             String sessionName;
190             if (StringUtils.equalsIgnoreCase(nodeName, NODE_ROLES)) {
191                 sessionName = RepositoryConstants.USER_ROLES;
192             } else {
193                 sessionName = RepositoryConstants.USER_GROUPS;
194             }
195 
196             // 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.
197             final Collection<String> groupsOrRoles = MgnlContext.doInSystemContext(new SilentSessionOp<Collection<String>>(RepositoryConstants.USERS) {
198 
199                 @Override
200                 public Collection<String> doExec(Session session) throws RepositoryException {
201                     Node groupsOrRoles = session.getNode(getPath()).getNode(nodeName);
202                     List<String> list = new ArrayList<String>();
203                     for (PropertyIterator props = new FilteringPropertyIterator(groupsOrRoles.getProperties(), new JCRPropertyHidingPredicate()); props.hasNext();) {
204                         // check for the existence of this ID
205                         Property property = props.nextProperty();
206 
207                         try {
208                             list.add(property.getString());
209                         } catch (ItemNotFoundException e) {
210                             log.debug("Role [{}] does not exist in the ROLES repository", name);
211                         }
212                     }
213                     return list;
214                 }});
215 
216 
217             return MgnlContext.doInSystemContext(new JCRSessionOp<Boolean>(sessionName) {
218 
219                 @Override
220                 public Boolean exec(Session session) throws RepositoryException {
221                     for (String groupOrRole : groupsOrRoles) {
222                         // check for the existence of this ID
223                         try {
224                             if (session.getNodeByIdentifier(groupOrRole).getName().equalsIgnoreCase(name)) {
225                                 return true;
226                             }
227                         } catch (ItemNotFoundException e) {
228                             log.debug("Role [{}] does not exist in the ROLES repository", name);
229                         } catch (RepositoryException e) {
230                             //log exception and continue iterate over the groups or roles
231                             log.debug(e.getMessage(), e);
232                         }
233                     }
234                     return false;
235                 }});
236 
237         } catch (RepositoryException e) {
238             log.debug(e.getMessage(), e);
239             //TODO: why are we swallowing exceptions silently here?
240         } finally {
241             log.debug("checked {} for {} in {}ms.", new Object[] {name, nodeName, (System.currentTimeMillis() - start)});
242         }
243         return false;
244     }
245 
246     public int getFailedLoginAttempts(){
247         try{
248             return Integer.valueOf(this.properties.get("failedLoginAttempts"));
249         }catch(Exception e){
250             return 0;
251         }
252     }
253 
254     public Calendar getReleaseTime(){
255         try{
256             return ISO8601.parse(this.properties.get("releaseTime"));
257         }catch(Exception e){
258             return null;
259         }
260     }
261 
262     @Override
263     public String getName() {
264         log.debug("getName()=>{}", name);
265         return name;
266     }
267 
268     @Override
269     public String getPassword() {
270         return encodedPassword;
271     }
272 
273     @Deprecated
274     /**
275      * @deprecated Since 4.5.8. Password is now encoded by BCrypt and therefore cannot be decoded.
276      */
277     protected String decodePassword(String encodedPassword) {
278         throw new UnsupportedOperationException();
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     @Override
423     public String getIdentifier() {
424         return uuid;
425     }
426     
427     /**
428      * @deprecated since 4.5.1, use {@link MgnlUser#getIdentifier()} instead
429      */
430     @Deprecated
431     public String getUuid() {
432         return uuid;
433     }
434 
435     @Override
436     public String toString() {
437         return "MgnlUser - " + name + " [" + uuid + "]";
438     }
439 }