View Javadoc

1   /**
2    * This file Copyright (c) 2012-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.security.app.dialog.action;
35  
36  import info.magnolia.cms.security.Permission;
37  import info.magnolia.cms.security.PrincipalUtil;
38  import info.magnolia.cms.security.Role;
39  import info.magnolia.cms.security.RoleManager;
40  import info.magnolia.cms.security.SecuritySupport;
41  import info.magnolia.cms.security.auth.ACL;
42  import info.magnolia.cms.security.operations.AccessDefinition;
43  import info.magnolia.context.MgnlContext;
44  import info.magnolia.jcr.util.NodeUtil;
45  import info.magnolia.objectfactory.Components;
46  import info.magnolia.security.app.dialog.field.AccessControlList;
47  import info.magnolia.security.app.dialog.field.WorkspaceAccessFieldFactory;
48  import info.magnolia.security.app.util.UsersWorkspaceUtil;
49  import info.magnolia.ui.admincentral.dialog.action.SaveDialogAction;
50  import info.magnolia.ui.admincentral.dialog.action.SaveDialogActionDefinition;
51  import info.magnolia.ui.api.action.ActionExecutionException;
52  import info.magnolia.ui.form.EditorCallback;
53  import info.magnolia.ui.form.EditorValidator;
54  import info.magnolia.ui.vaadin.integration.jcr.AbstractJcrNodeAdapter;
55  import info.magnolia.ui.vaadin.integration.jcr.JcrNewNodeAdapter;
56  import info.magnolia.ui.vaadin.integration.jcr.JcrNodeAdapter;
57  import info.magnolia.ui.vaadin.integration.jcr.ModelConstants;
58  
59  import java.security.AccessControlException;
60  import java.util.ArrayList;
61  import java.util.List;
62  
63  import javax.jcr.Node;
64  import javax.jcr.RepositoryException;
65  import javax.jcr.Session;
66  import javax.jcr.Value;
67  
68  import org.apache.commons.lang3.StringUtils;
69  
70  import com.vaadin.data.Item;
71  import com.vaadin.data.Property;
72  
73  /**
74   * Save role dialog action. Transforms nodes added by {@link info.magnolia.security.app.dialog.field.WorkspaceAccessFieldFactory} to its final representation.
75   */
76  public class SaveRoleDialogAction extends SaveDialogAction {
77  
78      private final SecuritySupport securitySupport;
79  
80      public SaveRoleDialogAction(SaveDialogActionDefinition definition, Item item, EditorValidator validator, EditorCallback callback, SecuritySupport securitySupport) {
81          super(definition, item, validator, callback);
82          this.securitySupport = securitySupport;
83      }
84  
85      /**
86       * @deprecated since 5.2.1 - use {@link SaveRoleDialogAction#SaveRoleDialogAction(info.magnolia.ui.admincentral.dialog.action.SaveDialogActionDefinition, com.vaadin.data.Item, info.magnolia.ui.form.EditorValidator, info.magnolia.ui.form.EditorCallback, info.magnolia.cms.security.SecuritySupport)} instead.
87       */
88      public SaveRoleDialogAction(SaveDialogActionDefinition definition, Item item, EditorValidator validator, EditorCallback callback) {
89          this(definition, item, validator, callback, Components.getComponent(SecuritySupport.class));
90      }
91  
92      @Override
93      public void execute() throws ActionExecutionException {
94  
95          final JcrNodeAdapter nodeAdapter = (JcrNodeAdapter) item;
96  
97          // First validate
98          validator.showValidation(true);
99          if (validator.isValid() && validateAccessControlLists(nodeAdapter)) {
100             createOrUpdateRole(nodeAdapter);
101             callback.onSuccess(getDefinition().getName());
102 
103         } else {
104             // validation errors are displayed in the UI.
105         }
106     }
107 
108     private void createOrUpdateRole(JcrNodeAdapter roleItem) throws ActionExecutionException {
109         try {
110 
111             final RoleManager roleManager = securitySupport.getRoleManager();
112 
113             final String newRoleName = (String) roleItem.getItemProperty(ModelConstants.JCR_NAME).getValue();
114 
115             Role role;
116             Node roleNode;
117             if (roleItem instanceof JcrNewNodeAdapter) {
118 
119                 // JcrNewNodeAdapter returns the parent JCR item here
120                 Node parentNode = roleItem.getJcrItem();
121                 String parentPath = parentNode.getPath();
122 
123                 role = roleManager.createRole(parentPath, newRoleName);
124                 roleNode = parentNode.getNode(role.getName());
125 
126                 // Repackage the JcrNewNodeAdapter as a JcrNodeAdapter so we can update the node
127                 roleItem = convertNewNodeAdapterForUpdating((JcrNewNodeAdapter) roleItem, roleNode);
128 
129             } else {
130                 roleNode = roleItem.getJcrItem();
131                 String existingRoleName = roleNode.getName();
132 
133                 if (!StringUtils.equals(existingRoleName, newRoleName)) {
134                     String pathBefore = roleNode.getPath();
135                     NodeUtil.renameNode(roleNode, newRoleName);
136                     roleNode.setProperty("name", newRoleName);
137                     UsersWorkspaceUtil.updateAcls(roleNode, pathBefore);
138                 }
139             }
140 
141             roleNode = roleItem.applyChanges();
142 
143             if (roleNode.hasNode("acl_userroles/0")) {
144                 Node entryNode = roleNode.getNode("acl_userroles/0");
145                 entryNode.setProperty(WorkspaceAccessFieldFactory.INTERMEDIARY_FORMAT_PROPERTY_NAME, "true");
146                 entryNode.setProperty(WorkspaceAccessFieldFactory.ACCESS_TYPE_PROPERTY_NAME, AccessControlList.ACCESS_TYPE_NODE);
147                 entryNode.getSession().save();
148             }
149 
150             for (Node aclNode : NodeUtil.getNodes(roleNode)) {
151 
152                 if (aclNode.getName().startsWith("acl_") && !aclNode.getName().equals("acl_uri")) {
153 
154                     AccessControlList acl = new AccessControlList();
155 
156                     for (Node entryNode : NodeUtil.getNodes(aclNode)) {
157 
158                         if (entryNode.hasProperty(WorkspaceAccessFieldFactory.INTERMEDIARY_FORMAT_PROPERTY_NAME)) {
159                             String path = entryNode.getProperty(AccessControlList.PATH_PROPERTY_NAME).getString();
160                             long accessType = entryNode.getProperty(WorkspaceAccessFieldFactory.ACCESS_TYPE_PROPERTY_NAME).getLong();
161                             long permissions = entryNode.getProperty(AccessControlList.PERMISSIONS_PROPERTY_NAME).getLong();
162 
163                             path = stripWildcardsFromPath(path);
164 
165                             if (StringUtils.isNotBlank(path)) {
166                                 acl.addEntry(new AccessControlList.Entry(permissions, accessType, path));
167                             }
168                         }
169                         entryNode.remove();
170                     }
171 
172                     aclNode.setProperty(WorkspaceAccessFieldFactory.INTERMEDIARY_FORMAT_PROPERTY_NAME, (Value) null);
173                     acl.saveEntries(aclNode);
174                 }
175             }
176 
177             roleNode.getSession().save();
178 
179         } catch (final Exception e) {
180             throw new ActionExecutionException(e);
181         }
182     }
183 
184     private JcrNodeAdapter convertNewNodeAdapterForUpdating(JcrNewNodeAdapter newNodeAdapter, Node node) throws RepositoryException {
185 
186         JcrNodeAdapter adapter = new JcrNodeAdapter(node);
187 
188         for (Object propertyId : newNodeAdapter.getItemPropertyIds()) {
189             Property property = adapter.getItemProperty(propertyId);
190             if (property == null) {
191                 adapter.addItemProperty(propertyId, newNodeAdapter.getItemProperty(propertyId));
192             } else {
193                 property.setValue(newNodeAdapter.getItemProperty(propertyId).getValue());
194             }
195         }
196 
197         adapter.getChildren().clear();
198         for (AbstractJcrNodeAdapter child : newNodeAdapter.getChildren().values()) {
199 
200             if (child instanceof JcrNewNodeAdapter) {
201                 if (node.hasNode(child.getNodeName())) {
202                     if (child.getNodeName().startsWith("acl_")) {
203                         child = convertNewNodeAdapterForUpdating((JcrNewNodeAdapter) child, node.getNode(child.getNodeName()));
204                         adapter.addChild(child);
205                     } else {
206                         child.setNodeName(getUniqueNodeNameForChild(child.getParent()));
207                         child.setParent(adapter);
208                         child.setItemId(adapter.getItemId());
209                     }
210                 } else {
211                     child.setParent(adapter);
212                     child.setItemId(adapter.getItemId());
213                 }
214             }
215             adapter.addChild(child);
216         }
217 
218         return adapter;
219     }
220 
221     private String getUniqueNodeNameForChild(AbstractJcrNodeAdapter parentItem) throws RepositoryException {
222 
223         // The adapter cannot handle more than one unnamed child, see MGNLUI-1459, so we have to generate unique ones
224 
225         Node parentNode = null;
226         if (!(parentItem instanceof JcrNewNodeAdapter)) {
227             parentNode = parentItem.getJcrItem();
228         }
229 
230         int newNodeName = 0;
231         while (true) {
232             if (parentItem.getChild(String.valueOf(newNodeName)) != null) {
233                 newNodeName++;
234                 continue;
235             }
236             if (parentNode != null && parentNode.hasNode(String.valueOf(newNodeName))) {
237                 newNodeName++;
238                 continue;
239             }
240             break;
241         }
242 
243         return String.valueOf(newNodeName);
244     }
245 
246     /**
247      * Validates the ACLs present in the dialog. The validation is done on the JcrNodeAdapter because we have to validate
248      * before calling applyChanges. applyChanges() modifies the adapter and it needs to be untouched when validation
249      * fails because it is then still used in the dialog.
250      */
251     private boolean validateAccessControlLists(JcrNodeAdapter roleItem) throws ActionExecutionException {
252 
253         if (MgnlContext.getUser().hasRole(AccessDefinition.DEFAULT_SUPERUSER_ROLE)) {
254             return true;
255         }
256 
257         try {
258             if (roleItem instanceof JcrNewNodeAdapter) {
259                 Node parentNode = roleItem.getJcrItem();
260 
261                 // Make sure this user is allowed to add a role here, the role manager would happily do it and then we'd fail to read the node
262                 parentNode.getSession().checkPermission(parentNode.getPath(), Session.ACTION_ADD_NODE);
263             }
264 
265             for (AbstractJcrNodeAdapter aclItem : roleItem.getChildren().values()) {
266 
267                 String aclNodeName = aclItem.getNodeName();
268 
269                 if (aclNodeName.startsWith("acl_")) {
270 
271                     if (aclItem.getItemProperty(WorkspaceAccessFieldFactory.INTERMEDIARY_FORMAT_PROPERTY_NAME) != null) {
272 
273                         // This is an ACL added using WorkspaceAccessFieldFactory
274 
275                         for (AbstractJcrNodeAdapter entryItem : aclItem.getChildren().values()) {
276 
277                             String path = (String) entryItem.getItemProperty(AccessControlList.PATH_PROPERTY_NAME).getValue();
278                             long accessType = (Long) entryItem.getItemProperty(WorkspaceAccessFieldFactory.ACCESS_TYPE_PROPERTY_NAME).getValue();
279                             long permissions = (Long) entryItem.getItemProperty(AccessControlList.PERMISSIONS_PROPERTY_NAME).getValue();
280 
281                             String workspaceName = StringUtils.replace(aclItem.getNodeName(), "acl_", "");
282 
283                             if (!isCurrentUserEntitledToGrantRights(workspaceName, path, accessType, permissions)) {
284                                 throw new ActionExecutionException("Access violation: could not create role. Have you the necessary grants to create such a role?");
285                             }
286                         }
287                     } else if (aclNodeName.equals("acl_uri")) {
288 
289                         // This is an ACL added using WebAccessFieldFactory
290 
291                         for (AbstractJcrNodeAdapter entryItem : aclItem.getChildren().values()) {
292 
293                             String path = (String) entryItem.getItemProperty(AccessControlList.PATH_PROPERTY_NAME).getValue();
294                             long permissions = (Long) entryItem.getItemProperty(AccessControlList.PERMISSIONS_PROPERTY_NAME).getValue();
295 
296                             if (!isCurrentUserEntitledToGrantUriRights(path, permissions)) {
297                                 throw new ActionExecutionException("Access violation: could not create role. Have you the necessary grants to create such a role?");
298                             }
299                         }
300                     }
301                 }
302             }
303 
304             return true;
305 
306         } catch (AccessControlException e) {
307             throw new ActionExecutionException(e);
308         } catch (RepositoryException e) {
309             throw new ActionExecutionException(e);
310         }
311     }
312 
313     /**
314      * Examines whether the current user creating/editing a role has himself the required permissions to the workspaces
315      * he's specifying in the ACLs. We See MGNLUI-2357.
316      */
317     private boolean isCurrentUserEntitledToGrantRights(String workspaceName, String path, long accessType, long permissions) throws RepositoryException {
318 
319         if (MgnlContext.getUser().hasRole(AccessDefinition.DEFAULT_SUPERUSER_ROLE)) {
320             return true;
321         }
322 
323         // Granting DENY access is only allowed if the user has READ access to the node
324         if (permissions == Permission.NONE) {
325             permissions = Permission.READ;
326         }
327 
328         ACL acl = PrincipalUtil.findAccessControlList(MgnlContext.getSubject(), workspaceName);
329         if (acl == null) {
330             return false;
331         }
332 
333         Permission ownPermissions = findBestMatchingPermissions(acl.getList(), stripWildcardsFromPath(path));
334         if (ownPermissions == null) {
335             return false;
336         }
337 
338         boolean recursive = (accessType & AccessControlList.ACCESS_TYPE_CHILDREN) != 0;
339 
340         if (recursive && !ownPermissions.getPattern().getPatternString().endsWith("/*")) {
341             return false;
342         }
343 
344         return granted(ownPermissions, permissions);
345     }
346 
347     /**
348      * Examines whether the current user creating/editing a role has himself the required permissions to the URIs
349      * he's specifying in the ACLs. We See MGNLUI-2357.
350      */
351     private boolean isCurrentUserEntitledToGrantUriRights(String path, long permissions) throws RepositoryException {
352 
353         if (MgnlContext.getUser().hasRole(AccessDefinition.DEFAULT_SUPERUSER_ROLE)) {
354             return true;
355         }
356 
357         // Granting DENY access is only allowed if the user has READ access to the path
358         if (permissions == Permission.NONE) {
359             permissions = Permission.READ;
360         }
361 
362         ACL acl = PrincipalUtil.findAccessControlList(MgnlContext.getSubject(), "uri");
363         if (acl == null) {
364             return false;
365         }
366 
367         boolean recursive = path.endsWith("*");
368 
369         Permission ownPermissions = findBestMatchingPermissions(acl.getList(), stripWildcardsFromPath(path));
370         if (ownPermissions == null) {
371             return false;
372         }
373 
374         if (recursive && !ownPermissions.getPattern().getPatternString().endsWith("*")) {
375             return false;
376         }
377 
378         return granted(ownPermissions, permissions);
379     }
380 
381     private String stripWildcardsFromPath(String path) {
382         path = StringUtils.stripEnd(path, "/*");
383         if (StringUtils.isBlank(path)) {
384             path = "/";
385         }
386         return path;
387     }
388 
389     private boolean granted(Permission permissionsGranted, long permissionsNeeded) {
390         return (permissionsGranted.getPermissions() & permissionsNeeded) == permissionsNeeded;
391     }
392 
393     private Permission findBestMatchingPermissions(List<Permission> permissions, String path) {
394         if (permissions == null) {
395             return null;
396         }
397         Permission bestMatch = null;
398         long permission = 0;
399         int patternLength = 0;
400         ArrayList<Permission> temp = new ArrayList<Permission>();
401         temp.addAll(permissions);
402         for (Permission p : temp) {
403             if (p.match(path)) {
404                 int l = p.getPattern().getLength();
405                 if (patternLength == l && (permission < p.getPermissions())) {
406                     permission = p.getPermissions();
407                     bestMatch = p;
408                 } else if (patternLength < l) {
409                     patternLength = l;
410                     permission = p.getPermissions();
411                     bestMatch = p;
412                 }
413             }
414         }
415         return bestMatch;
416     }
417 }