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