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