View Javadoc
1   /**
2    * This file Copyright (c) 2013-2018 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.security.app.dialog.field.validator;
35  
36  import static info.magnolia.security.app.util.AccessControlPropertyUtil.*;
37  
38  import info.magnolia.cms.security.Permission;
39  import info.magnolia.cms.security.PrincipalUtil;
40  import info.magnolia.cms.security.auth.ACL;
41  import info.magnolia.cms.security.operations.AccessDefinition;
42  import info.magnolia.context.MgnlContext;
43  import info.magnolia.security.app.dialog.field.WorkspaceAccessControlList;
44  
45  import java.security.AccessControlException;
46  import java.text.MessageFormat;
47  import java.util.Set;
48  
49  import javax.jcr.RepositoryException;
50  
51  import org.apache.commons.lang3.StringUtils;
52  import org.slf4j.Logger;
53  import org.slf4j.LoggerFactory;
54  
55  import com.vaadin.v7.data.validator.AbstractValidator;
56  
57  /**
58   * Typed validator for workspace-specific ACL {@linkplain WorkspaceAccessControlList.Entry entries}.
59   * It examines whether the current user creating/editing a role has himself the required permissions
60   * to the workspaces he's specifying in the ACLs. See MGNLUI-2357.
61   *
62   * <p>The validation strategy is as follows:</p>
63   * <ul>
64   * <li>Current user must have access to the given workspace
65   * <li>Current user needs to have a matching permission to the path he's granting permission for
66   * <li>The path he's granting permission must not interfere with other permissions
67   * <li>The user's best matching permission needs to grant equal or greater rights than the ones being granted
68   * <li>In order to deny permission, user requires read permission to that path
69   * <li>Assigning a recursive permission (ending with '*' wildcard) requires user to also have such recursive permission
70   * </ul>
71   */
72  public class WorkspaceAccessControlValidator extends AbstractValidator<WorkspaceAccessControlList.Entry> {
73      private static final Logger log = LoggerFactory.getLogger(WorkspaceAccessControlValidator.class);
74      private final String originalErrorMessage;
75      private String workspace;
76  
77      public WorkspaceAccessControlValidator(String workspace, String errorMessage) {
78          super(errorMessage);
79          this.workspace = workspace;
80          this.originalErrorMessage = errorMessage;
81      }
82  
83      @Override
84      public Class<WorkspaceAccessControlList.Entry> getType() {
85          return WorkspaceAccessControlList.Entry.class;
86      }
87  
88      @Override
89      protected boolean isValidValue(WorkspaceAccessControlList.Entry entry) {
90          boolean isValid = true;
91  
92          if (MgnlContext.getUser().hasRole(AccessDefinition.DEFAULT_SUPERUSER_ROLE)) {
93              return true;
94          }
95  
96          // This is an ACL added using WorkspaceAccessFieldFactory
97          WorkspaceAccessControlList.Entry entryItem = entry;
98          String path = entryItem.getPath();
99          long accessType = entryItem.getAccessType();
100         long permissions = entryItem.getPermissions();
101 
102         if (accessType < WorkspaceAccessControlList.ACCESS_TYPE_NODE || accessType > WorkspaceAccessControlList.ACCESS_TYPE_NODE_AND_CHILDREN) {
103             throw new IllegalArgumentException("Access type should be one of ACCESS_TYPE_NODE (1), ACCESS_TYPE_CHILDREN (2) or ACCESS_TYPE_NODE_AND_CHILDREN (3)");
104         }
105 
106         try {
107             if (!isCurrentUserEntitledToGrantRights(workspace, path, accessType, permissions)) {
108                 isValid = !isValid;
109             }
110         } catch (AccessControlException e) {
111             isValid = !isValid;
112         } catch (RepositoryException e) {
113             log.error("Could not validate current user permissions: ", e);
114             isValid = !isValid;
115         }
116 
117         if (!isValid) {
118             setErrorMessage(MessageFormat.format(originalErrorMessage, permissions, path, accessType));
119         }
120 
121         return isValid;
122     }
123 
124     /**
125      * @return false if current user has no matching permission in the given workspace, if the matching permission does not grant sufficient rights,
126      * or if the granted permission conflicts with sub-path restrictions.
127      */
128     private boolean isCurrentUserEntitledToGrantRights(String workspaceName, String path, long accessType, long permissions) throws RepositoryException {
129         // Granting DENY access is only allowed if the user has READ access to the node
130         if (permissions == Permission.NONE) {
131             permissions = Permission.READ;
132         }
133 
134         ACL acl = PrincipalUtil.findAccessControlList(MgnlContext.getSubject(), workspaceName);
135         if (acl == null) {
136             return false;
137         }
138 
139         String selectedPath = stripWildcardsFromPath(path);
140 
141         // validate node permission
142         if ((accessType & WorkspaceAccessControlList.ACCESS_TYPE_NODE) == WorkspaceAccessControlList.ACCESS_TYPE_NODE) {
143             Permission nodePerm = findBestMatchingPermissions(acl.getList(), selectedPath);
144             if (nodePerm == null || !granted(nodePerm, permissions)) {
145                 return false;
146             }
147         }
148 
149         // validate sub-node permission
150         if ((accessType & WorkspaceAccessControlList.ACCESS_TYPE_CHILDREN) == WorkspaceAccessControlList.ACCESS_TYPE_CHILDREN) {
151 
152             String suffixForChildren = selectedPath.equals("/") ? "*" : "/*";
153             String childPath = selectedPath + suffixForChildren;
154 
155             // find sub-path restrictions: any permission to a sub-path of the selected path, with lower permission
156             Set<Permission> violatedPerms = findViolatedPermissions(acl.getList(), childPath, permissions);
157             if (!violatedPerms.isEmpty()) {
158                 return false;
159             }
160 
161             Permission childPerm = findBestMatchingPermissions(acl.getList(), childPath);
162             // The child permission must end with /*
163             if (childPerm == null || !granted(childPerm, permissions) || !StringUtils.endsWith(childPerm.getPattern().getPatternString(), "/*")) {
164                 return false;
165             }
166         }
167 
168         return true;
169     }
170 
171     private String stripWildcardsFromPath(String path) {
172         path = StringUtils.stripEnd(path, "/*");
173         if (StringUtils.isBlank(path)) {
174             path = "/";
175         }
176         return path;
177     }
178 
179     private boolean granted(Permission permissionsGranted, long permissionsNeeded) {
180         return (permissionsGranted.getPermissions() & permissionsNeeded) == permissionsNeeded;
181     }
182 }