View Javadoc
1   /**
2    * This file Copyright (c) 2012-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.action;
35  
36  import static info.magnolia.cms.security.operations.AccessDefinition.DEFAULT_SUPERUSER_ROLE;
37  
38  import info.magnolia.cms.core.Path;
39  import info.magnolia.cms.security.Role;
40  import info.magnolia.cms.security.RoleManager;
41  import info.magnolia.cms.security.SecuritySupport;
42  import info.magnolia.context.MgnlContext;
43  import info.magnolia.jcr.util.NodeTypes;
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.util.UsersWorkspaceUtil;
48  import info.magnolia.ui.api.action.ActionExecutionException;
49  import info.magnolia.ui.dialog.action.SaveDialogAction;
50  import info.magnolia.ui.dialog.action.SaveDialogActionDefinition;
51  import info.magnolia.ui.form.EditorCallback;
52  import info.magnolia.ui.form.EditorValidator;
53  import info.magnolia.ui.vaadin.integration.jcr.JcrNewNodeAdapter;
54  import info.magnolia.ui.vaadin.integration.jcr.JcrNodeAdapter;
55  import info.magnolia.ui.vaadin.integration.jcr.ModelConstants;
56  
57  import java.lang.reflect.Field;
58  import java.security.AccessControlException;
59  import java.util.Collection;
60  import java.util.HashMap;
61  import java.util.Map;
62  import java.util.Map.Entry;
63  
64  import javax.jcr.Node;
65  import javax.jcr.RepositoryException;
66  import javax.jcr.Session;
67  
68  import org.apache.commons.lang3.StringUtils;
69  import org.slf4j.Logger;
70  import org.slf4j.LoggerFactory;
71  
72  import com.google.common.base.Predicate;
73  import com.google.common.collect.Collections2;
74  import com.google.common.collect.ImmutableList;
75  import com.vaadin.v7.data.Item;
76  import com.vaadin.v7.data.Property;
77  
78  /**
79   * Save action for the role dialog.
80   *
81   * <p>Saving of ACL entries to the role node is delegated to {@link AccessControlList AccessControlLists}.
82   * These typed ACLs are carried over from the form, as properties of the dialog item.
83   * They get removed from the item here, not to interfere with the JCR adapter.</p>
84   *
85   * <p>Other properties of the role item are saved by the regular JCR adapter.</p>
86   *
87   * @see info.magnolia.security.app.dialog.field.WorkspaceAccessFieldFactory
88   * @see info.magnolia.security.app.dialog.field.WebAccessFieldFactory
89   */
90  public class SaveRoleDialogAction extends SaveDialogAction {
91  
92      private static final Logger log = LoggerFactory.getLogger(SaveRoleDialogAction.class);
93      private final SecuritySupport securitySupport;
94  
95      public SaveRoleDialogAction(SaveDialogActionDefinition definition, Item item, EditorValidator validator, EditorCallback callback, SecuritySupport securitySupport) {
96          super(definition, item, validator, callback);
97          this.securitySupport = securitySupport;
98      }
99  
100     /**
101      * @deprecated since 5.2.1 - use {@link #SaveRoleDialogAction(SaveDialogActionDefinition, Item, EditorValidator, EditorCallback, SecuritySupport)} instead.
102      */
103     @Deprecated
104     public SaveRoleDialogAction(SaveDialogActionDefinition definition, Item item, EditorValidator validator, EditorCallback callback) {
105         this(definition, item, validator, callback, Components.getComponent(SecuritySupport.class));
106     }
107 
108     @Override
109     public void execute() throws ActionExecutionException {
110         final JcrNodeAdapter nodeAdapter = (JcrNodeAdapter) item;
111 
112         if (validateForm() && validateNewRolePermission(nodeAdapter)) {
113             createOrUpdateRole(nodeAdapter);
114             callback.onSuccess(getDefinition().getName());
115         }
116     }
117 
118     /**
119      * Override this function to make sure all previous errors are clean before calling validate.
120      */
121     protected boolean validateForm() {
122         validator.showValidation(false);
123         boolean isValid = validator.isValid();
124         validator.showValidation(!isValid);
125         if (!isValid) {
126             log.info("Validation error(s) occurred. No save performed.");
127         }
128         return isValid;
129     }
130 
131     private boolean validateNewRolePermission(JcrNodeAdapter roleItem) throws ActionExecutionException {
132         if (MgnlContext.getUser().hasRole(DEFAULT_SUPERUSER_ROLE)) {
133             return true;
134         }
135 
136         // 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
137         try {
138             if (roleItem instanceof JcrNewNodeAdapter) {
139                 Node parentNode = roleItem.getJcrItem();
140                 parentNode.getSession().checkPermission(parentNode.getPath(), Session.ACTION_ADD_NODE);
141             }
142             return true;
143         } catch (AccessControlException | RepositoryException e) {
144             throw new ActionExecutionException(e);
145         }
146     }
147 
148     private void createOrUpdateRole(JcrNodeAdapter roleItem) throws ActionExecutionException {
149         try {
150 
151             final RoleManager roleManager = securitySupport.getRoleManager();
152             final String newRoleName = Path.getValidatedLabel((String) roleItem.getItemProperty(ModelConstants.JCR_NAME).getValue());
153 
154             Node roleNode;
155 
156             // Remove ACL properties from JCR adapter so that it doesn't try to save them (and fail because JCR doesn't know about this type)
157             Map<String, AccessControlList<AccessControlList.Entry>> aclsProperties = removeTransientAclProperties(roleItem);
158 
159             if (roleItem instanceof JcrNewNodeAdapter) {
160                 // JcrNewNodeAdapter returns the parent JCR item here
161                 Node parentNode = roleItem.getJcrItem();
162                 String parentPath = parentNode.getPath();
163 
164                 Role role = roleManager.createRole(parentPath, newRoleName);
165                 roleNode = parentNode.getNode(role.getName());
166                 // Repackage the JcrNewNodeAdapter as a JcrNodeAdapter so we can update the node
167                 JcrNodeAdapter newRoleItem = convertNewNodeAdapterForUpdating((JcrNewNodeAdapter) roleItem, roleNode, newRoleName);
168                 roleNode = newRoleItem.applyChanges();
169                 // Workaround that updates item id of the roleItem so we can use it in OpenAddRoleDialogAction to fire ContentChangedEvent
170                 try {
171                     Field f = roleItem.getClass().getDeclaredField("appliedChanges");
172                     f.setAccessible(true);
173                     f.setBoolean(roleItem, true);
174                     f.setAccessible(false);
175                     roleItem.setItemId(newRoleItem.getItemId());
176                 } catch (IllegalAccessException | NoSuchFieldException e) {
177                     log.warn("Unable to set new JcrItemId for adapter {}", roleItem, e);
178                 }
179 
180                 updateAcls(roleNode, aclsProperties, false);
181 
182             } else {
183                 // First fetch the initial name (changes not applied yet here).
184                 String existingRoleName = roleItem.getJcrItem().getName();
185                 String pathBefore = roleItem.getJcrItem().getPath();
186                 // Apply changes now since the further operations on ACL's are done on nodes.
187                 roleNode = roleItem.applyChanges();
188 
189                 // apply ACL changes before renaming node (so that it doesn't add old entries again)
190                 updateAcls(roleNode, aclsProperties, true);
191 
192                 if (!StringUtils.equals(existingRoleName, newRoleName)) {
193                     UsersWorkspaceUtil.updateAcls(roleNode, pathBefore);
194                 }
195             }
196 
197             roleNode.getSession().save();
198         } catch (final RepositoryException e) {
199             throw new ActionExecutionException(e);
200         }
201     }
202 
203     private void updateAcls(Node roleNode, Map<String, AccessControlList<AccessControlList.Entry>> acls, boolean removeOldEntries) throws RepositoryException {
204         for (Entry<String, AccessControlList<AccessControlList.Entry>> aclEntry : acls.entrySet()) {
205             String aclNodeName = aclEntry.getKey();
206             AccessControlList<AccessControlList.Entry> acl = aclEntry.getValue();
207 
208             Node aclNode;
209             if (roleNode.hasNode(aclNodeName)) {
210                 aclNode = roleNode.getNode(aclNodeName);
211                 if (removeOldEntries) {
212                     // Clean up all exiting child before saving all entries
213                     for (Node entryNode : NodeUtil.getNodes(aclNode)) {
214                         entryNode.remove();
215                     }
216                 }
217             } else {
218                 aclNode = roleNode.addNode(aclNodeName, NodeTypes.ContentNode.NAME);
219             }
220 
221             acl.saveEntries(aclNode);
222 
223             // Prevent saving of empty ACL nodes
224             if (!aclNode.hasNodes()) {
225                 aclNode.remove();
226             }
227         }
228     }
229 
230     private JcrNodeAdapter convertNewNodeAdapterForUpdating(JcrNewNodeAdapter newNodeAdapter, Node node, String newRoleName) throws RepositoryException {
231 
232         JcrNodeAdapter adapter = new JcrNodeAdapter(node);
233 
234         for (Object propertyId : newNodeAdapter.getItemPropertyIds()) {
235             Property property = adapter.getItemProperty(propertyId);
236             if (property == null) {
237                 adapter.addItemProperty(propertyId, newNodeAdapter.getItemProperty(propertyId));
238             } else if (ModelConstants.JCR_NAME.equals(propertyId) && newRoleName != null) {
239                 property.setValue(node.getName());
240             } else {
241                 property.setValue(newNodeAdapter.getItemProperty(propertyId).getValue());
242             }
243         }
244         return adapter;
245     }
246 
247     /**
248      * Filter the role's Vaadin item for propertyIds starting with "acl_", put 'em in the return map and remove 'em from the adapter.
249      */
250     private Map<String, AccessControlList<AccessControlList.Entry>> removeTransientAclProperties(final JcrNodeAdapter roleItem) {
251         // make a copy, because we're gonna remove properties while iterating on them further below
252         Collection<?> propertyIds = ImmutableList.copyOf(roleItem.getItemPropertyIds());
253         Collection<?> aclPropertyIds = Collections2.filter(propertyIds, new Predicate<Object>() {
254             @Override
255             public boolean apply(Object propertyId) {
256                 return propertyId instanceof String && ((String) propertyId).startsWith("acl_");
257             }
258         });
259 
260         Map<String, AccessControlList<AccessControlList.Entry>> acls = new HashMap<>();
261         for (Object aclPropertyId : aclPropertyIds) {
262             AccessControlList<AccessControlList.Entry> acl = (AccessControlList<AccessControlList.Entry>) roleItem.getItemProperty(aclPropertyId).getValue();
263             acls.put(aclPropertyId.toString(), acl);
264             roleItem.removeItemProperty(aclPropertyId);
265         }
266 
267         return acls;
268     }
269 }