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;
35  
36  import static java.util.stream.Collectors.toMap;
37  
38  import info.magnolia.cms.core.Path;
39  import info.magnolia.cms.security.PermissionImpl;
40  import info.magnolia.jcr.util.NodeTypes;
41  
42  import java.util.Collection;
43  import java.util.Map;
44  import java.util.function.Function;
45  
46  import javax.jcr.Node;
47  import javax.jcr.RepositoryException;
48  
49  import org.apache.commons.lang3.StringUtils;
50  
51  /**
52   * An extended ACL representation for use with workspace permissions.
53   *
54   * Workspace ACLs are edited with a simplified list of entries. The access type value (selected node and/or sub-nodes)
55   * is derived from the stored paths of entries which share common "base path" (without wildcard) and permission.
56   *
57   * @see WorkspaceAccessFieldFactory
58   */
59  public class WorkspaceAccessControlList extends AccessControlList<WorkspaceAccessControlList.Entry> {
60  
61      static final String ACCESS_TYPE_PROPERTY_NAME = "accessType";
62  
63      public static final long ACCESS_TYPE_NODE = 1;
64      public static final long ACCESS_TYPE_CHILDREN = 2;
65      public static final long ACCESS_TYPE_NODE_AND_CHILDREN = ACCESS_TYPE_NODE | ACCESS_TYPE_CHILDREN;
66  
67      /**
68       * {@linkplain WorkspaceAccessControlList.Entry Entries} are read and eventually merged with combined access type
69       * if two entries are found with same base path and permission.
70       */
71      @Override
72      protected Collection<Entry> createEntries(Node aclNode) throws RepositoryException {
73          Collection<Entry> rawEntries = super.createEntries(aclNode);
74          return mergeEntries(rawEntries);
75      }
76  
77      /**
78       * Create specialized ACL {@linkplain WorkspaceAccessControlList.Entry entries}, with access type.
79       */
80      @Override
81      protected Entry doCreateRawEntry(long permissions, String path) {
82          // use subclass' two-arg ctor, let access type be computed on the fly
83          return new Entry(permissions, path);
84      }
85  
86      /**
87       * {@linkplain WorkspaceAccessControlList.Entry Entries} are saved as follows, according to their access types:
88       * <ul>
89       * <li>{@link #ACCESS_TYPE_NODE}: creates a single node per entry, with base path and permission value</li>
90       * <li>{@link #ACCESS_TYPE_CHILDREN}: creates a single node per entry, with path (wildcard appended) and permission value</li>
91       * <li>{@link #ACCESS_TYPE_NODE_AND_CHILDREN}: creates two nodes per entry, one for each of the above cases, with same permission value</li>
92       * </ul>
93       * <p>Therefore, access type value is not persisted directly to ACL entry nodes.</p>
94       * <p>This also performs additional cleanup upfront, in case the list contains redundant entries.</p>
95       */
96      @Override
97      public void saveEntries(Node aclNode) throws RepositoryException {
98  
99          Collection<Entry> mergedEntries = mergeEntries(getEntries());
100 
101         for (Entry entry : mergedEntries) {
102 
103             // Ignore entries with empty path
104             if (StringUtils.isNotEmpty(entry.getPath())) {
105                 Node entryNode = aclNode.addNode(Path.getUniqueLabel(aclNode, "0"), NodeTypes.ContentNode.NAME);
106 
107                 String path = entry.getPath();
108                 long permissions = entry.getPermissions();
109                 long accessType = entry.getAccessType();
110 
111                 String suffixForChildren = path.equals("/") ? "*" : "/*";
112                 switch ((int) accessType) {
113                 case (int) ACCESS_TYPE_CHILDREN:
114                     path += suffixForChildren;
115                     break;
116                 case (int) ACCESS_TYPE_NODE_AND_CHILDREN:
117                     String nodeName = Path.getUniqueLabel(aclNode, "0");
118                     Node extraEntry = aclNode.addNode(nodeName, NodeTypes.ContentNode.NAME);
119                     extraEntry.setProperty(PATH_PROPERTY_NAME, path + suffixForChildren);
120                     extraEntry.setProperty(PERMISSIONS_PROPERTY_NAME, permissions);
121                     break;
122                 }
123 
124                 entryNode.setProperty(PERMISSIONS_PROPERTY_NAME, permissions);
125                 entryNode.setProperty(PATH_PROPERTY_NAME, path);
126             }
127         }
128     }
129 
130     Collection<Entry> mergeEntries(Collection<Entry> entries) {
131         // Group entries by "key", i.e. by common base path (without wildcard) and permission value
132         // Map values are the input elements (hence the identity), while collisions are handled by merging entries' access type
133         Map<AccessControlList.Entry, Entry> mergedEntriesByKey = entries.stream()
134                 .distinct()
135                 .collect(toMap(Entry::getKey, Function.identity(), Entry::merge));
136 
137         // Drop the keys, return only merged entries
138         return mergedEntriesByKey.values();
139     }
140 
141     /**
142      * An extended {@linkplain AccessControlList.Entry entry} for the workspace ACLs, with notion of access type and sanitized path.
143      */
144     public static class Entry extends AccessControlList.Entry {
145         private long accessType;
146 
147         /**
148          * Creates a workspace access control entry by decoding access type and base path from the given path.
149          */
150         public Entry(long permissions, String path) {
151             super(permissions, path);
152             setPathAndAccessType(path);
153         }
154 
155         /**
156          * Creates a workspace access control entry by decoding base path from the given path.
157          * Given access type is applied as is.
158          *
159          * @throws IllegalArgumentException if access type is different from supported values {@link #ACCESS_TYPE_NODE} (1), {@link #ACCESS_TYPE_CHILDREN} (2) or {@link #ACCESS_TYPE_NODE_AND_CHILDREN} (3)
160          */
161         public Entry(long permissions, long accessType, String path) throws IllegalArgumentException {
162             this(permissions, path);
163             if (accessType == 0) {
164                 throw new IllegalArgumentException("Access type should be one of ACCESS_TYPE_NODE (1), ACCESS_TYPE_CHILDREN (2) or ACCESS_TYPE_NODE_AND_CHILDREN (3)");
165             }
166             this.accessType = accessType;
167         }
168 
169         /**
170          * Decorates the path setter with sanitation of the path given as input.
171          */
172         @Override
173         public void setPath(String path) {
174             setPathAndAccessType(path);
175         }
176 
177         /**
178          * Sanitizes the given path and decodes the access type from it.
179          * <p>Implementation is lenient towards malformed paths with repeated consecutive slashes or stars,
180          * and will reduce path to its simplest form if there are redundant wildcards.</p>
181          * <p>Besides, setting path with a wildcard will tentatively update access type.</p>
182          */
183         void setPathAndAccessType(String path) {
184 
185             if(StringUtils.isEmpty(path)){
186                 path = StringUtils.EMPTY;
187             }
188 
189             // First remove repeated consecutive slashes and stars, then trailing slash if any
190             path = deduplicateSlashesAndStars(path);
191             if (!path.equals("/") && path.endsWith("/")) {
192                 path = StringUtils.removeEnd(path, "/");
193             }
194 
195             if (path.endsWith("/*")) {
196                 // Reduce path to simplest form if there are redundant wildcards
197                 do {
198                     path = StringUtils.removeEnd(path, "/*");
199                 } while (path.endsWith("/*"));
200                 if (path.isEmpty()) {
201                     path = "/";
202                 }
203                 this.accessType |= ACCESS_TYPE_CHILDREN;
204             } else {
205                 // do not add NODE access type if already sanitized
206                 if (this.accessType == 0) {
207                     this.accessType = ACCESS_TYPE_NODE;
208                 }
209             }
210 
211             super.setPath(path);
212         }
213 
214         public long getAccessType() {
215             return accessType;
216         }
217 
218         public void setAccessType(long accessType) {
219             this.accessType = accessType;
220         }
221 
222         /**
223          * Creates and returns a new {@link Entry.Key Entry.Key} representing this entry's grouping.
224          */
225         Entry.Key getKey() {
226             return new Entry.Key(getPermissions(), getPath());
227         }
228 
229         /**
230          * Merges two entries together by combining their respective access type values.
231          */
232         Entry merge(Entry entry) {
233             // only base path and permission need to be equal in order to merge
234             if (!super.equals(entry)) {
235                 throw new IllegalArgumentException("Can only merge ACL entries with same base path (without wildcard) and permission");
236             }
237             accessType |= entry.getAccessType();
238             return this;
239         }
240 
241         private String deduplicateSlashesAndStars(String path) {
242             final StringBuilder builder = new StringBuilder();
243             char[] chars = path.toCharArray();
244             int i = 0;
245             char prevChar = 0;
246             while (i < chars.length) {
247                 char c = chars[i];
248                 if (i <= 0 || c != prevChar || (c != '*' && c != '/')) {
249                     prevChar = c;
250                     builder.append(c);
251                 }
252                 i++;
253             }
254             return builder.toString();
255         }
256 
257         @Override
258         public boolean equals(Object o) {
259             if (this == o) return true;
260             if (o == null || getClass() != o.getClass()) return false;
261             if (!super.equals(o)) return false;
262 
263             Entry entry = (Entry) o;
264             return accessType == entry.accessType;
265         }
266 
267         @Override
268         public int hashCode() {
269             int result = super.hashCode();
270             result = 31 * result + (int) (accessType ^ (accessType >>> 32));
271             return result;
272         }
273 
274         @Override
275         public String toString() {
276             return String.format("WorkspaceAccessControlList.Entry: %s\t%s\t\"%s\"", PermissionImpl.getPermissionAsName(getPermissions()), getAccessTypeName(accessType), getPath());
277         }
278 
279         /**
280          * Entry key is a simplified—incomplete—representation of an {@link Entry}, intended for grouping entries which share common base path (without wildcard) and permission.
281          * If two entries have equal keys, then they can be merged together with combined access type.
282          */
283         class Key extends AccessControlList.Entry {
284             Key(long permissions, String path) {
285                 super(permissions, path);
286             }
287         }
288     }
289 
290     private static String getAccessTypeName(long accessType) {
291         if (accessType == ACCESS_TYPE_NODE) {
292             return "Node";
293         } else if (accessType == ACCESS_TYPE_CHILDREN) {
294             return "Sub-nodes";
295         } else if (accessType == ACCESS_TYPE_NODE_AND_CHILDREN) {
296             return "Node and sub-nodes";
297         } else {
298             return String.format("Undefined (%d)", accessType);
299         }
300     }
301 }