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