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                 roleNode = roleItem.applyChanges();
129             } else {
130                 // First fetch the initial name (changes not applied yet here).
131                 String existingRoleName = roleItem.getJcrItem().getName();
132                 String pathBefore = roleItem.getJcrItem().getPath();
133                 // Apply changes now since the further operations on ACL's are done on nodes.
134                 roleNode = roleItem.applyChanges();
135                 if (!StringUtils.equals(existingRoleName, newRoleName)) {
136                     NodeUtil.renameNode(roleNode, newRoleName);
137                     roleNode.setProperty("name", newRoleName);
138                     UsersWorkspaceUtil.updateAcls(roleNode, pathBefore);
139                 }
140             }
141 
142             if (roleNode.hasNode("acl_userroles/0")) {
143                 Node entryNode = roleNode.getNode("acl_userroles/0");
144                 entryNode.setProperty(WorkspaceAccessFieldFactory.INTERMEDIARY_FORMAT_PROPERTY_NAME, "true");
145                 entryNode.setProperty(WorkspaceAccessFieldFactory.ACCESS_TYPE_PROPERTY_NAME, AccessControlList.ACCESS_TYPE_NODE);
146                 entryNode.getSession().save();
147             }
148 
149             for (Node aclNode : NodeUtil.getNodes(roleNode)) {
150 
151                 if (aclNode.getName().startsWith("acl_") && !aclNode.getName().equals("acl_uri")) {
152 
153                     AccessControlList acl = new AccessControlList();
154 
155                     for (Node entryNode : NodeUtil.getNodes(aclNode)) {
156 
157                         if (entryNode.hasProperty(WorkspaceAccessFieldFactory.INTERMEDIARY_FORMAT_PROPERTY_NAME)) {
158                             String path = entryNode.getProperty(AccessControlList.PATH_PROPERTY_NAME).getString();
159                             long accessType = entryNode.getProperty(WorkspaceAccessFieldFactory.ACCESS_TYPE_PROPERTY_NAME).getLong();
160                             long permissions = entryNode.getProperty(AccessControlList.PERMISSIONS_PROPERTY_NAME).getLong();
161 
162                             path = stripWildcardsFromPath(path);
163 
164                             if (StringUtils.isNotBlank(path)) {
165                                 acl.addEntry(new AccessControlList.Entry(permissions, accessType, path));
166                             }
167                         }
168                         entryNode.remove();
169                     }
170 
171                     aclNode.setProperty(WorkspaceAccessFieldFactory.INTERMEDIARY_FORMAT_PROPERTY_NAME, (Value) null);
172                     acl.saveEntries(aclNode);
173                 }
174             }
175 
176             roleNode.getSession().save();
177         } catch (final Exception e) {
178             throw new ActionExecutionException(e);
179         }
180     }
181 
182     private JcrNodeAdapter convertNewNodeAdapterForUpdating(JcrNewNodeAdapter newNodeAdapter, Node node) throws RepositoryException {
183 
184         JcrNodeAdapter adapter = new JcrNodeAdapter(node);
185 
186         for (Object propertyId : newNodeAdapter.getItemPropertyIds()) {
187             Property property = adapter.getItemProperty(propertyId);
188             if (property == null) {
189                 adapter.addItemProperty(propertyId, newNodeAdapter.getItemProperty(propertyId));
190             } else {
191                 property.setValue(newNodeAdapter.getItemProperty(propertyId).getValue());
192             }
193         }
194 
195         adapter.getChildren().clear();
196         for (AbstractJcrNodeAdapter child : newNodeAdapter.getChildren().values()) {
197 
198             if (child instanceof JcrNewNodeAdapter) {
199                 if (node.hasNode(child.getNodeName())) {
200                     if (child.getNodeName().startsWith("acl_")) {
201                         child = convertNewNodeAdapterForUpdating((JcrNewNodeAdapter) child, node.getNode(child.getNodeName()));
202                         adapter.addChild(child);
203                     } else {
204                         child.setNodeName(getUniqueNodeNameForChild(child.getParent()));
205                         child.setParent(adapter);
206                         child.setItemId(adapter.getItemId());
207                     }
208                 } else {
209                     child.setParent(adapter);
210                     child.setItemId(adapter.getItemId());
211                 }
212             }
213             adapter.addChild(child);
214         }
215 
216         return adapter;
217     }
218 
219     private String getUniqueNodeNameForChild(AbstractJcrNodeAdapter parentItem) throws RepositoryException {
220 
221         // The adapter cannot handle more than one unnamed child, see MGNLUI-1459, so we have to generate unique ones
222 
223         Node parentNode = null;
224         if (!(parentItem instanceof JcrNewNodeAdapter)) {
225             parentNode = parentItem.getJcrItem();
226         }
227 
228         int newNodeName = 0;
229         while (true) {
230             if (parentItem.getChild(String.valueOf(newNodeName)) != null) {
231                 newNodeName++;
232                 continue;
233             }
234             if (parentNode != null && parentNode.hasNode(String.valueOf(newNodeName))) {
235                 newNodeName++;
236                 continue;
237             }
238             break;
239         }
240 
241         return String.valueOf(newNodeName);
242     }
243 
244     /**
245      * Validates the ACLs present in the dialog. The validation is done on the JcrNodeAdapter because we have to validate
246      * before calling applyChanges. applyChanges() modifies the adapter and it needs to be untouched when validation
247      * fails because it is then still used in the dialog.
248      */
249     private boolean validateAccessControlLists(JcrNodeAdapter roleItem) throws ActionExecutionException {
250 
251         if (MgnlContext.getUser().hasRole(AccessDefinition.DEFAULT_SUPERUSER_ROLE)) {
252             return true;
253         }
254 
255         try {
256             if (roleItem instanceof JcrNewNodeAdapter) {
257                 Node parentNode = roleItem.getJcrItem();
258 
259                 // 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
260                 parentNode.getSession().checkPermission(parentNode.getPath(), Session.ACTION_ADD_NODE);
261             }
262 
263             for (AbstractJcrNodeAdapter aclItem : roleItem.getChildren().values()) {
264 
265                 String aclNodeName = aclItem.getNodeName();
266 
267                 if (aclNodeName.startsWith("acl_")) {
268 
269                     if (aclItem.getItemProperty(WorkspaceAccessFieldFactory.INTERMEDIARY_FORMAT_PROPERTY_NAME) != null) {
270 
271                         // This is an ACL added using WorkspaceAccessFieldFactory
272 
273                         for (AbstractJcrNodeAdapter entryItem : aclItem.getChildren().values()) {
274 
275                             String path = (String) entryItem.getItemProperty(AccessControlList.PATH_PROPERTY_NAME).getValue();
276                             long accessType = (Long) entryItem.getItemProperty(WorkspaceAccessFieldFactory.ACCESS_TYPE_PROPERTY_NAME).getValue();
277                             long permissions = (Long) entryItem.getItemProperty(AccessControlList.PERMISSIONS_PROPERTY_NAME).getValue();
278 
279                             String workspaceName = StringUtils.replace(aclItem.getNodeName(), "acl_", "");
280 
281                             if (!isCurrentUserEntitledToGrantRights(workspaceName, path, accessType, permissions)) {
282                                 throw new ActionExecutionException("Access violation: could not create role. Have you the necessary grants to create such a role?");
283                             }
284                         }
285                     } else if (aclNodeName.equals("acl_uri")) {
286 
287                         // This is an ACL added using WebAccessFieldFactory
288 
289                         for (AbstractJcrNodeAdapter entryItem : aclItem.getChildren().values()) {
290 
291                             String path = (String) entryItem.getItemProperty(AccessControlList.PATH_PROPERTY_NAME).getValue();
292                             long permissions = (Long) entryItem.getItemProperty(AccessControlList.PERMISSIONS_PROPERTY_NAME).getValue();
293 
294                             if (!isCurrentUserEntitledToGrantUriRights(path, permissions)) {
295                                 throw new ActionExecutionException("Access violation: could not create role. Have you the necessary grants to create such a role?");
296                             }
297                         }
298                     }
299                 }
300             }
301 
302             return true;
303 
304         } catch (AccessControlException e) {
305             throw new ActionExecutionException(e);
306         } catch (RepositoryException e) {
307             throw new ActionExecutionException(e);
308         }
309     }
310 
311     /**
312      * Examines whether the current user creating/editing a role has himself the required permissions to the workspaces
313      * he's specifying in the ACLs. We See MGNLUI-2357.
314      */
315     private boolean isCurrentUserEntitledToGrantRights(String workspaceName, String path, long accessType, long permissions) throws RepositoryException {
316 
317         if (MgnlContext.getUser().hasRole(AccessDefinition.DEFAULT_SUPERUSER_ROLE)) {
318             return true;
319         }
320 
321         // Granting DENY access is only allowed if the user has READ access to the node
322         if (permissions == Permission.NONE) {
323             permissions = Permission.READ;
324         }
325 
326         ACL acl = PrincipalUtil.findAccessControlList(MgnlContext.getSubject(), workspaceName);
327         if (acl == null) {
328             return false;
329         }
330 
331         Permission ownPermissions = findBestMatchingPermissions(acl.getList(), stripWildcardsFromPath(path));
332         if (ownPermissions == null) {
333             return false;
334         }
335 
336         boolean recursive = (accessType & AccessControlList.ACCESS_TYPE_CHILDREN) != 0;
337 
338         if (recursive && !ownPermissions.getPattern().getPatternString().endsWith("/*")) {
339             return false;
340         }
341 
342         return granted(ownPermissions, permissions);
343     }
344 
345     /**
346      * Examines whether the current user creating/editing a role has himself the required permissions to the URIs
347      * he's specifying in the ACLs. We See MGNLUI-2357.
348      */
349     private boolean isCurrentUserEntitledToGrantUriRights(String path, long permissions) throws RepositoryException {
350 
351         if (MgnlContext.getUser().hasRole(AccessDefinition.DEFAULT_SUPERUSER_ROLE)) {
352             return true;
353         }
354 
355         // Granting DENY access is only allowed if the user has READ access to the path
356         if (permissions == Permission.NONE) {
357             permissions = Permission.READ;
358         }
359 
360         ACL acl = PrincipalUtil.findAccessControlList(MgnlContext.getSubject(), "uri");
361         if (acl == null) {
362             return false;
363         }
364 
365         boolean recursive = path.endsWith("*");
366 
367         Permission ownPermissions = findBestMatchingPermissions(acl.getList(), stripWildcardsFromPath(path));
368         if (ownPermissions == null) {
369             return false;
370         }
371 
372         if (recursive && !ownPermissions.getPattern().getPatternString().endsWith("*")) {
373             return false;
374         }
375 
376         return granted(ownPermissions, permissions);
377     }
378 
379     private String stripWildcardsFromPath(String path) {
380         path = StringUtils.stripEnd(path, "/*");
381         if (StringUtils.isBlank(path)) {
382             path = "/";
383         }
384         return path;
385     }
386 
387     private boolean granted(Permission permissionsGranted, long permissionsNeeded) {
388         return (permissionsGranted.getPermissions() & permissionsNeeded) == permissionsNeeded;
389     }
390 
391     private Permission findBestMatchingPermissions(List<Permission> permissions, String path) {
392         if (permissions == null) {
393             return null;
394         }
395         Permission bestMatch = null;
396         long permission = 0;
397         int patternLength = 0;
398         ArrayList<Permission> temp = new ArrayList<Permission>();
399         temp.addAll(permissions);
400         for (Permission p : temp) {
401             if (p.match(path)) {
402                 int l = p.getPattern().getLength();
403                 if (patternLength == l && (permission < p.getPermissions())) {
404                     permission = p.getPermissions();
405                     bestMatch = p;
406                 } else if (patternLength < l) {
407                     patternLength = l;
408                     permission = p.getPermissions();
409                     bestMatch = p;
410                 }
411             }
412         }
413         return bestMatch;
414     }
415 }