View Javadoc
1   /**
2    * This file Copyright (c) 2011-2015 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.cms.security;
35  
36  import static info.magnolia.cms.security.SecurityConstants.NODE_ROLES;
37  import info.magnolia.cms.core.Path;
38  import info.magnolia.cms.security.auth.ACL;
39  import info.magnolia.cms.util.SimpleUrlPattern;
40  import info.magnolia.cms.util.UrlPattern;
41  import info.magnolia.context.MgnlContext;
42  import info.magnolia.jcr.util.NodeTypes;
43  import info.magnolia.module.InstallContextImpl;
44  import info.magnolia.module.InstallStatus;
45  import info.magnolia.objectfactory.Components;
46  import info.magnolia.repository.RepositoryConstants;
47  
48  import java.util.ArrayList;
49  import java.util.Collection;
50  import java.util.Collections;
51  import java.util.HashMap;
52  import java.util.LinkedList;
53  import java.util.List;
54  import java.util.Map;
55  
56  import javax.jcr.ItemNotFoundException;
57  import javax.jcr.Node;
58  import javax.jcr.NodeIterator;
59  import javax.jcr.PathNotFoundException;
60  import javax.jcr.Property;
61  import javax.jcr.PropertyIterator;
62  import javax.jcr.RepositoryException;
63  import javax.jcr.Session;
64  import javax.jcr.ValueFormatException;
65  import javax.jcr.query.Query;
66  
67  import org.apache.commons.lang.StringUtils;
68  import org.apache.jackrabbit.commons.iterator.FilteringNodeIterator;
69  import org.apache.jackrabbit.commons.predicate.NodeTypePredicate;
70  import org.slf4j.Logger;
71  import org.slf4j.LoggerFactory;
72  
73  /**
74   * Common parent class for repo based security managers.
75   */
76  public abstract class RepositoryBackedSecurityManager {
77  
78      private static final Logger log = LoggerFactory.getLogger(RepositoryBackedSecurityManager.class);
79  
80      public boolean hasAny(final String principalName, final String resourceName, final String resourceTypeName) {
81          long start = System.currentTimeMillis();
82          try {
83              final String sessionName = (StringUtils.equalsIgnoreCase(resourceTypeName, NODE_ROLES)) ? RepositoryConstants.USER_ROLES : RepositoryConstants.USER_GROUPS;
84  
85              // this is an original code from old ***Managers.
86              // TODO: If you ever need to speed it up, turn it around - retrieve group or role by its name and read its ID, then loop through IDs this user has assigned to find out if he has that one or not.
87              final Collection<String> groupsOrRoles = MgnlContext.doInSystemContext(new JCRSessionOp<Collection<String>>(getRepositoryName()) {
88  
89                  @Override
90                  public Collection<String> exec(Session session) throws RepositoryException {
91                      List<String> list = new ArrayList<String>();
92                      Node principal = findPrincipalNode(principalName, session);
93                      if(principal == null) {
94                          log.debug("No User '"+principalName+"' found in repository");
95                          return list;
96                      }
97                      Node groupsOrRoles = principal.getNode(resourceTypeName);
98  
99                      for (PropertyIterator props = groupsOrRoles.getProperties(); props.hasNext();) {
100                         Property property = props.nextProperty();
101                         try {
102                             // just get all the IDs of given type assigned to the principal
103                             list.add(property.getString());
104                         } catch (ItemNotFoundException e) {
105                             log.debug("Role [{}] does not exist in the {} repository", resourceName, resourceTypeName);
106                         } catch (IllegalArgumentException e) {
107                             log.debug("{} has invalid value", property.getPath());
108                         } catch (ValueFormatException e) {
109                             log.debug("{} has invalid value", property.getPath());
110                         }
111                     }
112                     return list;
113                 }
114             });
115 
116 
117             // check if any of the assigned IDs match the requested name
118             return MgnlContext.doInSystemContext(new JCRSessionOp<Boolean>(sessionName) {
119 
120                 @Override
121                 public Boolean exec(Session session) throws RepositoryException {
122                     for (String groupOrRole : groupsOrRoles) {
123                         // check for the existence of this ID
124                         try {
125                             if (session.getNodeByIdentifier(groupOrRole).getName().equalsIgnoreCase(resourceName)) {
126                                 return true;
127                             }
128                         } catch (RepositoryException e) {
129                             log.debug("Role [{}] does not exist in the ROLES repository", resourceName);
130                         }
131                     }
132                     return false;
133                 }});
134 
135         } catch (RepositoryException e) {
136             // Item not found or access denied ...
137             log.debug(e.getMessage(), e);
138         } finally {
139             log.debug("checked {} for {} in {}ms.", new Object[] {resourceName, resourceTypeName, (System.currentTimeMillis() - start)});
140         }
141         return false;
142     }
143 
144     /**
145      * Adds link to a resource (group or role) to the principal (user or group).
146      * This call is lenient and will not throw exception in case principal doesn't exist! Instead it will simply return without making any change.
147      * @param principalName name of the user or group to be updated
148      * @param resourceName name of the group or role to be added
149      * @param resourceTypeName type of the added resource (group or role) {@link SecurityConstants#NODE_ROLES}, {@link SecurityConstants#NODE_GROUPS}
150      */
151     protected void add(final String principalName, final String resourceName, final String resourceTypeName) throws PrincipalNotFoundException {
152         try {
153             final String nodeID = getLinkedResourceId(resourceName, resourceTypeName);
154 
155             if (!hasAny(principalName, resourceName, resourceTypeName)) {
156                 Session session = MgnlContext.getJCRSession(getRepositoryName());
157                 Node principalNode = findPrincipalNode(principalName, session);
158                 if (principalNode == null) {
159                     throw new PrincipalNotFoundException("Principal " + principalName + " of type " + resourceTypeName + " was not found.");
160                 }
161                 if (!principalNode.hasNode(resourceTypeName)) {
162                     principalNode.addNode(resourceTypeName, NodeTypes.ContentNode.NAME);
163                 }
164                 Node node = principalNode.getNode(resourceTypeName);
165                 // add corresponding ID
166                 // used only to get the unique label
167                 String newName = Path.getUniqueLabel(session, node.getPath(), "0");
168                 node.setProperty(newName, nodeID);
169                 session.save();
170             }
171         }
172         catch (RepositoryException e) {
173             log.error("failed to add " + resourceTypeName + " "+ resourceName + " to  [" + principalName + "]", e);
174         }
175     }
176 
177     private String getLinkedResourceId(final String resourceName, final String resourceTypeName) throws AccessDeniedException {
178         final String nodeID;
179         if (StringUtils.equalsIgnoreCase(resourceTypeName, NODE_ROLES)) {
180             Role role = SecuritySupport.Factory.getInstance().getRoleManager().getRole(resourceName);
181             if (role == null) {
182                 log.warn("Invalid role requested: {}", resourceName);
183                 nodeID = null;
184             }
185             else {
186                 nodeID = role.getId();
187             }
188         }
189         else {
190             Group group = SecuritySupport.Factory.getInstance().getGroupManager().getGroup(resourceName);
191             if (group == null) {
192                 log.warn("Invalid group requested: {}", resourceName);
193                 nodeID = null;
194             }
195             else {
196                 nodeID = group.getId();
197             }
198         }
199         return nodeID;
200     }
201 
202     protected String getResourceName(final String resourceId) {
203         try {
204             return MgnlContext.getJCRSession(getRepositoryName()).getNodeByIdentifier(resourceId).getName();
205         } catch (ItemNotFoundException e) {
206             // referenced node doesn't exist
207             return null;
208         }
209         catch (RepositoryException e) {
210             log.error(e.getMessage(), e);
211         }
212         return null;
213     }
214 
215     /**
216      * This call is lenient and will not throw exception in case principal doesn't exist! Instead it will simply return without making any change.
217      *
218      * @param principalName name of the user or group to be updated
219      * @param resourceName name of the group or role to be added
220      * @param resourceTypeName type of the added resource (group or role) {@link SecurityConstants#NODE_ROLES}, {@link SecurityConstants#NODE_GROUPS}
221      */
222     protected void remove(final String principalName, final String resourceName, final String resourceTypeName) throws PrincipalNotFoundException {
223         try {
224             final String nodeID = getLinkedResourceId(resourceName, resourceTypeName);
225 
226             if (hasAny(principalName, resourceName, resourceTypeName)) {
227                 Session session = MgnlContext.getJCRSession(getRepositoryName());
228                 Node principalNode = findPrincipalNode(principalName, session);
229                 if (!principalNode.hasNode(resourceTypeName)) {
230                     throw new PrincipalNotFoundException("Principal " + principalName + " of type " + resourceTypeName + " was not found.");
231                 }
232                 Node node = principalNode.getNode(resourceTypeName);
233                 for (PropertyIterator iter = node.getProperties(); iter.hasNext();) {
234                     Property nodeData = iter.nextProperty();
235                     // check for the existence of this ID
236                     try {
237                         if (nodeData.getString().equals(nodeID)) {
238                             nodeData.remove();
239                             session.save();
240                             // do not break here ... if resource was ever added multiple times remove all occurrences
241                         }
242                     } catch (IllegalArgumentException e) {
243                         log.debug("{} has invalid value", nodeData.getPath());
244                     } catch (ValueFormatException e) {
245                         log.debug("{} has invalid value", nodeData.getPath());
246                     }
247                 }
248             }
249         }
250         catch (RepositoryException e) {
251             log.error("failed to remove " + resourceTypeName + " "+ resourceName + " from [" + principalName + "]", e);
252         }
253     }
254 
255     protected abstract String getRepositoryName();
256 
257     protected abstract Node findPrincipalNode(String principalName, Session session) throws RepositoryException;
258 
259     protected Node findPrincipalNode(String principalName, Session session, String primaryNodeType) throws RepositoryException {
260         return findPrincipalNode(principalName, session, primaryNodeType, null);
261     }
262 
263     /**
264      * Find principal nodes of type {@link NodeTypes.User#NAME}, {@link NodeTypes.Group#NAME} or {@link NodeTypes.Role#NAME}.
265      *
266      * As we don't save sessions during installation phase the principals that are searched might not be visible to JRC queries so we use traversal then.
267      */
268     protected Node findPrincipalNode(String principalName, Session session, String primaryNodeType, Node startNode) throws RepositoryException {
269         final boolean isInstallationPhase = InstallStatus.inProgress.equals(Components.getComponent(InstallContextImpl.class).getStatus());
270         final long start = System.currentTimeMillis();
271 
272         final Node principalNode = isInstallationPhase ? findPrincipalNodeByTraversal(principalName, session, primaryNodeType, startNode) : findPrincipalNodeByQuery(principalName, session, primaryNodeType, startNode);
273         log.debug("Retrieving node took {}ms (isInstallationPhase: {}): path = {}", System.currentTimeMillis() - start, isInstallationPhase, principalNode == null ? "<null>" : principalNode.getPath());
274 
275         return principalNode;
276     }
277 
278     /**
279      * Helper method to find principal nodes using JCR queries. While this might be much faster than traversing, it will not find nodes that have just been created but not saved yet (i.e. during installation of a module).
280      */
281     Node findPrincipalNodeByQuery(String principalName, Session session, String primaryNodeType, Node startNode) throws RepositoryException {
282         final Node root = startNode == null ? session.getRootNode() : startNode;
283 
284         final StringBuilder builder = new StringBuilder("select * from [").append(primaryNodeType).append("] where name() = '").append(principalName).append("'");
285 
286         if (!"/".equals(root.getPath())) {
287             builder.append(" and isdescendantnode(['").append(root.getPath()).append("'])");
288         }
289 
290         final String queryString = builder.toString();
291         log.debug("Executing query \"{}\".", queryString);
292 
293         final Query query = session.getWorkspace().getQueryManager().createQuery(queryString, Query.JCR_SQL2);
294         final NodeIterator iterator = query.execute().getNodes();
295         final Node user = iterator.hasNext() ? iterator.nextNode() : null;
296         if (iterator.hasNext()) {
297             log.error("Query found more than one node of type \"{}\" with name \"{}\" under the root path \"{}\".", primaryNodeType, principalName, root.getPath());
298         }
299         return user;
300     }
301 
302     /**
303      * Helper method to find principal nodes by traversing the jcr tree. While this might be much slower than querying, it will include nodes that have been created but not saved yet (i.e. during installation of a module).
304      */
305     Node findPrincipalNodeByTraversal(String principalName, Session session, String primaryNodeType, Node startNode) throws RepositoryException {
306         final Node root = startNode == null ? session.getRootNode() : startNode;
307         log.debug("Traversing to find nodes of type \"{}\" with name \"{}\" under the root path \"{}\".", primaryNodeType, principalName, root.getPath());
308 
309         final LinkedList<Node> nodes = new LinkedList<Node>();
310 
311         for (NodeIterator iterator = root.getNodes(); iterator.hasNext(); ) {
312             final Node node = iterator.nextNode();
313             if (!node.getName().startsWith(NodeTypes.JCR_PREFIX) && !node.getName().startsWith(NodeTypes.REP_PREFIX)) {
314                 nodes.add(node);
315             }
316         }
317 
318         Node principalNode = null;
319         while (!nodes.isEmpty()) {
320             final Node node = nodes.removeFirst();
321 
322             if (node.getName().equals(principalName) && node.getPrimaryNodeType().getName().equals(primaryNodeType)) {
323                 if (principalNode != null) {
324                     log.error("Traversal found more than one node of type \"{}\" with name \"{}\" under the root path \"{}\".", primaryNodeType, principalName, root.getPath());
325                     break;
326                 }
327                 principalNode = node;
328             }
329 
330             int i = 0;
331             for (NodeIterator iterator = node.getNodes(); iterator.hasNext(); ) {
332                 nodes.add(i++, iterator.nextNode());
333             }
334         }
335         return principalNode;
336     }
337 
338     public Map<String, ACL> getACLs(final String principalName) {
339         return MgnlContext.doInSystemContext(new SilentSessionOp<Map<String,ACL>>(getRepositoryName()) {
340             @Override
341             public Map<String, ACL> doExec(Session session) throws Throwable {
342                 Node node = findPrincipalNode(principalName, session);
343                 if(node == null){
344                     return Collections.emptyMap();
345                 }
346                 return getACLs(node);
347             }});
348     }
349 
350     protected Map<String, ACL> getACLs(Node node) throws RepositoryException, ValueFormatException, PathNotFoundException {
351         Map<String, ACL> principalList = new HashMap<String, ACL>();
352         NodeIterator it = new FilteringNodeIterator(node.getNodes(), new NodeTypePredicate(NodeTypes.ContentNode.NAME, true));
353         while (it.hasNext()) {
354             Node aclEntry = it.nextNode();
355             if (!aclEntry.getName().startsWith("acl")) {
356                 continue;
357             }
358             String name = StringUtils.substringAfter(aclEntry.getName(), "acl_");
359 
360             List<Permission> permissionList = new ArrayList<Permission>();
361             // add acl
362             NodeIterator permissionIterator = new FilteringNodeIterator(aclEntry.getNodes(), new NodeTypePredicate(NodeTypes.ContentNode.NAME, true));
363             while (permissionIterator.hasNext()) {
364                 Node map = permissionIterator.nextNode();
365                 String path = map.getProperty("path").getString();
366                 UrlPattern p = new SimpleUrlPattern(path);
367                 Permission permission = new PermissionImpl();
368                 permission.setPattern(p);
369                 permission.setPermissions(map.getProperty("permissions").getLong());
370                 permissionList.add(permission);
371             }
372 
373             ACL acl;
374             // get the existing acl object if created before with some
375             // other role
376             if (principalList.containsKey(name)) {
377                 acl = principalList.get(name);
378                 permissionList.addAll(acl.getList());
379             }
380             acl = new ACLImpl(name, permissionList);
381             principalList.put(name, acl);
382 
383         }
384         return principalList;
385     }
386 
387 }