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   */
72  public class MgnlUser extends AbstractUser implements User, Serializable {
73  
74      private static final Logger log = LoggerFactory.getLogger(MgnlUser.class);
75  
76      private final Map<String, String> properties;
77      private final Collection<String> groups;
78      private final Collection<String> roles;
79  
80      private final String name;
81      private final String language;
82      private final String encodedPassword;
83      private boolean enabled = true;
84      private String path;
85      private String uuid;
86  
87      private final String realm;
88  
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     public MgnlUser(String name, String realm, Collection<String> groups, Collection<String> roles, Map<String, String> properties, String path, String uuid) {
106         this(name,realm,groups,roles,properties);
107         this.path = path;
108         this.uuid = uuid;
109     }
110 
111     /**
112      * Is this user in a specified group?
113      * @param groupName the name of the group
114      * @return true if in group
115      */
116     @Override
117     public boolean inGroup(String groupName) {
118         log.debug("inGroup({})", groupName);
119         return this.hasAny(groupName, NODE_GROUPS);
120     }
121 
122     /**
123      * Remove a group. Implementation is optional
124      */
125     @Override
126     public void removeGroup(String groupName) throws UnsupportedOperationException {
127         log.debug("removeGroup({})", groupName);
128         throw new UnsupportedOperationException("use manager to remove groups!");
129     }
130 
131     /**
132      * Adds this user to a group. Implementation is optional
133      * @param groupName group the user should be added to
134      */
135     @Override
136     public void addGroup(String groupName) throws UnsupportedOperationException {
137         log.debug("addGroup({})", groupName);
138         throw new UnsupportedOperationException("use manager to add groups!");
139     }
140 
141     @Override
142     public boolean isEnabled() {
143         log.debug("isEnabled()");
144         return enabled ;
145     }
146 
147     /**
148      * This methods sets flag just on the bean. It does not update persisted user data. Use manager to update user data.
149      * @deprecated since 4.5, use {@link UserManager#setProperty(User, String, String)} instead
150      */
151     @Override
152     @Deprecated
153     public void setEnabled(boolean enabled) {
154         log.debug("setEnabled({})", enabled);
155         throw new UnsupportedOperationException("use manager to enable user!");
156     }
157 
158     /**
159      * Is this user in a specified role?
160      * @param roleName the name of the role
161      * @return true if in role
162      */
163     @Override
164     public boolean hasRole(String roleName) {
165         return SecuritySupport.Factory.getInstance().getUserManager(getRealm()).hasAny(getName(), roleName, NODE_ROLES);
166     }
167 
168     @Override
169     public void removeRole(String roleName) throws UnsupportedOperationException {
170         log.debug("removeRole({})", roleName);
171         throw new UnsupportedOperationException("use manager to remove roles!");
172     }
173 
174     @Override
175     public void addRole(String roleName) throws UnsupportedOperationException {
176         log.debug("addRole({})", roleName);
177         throw new UnsupportedOperationException("use manager to add roles!");
178     }
179 
180     // TODO: methods like the ones below should not be in the object but rather in the manager, making object reusable with different managers.
181     /**
182      * @param groupOrRoleName name of the group or role to check for
183      * @param groupOrRole tells what we're checking for groups or roles
184      * @return whether this users has the requested groupOrRole assigned
185      */
186     private boolean hasAny(final String groupOrRoleName, final String groupOrRole) {
187         long start = System.currentTimeMillis();
188         try {
189             final String workspaceName = StringUtils.equalsIgnoreCase(groupOrRole, NODE_ROLES) ? RepositoryConstants.USER_ROLES : RepositoryConstants.USER_GROUPS;
190 
191             // 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.
192             final Collection<String> groupsOrRoles = MgnlContext.doInSystemContext(new SilentSessionOp<Collection<String>>(RepositoryConstants.USERS) {
193 
194                 @Override
195                 public Collection<String> doExec(Session session) throws RepositoryException {
196                     List<String> list = new ArrayList<String>();
197                     if (!session.getNode(getPath()).hasNode(groupOrRole)) {
198                         return list;
199                     }
200                     Node groupsOrRoles = session.getNode(getPath()).getNode(groupOrRole);
201                     for (PropertyIterator props = new FilteringPropertyIterator(groupsOrRoles.getProperties(), new JCRMgnlPropertyHidingPredicate()); 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 or group [{}] does not exist in the ROLES/GROUPS workspace", groupOrRoleName);
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>(workspaceName) {
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(groupOrRoleName)) {
224                                 return true;
225                             }
226                         } catch (RepositoryException e) {
227                             log.debug("Role or group [{}] does not exist in the ROLES/GROUPS workspace", groupOrRoleName);
228                         }
229                     }
230                     return false;
231                 }});
232 
233         } catch (Exception e) {
234             log.error("Exception when trying to read " + groupOrRole, e);
235         } finally {
236             log.debug("checked {} for {} in {}ms.", new Object[] {groupOrRoleName, groupOrRole, (System.currentTimeMillis() - start)});
237         }
238         return false;
239     }
240 
241     public int getFailedLoginAttempts(){
242         try{
243             return Integer.valueOf(this.properties.get("failedLoginAttempts"));
244         }catch(Exception e){
245             return 0;
246         }
247     }
248 
249     public Calendar getReleaseTime(){
250         try{
251             return ISO8601.parse(this.properties.get("releaseTime"));
252         }catch(Exception e){
253             return null;
254         }
255     }
256 
257     @Override
258     public String getName() {
259         log.debug("getName()=>{}", name);
260         return name;
261     }
262 
263     @Override
264     public String getPassword() {
265         return encodedPassword;
266     }
267 
268     @Deprecated
269     /**
270      * @deprecated Since 4.5.8. Password is now encoded by BCrypt and therefore cannot be decoded.
271      */
272     protected String decodePassword(String encodedPassword) {
273         throw new UnsupportedOperationException();
274     }
275 
276     @Override
277     public String getLanguage() {
278         log.debug("getLang()=>{}", language);
279         return this.language;
280     }
281 
282     @Override
283     public String getProperty(String propertyName) {
284         log.debug("getProperty({})", propertyName);
285         return properties.get(propertyName);
286     }
287 
288     @Override
289     public Collection<String> getGroups() {
290         log.debug("getGroups()");
291         return groups;
292     }
293 
294     @Override
295     public Collection<String> getAllGroups() {
296         // TODO: if the user is just a simple bean, then this method doesn't belong here anymore!!!!
297         // should be moved to user manager or to group manager???
298         log.debug("get groups for {}", getName());
299 
300         final Set<String> allGroups = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
301         final Collection<String> groups = getGroups();
302 
303         // FYI: can't initialize upfront as the instance of the user class needs to be created BEFORE repo is ready
304         GroupManager man = SecuritySupport.Factory.getInstance().getGroupManager();
305 
306         // add all subbroups
307         addSubgroups(allGroups, man, groups);
308 
309         return allGroups;
310     }
311 
312     @Override
313     public Collection<String> getRoles() {
314         log.debug("getRoles()");
315         return roles;
316     }
317 
318     @Override
319     public Collection<String> getAllRoles() {
320         // TODO: if the user is just a simple bean, then this method doesn't belong here anymore!!!!
321         log.debug("get roles for {}", getName());
322 
323         final Set<String> allRoles = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
324         final Collection<String> roles = getRoles();
325 
326         // add all direct user groups
327         allRoles.addAll(roles);
328 
329         Collection<String> allGroups = getAllGroups();
330 
331         // FYI: can't initialize upfront as the instance of the user class needs to be created BEFORE repo is ready
332         GroupManager man = SecuritySupport.Factory.getInstance().getGroupManager();
333 
334         // add roles from all groups
335         for (String group : allGroups) {
336             try {
337                 allRoles.addAll(man.getGroup(group).getRoles());
338             } catch (AccessDeniedException e) {
339                 log.debug("Skipping denied group " + group + " for user " + getName(), e);
340             } catch (UnsupportedOperationException e) {
341                 log.debug("Skipping unsupported  getGroup() for group " + group + " and user " + getName(), e);
342             }
343         }
344         return allRoles;
345     }
346 
347     public String getPath() {
348         return this.path;
349     }
350 
351     @Deprecated
352     public void setPath(String path) {
353         this.path = path;
354     }
355 
356     /**
357      * 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.
358      */
359     private void addSubgroups(final Set<String> allGroups, GroupManager man, Collection<String> groups) {
360         for (String groupName : groups) {
361             // check if this group was not already added to prevent infinite loops
362             if (!allGroups.contains(groupName)) {
363                 allGroups.add(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                 } catch (AccessDeniedException e) {
374                     log.debug("Skipping denied group " + groupName + " for user " + getName(), e);
375                 } catch (UnsupportedOperationException e) {
376                     log.debug("Skipping unsupported  getGroup() for group " + groupName + " and user " + getName(), e);
377                 }
378 
379             }
380         }
381     }
382 
383     public String getRealm() {
384         return realm;
385     }
386 
387     /**
388      * Update the "last access" timestamp.
389      * @deprecated since 4.5, use {@link UserManager#updateLastAccessTimestamp(User)} instead
390      */
391     @Deprecated
392     public void setLastAccess() {
393         throw new UnsupportedOperationException("Use manager to update user details.");
394     }
395 
396     /**
397      * Not every user needs to have a node behind. Use manager to obtain nodes
398      * @deprecated since 4.5, use {@link UserManager#updateLastAccessTimestamp(User)} instead
399      */
400     @Deprecated
401     public Content getUserNode() {
402         throw new UnsupportedOperationException("Underlying storage node is no longer exposed nor required for custom user stores.");
403     }
404 
405     /**
406      * @deprecated since 4.5, use {@link UserManager} instead
407      */
408     @Override
409     @Deprecated
410     public void setProperty(String propertyName, String value) {
411         throw new UnsupportedOperationException("Use manager to modify properties of the user.");
412     }
413 
414     @Override
415     public String getIdentifier() {
416         return uuid;
417     }
418 
419     /**
420      * @deprecated since 4.5.1, use {@link MgnlUser#getIdentifier()} instead
421      */
422     @Deprecated
423     public String getUuid() {
424         return uuid;
425     }
426 
427     @Override
428     public String toString() {
429         return "MgnlUser - " + name + " [" + uuid + "]";
430     }
431 }