View Javadoc
1   /**
2    * This file Copyright (c) 2011-2015 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
32   *
33   */
34  package info.magnolia.cms.security;
35  
36  import static info.magnolia.cms.security.SecurityConstants.NODE_ROLES;
37  import info.magnolia.cms.core.Path;
38  import info.magnolia.cms.security.auth.ACL;
39  import info.magnolia.cms.util.SimpleUrlPattern;
40  import info.magnolia.cms.util.UrlPattern;
41  import info.magnolia.context.MgnlContext;
42  import info.magnolia.jcr.util.NodeTypes;
43  import info.magnolia.module.InstallContextImpl;
44  import info.magnolia.module.InstallStatus;
45  import info.magnolia.objectfactory.Components;
46  import info.magnolia.repository.RepositoryConstants;
47  
48  import java.util.ArrayList;
49  import java.util.Collection;
50  import java.util.Collections;
51  import java.util.HashMap;
52  import java.util.LinkedList;
53  import java.util.List;
54  import java.util.Map;
55  
56  import javax.jcr.ItemNotFoundException;
57  import javax.jcr.Node;
58  import javax.jcr.NodeIterator;
59  import javax.jcr.PathNotFoundException;
60  import javax.jcr.Property;
61  import javax.jcr.PropertyIterator;
62  import javax.jcr.RepositoryException;
63  import javax.jcr.Session;
64  import javax.jcr.ValueFormatException;
65  import javax.jcr.query.Query;
66  
67  import org.apache.commons.lang3.StringUtils;
68  import org.apache.jackrabbit.commons.iterator.FilteringNodeIterator;
69  import org.apache.jackrabbit.commons.predicate.NodeTypePredicate;
70  import org.slf4j.Logger;
71  import org.slf4j.LoggerFactory;
72  
73  /**
74   * Common parent class for repo based security managers.
75   */
76  public abstract class RepositoryBackedSecurityManager {
77  
78      private static final Logger log = LoggerFactory.getLogger(RepositoryBackedSecurityManager.class);
79  
80      /**
81       * Name of the subnodes hosting groups.
82       */
83      static final String GROUPS_NODE_NAME = "groups";
84  
85      /**
86       * Name of the subnodes hosting roles.
87       */
88      static final String ROLES_NODE_NAME = "roles";
89  
90  
91      public boolean hasAny(final String principalName, final String resourceName, final String resourceTypeName) {
92          long start = System.currentTimeMillis();
93          try {
94              final String sessionName = (StringUtils.equalsIgnoreCase(resourceTypeName, NODE_ROLES)) ? RepositoryConstants.USER_ROLES : RepositoryConstants.USER_GROUPS;
95  
96              // this is an original code from old ***Managers.
97              // 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.
98              final Collection<String> groupsOrRoles = MgnlContext.doInSystemContext(new JCRSessionOp<Collection<String>>(getRepositoryName()) {
99  
100                 @Override
101                 public Collection<String> exec(Session session) throws RepositoryException {
102                     List<String> list = new ArrayList<String>();
103                     Node principal = findPrincipalNode(principalName, session);
104                     if(principal == null) {
105                         log.debug("No User '{}' found in repository", principalName);
106                         return list;
107                     }
108                     Node groupsOrRoles = principal.getNode(resourceTypeName);
109 
110                     for (PropertyIterator props = groupsOrRoles.getProperties(); props.hasNext();) {
111                         Property property = props.nextProperty();
112                         try {
113                             // just get all the IDs of given type assigned to the principal
114                             list.add(property.getString());
115                         } catch (ItemNotFoundException e) {
116                             log.debug("Role [{}] does not exist in the {} repository", resourceName, resourceTypeName);
117                         } catch (IllegalArgumentException e) {
118                             log.debug("{} has invalid value", property.getPath());
119                         } catch (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         } 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      * @param principalName name of the user or group to be updated
159      * @param resourceName name of the group or role to be added
160      * @param resourceTypeName type of the added resource (group or role) {@link SecurityConstants#NODE_ROLES}, {@link SecurityConstants#NODE_GROUPS}
161      */
162     protected void add(final String principalName, final String resourceName, final String resourceTypeName) throws PrincipalNotFoundException {
163         try {
164             final String nodeID = getLinkedResourceId(resourceName, resourceTypeName);
165 
166             if (!hasAny(principalName, resourceName, resourceTypeName)) {
167                 Session session = MgnlContext.getJCRSession(getRepositoryName());
168                 Node principalNode = findPrincipalNode(principalName, session);
169                 if (principalNode == null) {
170                     throw new PrincipalNotFoundException("Principal " + principalName + " of type " + resourceTypeName + " was not found.");
171                 }
172                 if (!principalNode.hasNode(resourceTypeName)) {
173                     principalNode.addNode(resourceTypeName, NodeTypes.ContentNode.NAME);
174                 }
175                 Node node = principalNode.getNode(resourceTypeName);
176                 // add corresponding ID
177                 // used only to get the unique label
178                 String newName = Path.getUniqueLabel(session, node.getPath(), "0");
179                 node.setProperty(newName, nodeID);
180                 session.save();
181             }
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             }
196             else {
197                 nodeID = role.getId();
198             }
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             }
206             else {
207                 nodeID = group.getId();
208             }
209         }
210         return nodeID;
211     }
212 
213     protected String getResourceName(final String resourceId) {
214         try {
215             return MgnlContext.getJCRSession(getRepositoryName()).getNodeByIdentifier(resourceId).getName();
216         } catch (ItemNotFoundException e) {
217             // referenced node doesn't exist
218             return null;
219         }
220         catch (RepositoryException e) {
221             log.error(e.getMessage(), e);
222         }
223         return null;
224     }
225 
226     /**
227      * This call is lenient and will not throw exception in case principal doesn't exist! Instead it will simply return without making any change.
228      *
229      * @param principalName name of the user or group to be updated
230      * @param resourceName name of the group or role to be added
231      * @param resourceTypeName type of the added resource (group or role) {@link SecurityConstants#NODE_ROLES}, {@link SecurityConstants#NODE_GROUPS}
232      */
233     protected void remove(final String principalName, final String resourceName, final String resourceTypeName) throws PrincipalNotFoundException {
234         try {
235             final String nodeID = getLinkedResourceId(resourceName, resourceTypeName);
236 
237             if (hasAny(principalName, resourceName, resourceTypeName)) {
238                 Session session = MgnlContext.getJCRSession(getRepositoryName());
239                 Node principalNode = findPrincipalNode(principalName, session);
240                 if (!principalNode.hasNode(resourceTypeName)) {
241                     throw new PrincipalNotFoundException("Principal " + principalName + " of type " + resourceTypeName + " was not found.");
242                 }
243                 Node node = principalNode.getNode(resourceTypeName);
244                 for (PropertyIterator iter = node.getProperties(); iter.hasNext();) {
245                     Property nodeData = iter.nextProperty();
246                     // check for the existence of this ID
247                     try {
248                         if (nodeData.getString().equals(nodeID)) {
249                             nodeData.remove();
250                             session.save();
251                             // do not break here ... if resource was ever added multiple times remove all occurrences
252                         }
253                     } catch (IllegalArgumentException e) {
254                         log.debug("{} has invalid value", nodeData.getPath());
255                     } catch (ValueFormatException e) {
256                         log.debug("{} has invalid value", nodeData.getPath());
257                     }
258                 }
259             }
260         }
261         catch (RepositoryException e) {
262             log.error("failed to remove {} {} from [{}]", resourceTypeName, resourceName, principalName, e);
263         }
264     }
265 
266     protected abstract String getRepositoryName();
267 
268     protected abstract Node findPrincipalNode(String principalName, Session session) throws RepositoryException;
269 
270     protected Node findPrincipalNode(String principalName, Session session, String primaryNodeType) throws RepositoryException {
271         return findPrincipalNode(principalName, session, primaryNodeType, null);
272     }
273 
274     /**
275      * Find principal nodes of type {@link NodeTypes.User#NAME}, {@link NodeTypes.Group#NAME} or {@link NodeTypes.Role#NAME}.
276      *
277      * 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.
278      */
279     protected Node findPrincipalNode(String principalName, Session session, String primaryNodeType, Node startNode) throws RepositoryException {
280         final boolean isInstallationPhase = InstallStatus.inProgress.equals(Components.getComponent(InstallContextImpl.class).getStatus());
281         final long start = System.currentTimeMillis();
282 
283         final Node principalNode = isInstallationPhase ? findPrincipalNodeByTraversal(principalName, session, primaryNodeType, startNode) : findPrincipalNodeByQuery(principalName, session, primaryNodeType, startNode);
284         log.debug("Retrieving node took {}ms (isInstallationPhase: {}): path = {}", System.currentTimeMillis() - start, isInstallationPhase, principalNode == null ? "<null>" : principalNode.getPath());
285 
286         if (principalNode == null) {
287             log.debug("Could not find principal node '{}' of primary type '{}' under startnode '{}' in workspace '{}'.", principalName, primaryNodeType, startNode == null ? "/" : startNode.getPath(), session.getWorkspace().getName());
288         }
289         return principalNode;
290     }
291 
292     /**
293      * 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).
294      */
295     Node findPrincipalNodeByQuery(String principalName, Session session, String primaryNodeType, Node startNode) throws RepositoryException {
296         final Node root = startNode == null ? session.getRootNode() : startNode;
297 
298         final StringBuilder builder = new StringBuilder("select * from [").append(primaryNodeType).append("] where name() = '").append(principalName).append("'");
299 
300         if (!"/".equals(root.getPath())) {
301             builder.append(" and isdescendantnode(['").append(root.getPath()).append("'])");
302         }
303 
304         final String queryString = builder.toString();
305         log.debug("Executing query \"{}\".", queryString);
306 
307         final Query query = session.getWorkspace().getQueryManager().createQuery(queryString, Query.JCR_SQL2);
308         final NodeIterator iterator = query.execute().getNodes();
309         final Node user = iterator.hasNext() ? iterator.nextNode() : null;
310         if (iterator.hasNext()) {
311             log.error("Query found more than one node of type \"{}\" with name \"{}\" under the root path \"{}\".", primaryNodeType, principalName, root.getPath());
312         }
313         return user;
314     }
315 
316     /**
317      * 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).
318      */
319     Node findPrincipalNodeByTraversal(String principalName, Session session, String primaryNodeType, Node startNode) throws RepositoryException {
320         final Node root = startNode == null ? session.getRootNode() : startNode;
321         log.debug("Traversing to find nodes of type \"{}\" with name \"{}\" under the root path \"{}\".", primaryNodeType, principalName, root.getPath());
322 
323         final LinkedList<Node> nodes = new LinkedList<Node>();
324 
325         for (NodeIterator iterator = root.getNodes(); iterator.hasNext(); ) {
326             final Node node = iterator.nextNode();
327             if (!node.getName().startsWith(NodeTypes.JCR_PREFIX) && !node.getName().startsWith(NodeTypes.REP_PREFIX)) {
328                 nodes.add(node);
329             }
330         }
331 
332         Node principalNode = null;
333         while (!nodes.isEmpty()) {
334             final Node node = nodes.removeFirst();
335 
336             if (node.getName().equals(principalName) && node.getPrimaryNodeType().getName().equals(primaryNodeType)) {
337                 if (principalNode != null) {
338                     log.error("Traversal found more than one node of type \"{}\" with name \"{}\" under the root path \"{}\".", primaryNodeType, principalName, root.getPath());
339                     break;
340                 }
341                 principalNode = node;
342             }
343 
344             int i = 0;
345             for (NodeIterator iterator = node.getNodes(); iterator.hasNext(); ) {
346                 nodes.add(i++, iterator.nextNode());
347             }
348         }
349         return principalNode;
350     }
351 
352     public Map<String, ACL> getACLs(final String principalName) {
353         return MgnlContext.doInSystemContext(new SilentSessionOp<Map<String,ACL>>(getRepositoryName()) {
354             @Override
355             public Map<String, ACL> doExec(Session session) throws Throwable {
356                 Node node = findPrincipalNode(principalName, session);
357                 if(node == null){
358                     return Collections.emptyMap();
359                 }
360                 return getACLs(node);
361             }});
362     }
363 
364     protected Map<String, ACL> getACLs(Node node) throws RepositoryException, ValueFormatException, PathNotFoundException {
365         Map<String, ACL> principalList = new HashMap<String, ACL>();
366         NodeIterator it = new FilteringNodeIterator(node.getNodes(), new NodeTypePredicate(NodeTypes.ContentNode.NAME, true));
367         while (it.hasNext()) {
368             Node aclEntry = it.nextNode();
369             if (!aclEntry.getName().startsWith("acl")) {
370                 continue;
371             }
372             String name = StringUtils.substringAfter(aclEntry.getName(), "acl_");
373 
374             List<Permission> permissionList = new ArrayList<Permission>();
375             // add acl
376             NodeIterator permissionIterator = new FilteringNodeIterator(aclEntry.getNodes(), new NodeTypePredicate(NodeTypes.ContentNode.NAME, true));
377             while (permissionIterator.hasNext()) {
378                 Node map = permissionIterator.nextNode();
379                 String path = map.getProperty("path").getString();
380                 UrlPattern p = new SimpleUrlPattern(path);
381                 Permission permission = new PermissionImpl();
382                 permission.setPattern(p);
383                 permission.setPermissions(map.getProperty("permissions").getLong());
384                 permissionList.add(permission);
385             }
386 
387             ACL acl;
388             // get the existing acl object if created before with some
389             // other role
390             if (principalList.containsKey(name)) {
391                 acl = principalList.get(name);
392                 permissionList.addAll(acl.getList());
393             }
394             acl = new ACLImpl(name, permissionList);
395             principalList.put(name, acl);
396 
397         }
398         return principalList;
399     }
400 
401     /**
402      * Find nodes located in the provided node or in sub-folders within it that have the provided nodeType.
403      * 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).
404      */
405     protected NodeIterator findPrincipalNodes(final Node node, final String nodeType) throws RepositoryException {
406         final StringBuilder builder = new StringBuilder("select * from [").append(nodeType).append("]");
407         if (!"/".equals(node.getPath())) {
408             builder.append(" where isdescendantnode(['").append(node.getPath()).append("'])");
409         }
410         final String queryString = builder.toString();
411         log.debug("Executing query \"{}\".", queryString);
412 
413         final Query query = node.getSession().getWorkspace().getQueryManager().createQuery(queryString, Query.JCR_SQL2);
414         return query.execute().getNodes();
415     }
416 
417     protected Collection<String> findUsersOrGroupsHavingAssignedGroupOrRoleWithUid(final Session session, final Node groupOrRoleNode, final String parentNodeName) throws RepositoryException {
418         if (groupOrRoleNode == null) {
419             return Collections.EMPTY_LIST;
420         }
421 
422         final StringBuilder builder = new StringBuilder("select * from [").append(NodeTypes.ContentNode.NAME).append("] as s");
423         builder.append(" where contains(s.*, '").append(groupOrRoleNode.getIdentifier()).append("')");
424         final String queryString = builder.toString();
425         log.debug("Executing query \"{}\".", queryString);
426 
427         final Query query = session.getWorkspace().getQueryManager().createQuery(queryString, Query.JCR_SQL2);
428         final NodeIterator nodeIterator = query.execute().getNodes();
429 
430         final Collection<String> matches = new ArrayList<String>();
431         Node current;
432         while (nodeIterator.hasNext()) {
433             current = nodeIterator.nextNode();
434             if (parentNodeName.equals(current.getName())) {
435                 matches.add(current.getParent().getName());
436             }
437         }
438         return matches;
439     }
440 
441 }