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