View Javadoc
1   /**
2    * This file Copyright (c) 2003-2014 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.JCRMgnlPropertyHidingPredicate;
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 group the user should be added to
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, String)} 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     /**
187      * @param groupOrRoleName name of the group or role to check for
188      * @param groupOrRole tells what we're checking for groups or roles
189      * @return whether this users has the requested groupOrRole assigned
190      */
191     private boolean hasAny(final String groupOrRoleName, final String groupOrRole) {
192         long start = System.currentTimeMillis();
193         try {
194             final String workspaceName = StringUtils.equalsIgnoreCase(groupOrRole, NODE_ROLES) ? RepositoryConstants.USER_ROLES : RepositoryConstants.USER_GROUPS;
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                     List<String> list = new ArrayList<String>();
202                     if (!session.getNode(getPath()).hasNode(groupOrRole)) {
203                         return list;
204                     }
205                     Node groupsOrRoles = session.getNode(getPath()).getNode(groupOrRole);
206                     for (PropertyIterator props = new FilteringPropertyIterator(groupsOrRoles.getProperties(), new JCRMgnlPropertyHidingPredicate()); props.hasNext();) {
207                         // check for the existence of this ID
208                         Property property = props.nextProperty();
209                         try {
210                             list.add(property.getString());
211                         } catch (ItemNotFoundException e) {
212                             log.debug("Role or group [{}] does not exist in the ROLES/GROUPS workspace", groupOrRoleName);
213                         } catch (IllegalArgumentException e) {
214                             log.debug("{} has invalid value", property.getPath());
215                         }
216                     }
217                     return list;
218                 }});
219 
220 
221             return MgnlContext.doInSystemContext(new JCRSessionOp<Boolean>(workspaceName) {
222 
223                 @Override
224                 public Boolean exec(Session session) throws RepositoryException {
225                     for (String groupOrRole : groupsOrRoles) {
226                         // check for the existence of this ID
227                         try {
228                             if (session.getNodeByIdentifier(groupOrRole).getName().equalsIgnoreCase(groupOrRoleName)) {
229                                 return true;
230                             }
231                         } catch (RepositoryException e) {
232                             log.debug("Role or group [{}] does not exist in the ROLES/GROUPS workspace", groupOrRoleName);
233                         }
234                     }
235                     return false;
236                 }});
237 
238         } catch (Exception e) {
239             log.error("Exception when trying to read " + groupOrRole, e);
240         } finally {
241             log.debug("checked {} for {} in {}ms.", new Object[] {groupOrRoleName, groupOrRole, (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         return allGroups;
315     }
316 
317     @Override
318     public Collection<String> getRoles() {
319         log.debug("getRoles()");
320         return roles;
321     }
322 
323     @Override
324     public Collection<String> getAllRoles() {
325         // TODO: if the user is just a simple bean, then this method doesn't belong here anymore!!!!
326         log.debug("get roles for {}", getName());
327 
328         final Set<String> allRoles = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
329         final Collection<String> roles = getRoles();
330 
331         // add all direct user groups
332         allRoles.addAll(roles);
333 
334         Collection<String> allGroups = getAllGroups();
335 
336         // FYI: can't initialize upfront as the instance of the user class needs to be created BEFORE repo is ready
337         GroupManager man = SecuritySupport.Factory.getInstance().getGroupManager();
338 
339         // add roles from all groups
340         for (String group : allGroups) {
341             try {
342                 allRoles.addAll(man.getGroup(group).getRoles());
343             } catch (AccessDeniedException e) {
344                 log.debug("Skipping denied group " + group + " for user " + getName(), e);
345             } catch (UnsupportedOperationException e) {
346                 log.debug("Skipping unsupported  getGroup() for group " + group + " and user " + getName(), e);
347             }
348         }
349         return allRoles;
350     }
351 
352     public String getPath() {
353         return this.path;
354     }
355 
356     @Deprecated
357     public void setPath(String path) {
358         this.path = path;
359     }
360 
361     /**
362      * 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.
363      */
364     private void addSubgroups(final Set<String> allGroups, GroupManager man, Collection<String> groups) {
365         for (String groupName : groups) {
366             // check if this group was not already added to prevent infinite loops
367             if (!allGroups.contains(groupName)) {
368                 allGroups.add(groupName);
369                 try {
370                     Group group = man.getGroup(groupName);
371                     if (group == null) {
372                         log.error("Failed to resolve group {} for user {}.", groupName, name);
373                         continue;
374                     }
375                     Collection<String> subgroups = group.getGroups();
376                     // and recursively add more subgroups
377                     addSubgroups(allGroups, man, subgroups);
378                 } catch (AccessDeniedException e) {
379                     log.debug("Skipping denied group " + groupName + " for user " + getName(), e);
380                 } catch (UnsupportedOperationException e) {
381                     log.debug("Skipping unsupported  getGroup() for group " + groupName + " and user " + getName(), e);
382                 }
383 
384             }
385         }
386     }
387 
388     public String getRealm() {
389         return realm;
390     }
391 
392     /**
393      * Update the "last access" timestamp.
394      * @deprecated since 4.5, use {@link UserManager#updateLastAccessTimestamp(User)} instead
395      */
396     @Deprecated
397     public void setLastAccess() {
398         throw new UnsupportedOperationException("Use manager to update user details.");
399     }
400 
401     /**
402      * Not every user needs to have a node behind. Use manager to obtain nodes
403      * @deprecated since 4.5, use {@link UserManager#updateLastAccessTimestamp(User)} instead
404      */
405     @Deprecated
406     public Content getUserNode() {
407         throw new UnsupportedOperationException("Underlying storage node is no longer exposed nor required for custom user stores.");
408     }
409 
410     /**
411      * @deprecated since 4.5, use {@link UserManager} instead
412      */
413     @Override
414     @Deprecated
415     public void setProperty(String propertyName, String value) {
416         throw new UnsupportedOperationException("Use manager to modify properties of the user.");
417     }
418 
419     @Override
420     public String getIdentifier() {
421         return uuid;
422     }
423 
424     /**
425      * @deprecated since 4.5.1, use {@link MgnlUser#getIdentifier()} instead
426      */
427     @Deprecated
428     public String getUuid() {
429         return uuid;
430     }
431 
432     @Override
433     public String toString() {
434         return "MgnlUser - " + name + " [" + uuid + "]";
435     }
436 }