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  
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<String>();
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 e) {
119                             log.debug("{} has invalid value", property.getPath());
120                         } catch (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.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 e) {
252                         log.debug("{} has invalid value", nodeData.getPath());
253                     } catch (ValueFormatException e) {
254                         log.debug("{} has invalid value", nodeData.getPath());
255                     }
256                 }
257             }
258         } catch (RepositoryException e) {
259             log.error("failed to remove {} {} from [{}]", resourceTypeName, resourceName, principalName, e);
260         }
261     }
262 
263     protected abstract String getRepositoryName();
264 
265     protected abstract Node findPrincipalNode(String principalName, Session session) throws RepositoryException;
266 
267     protected Node findPrincipalNode(String principalName, Session session, String primaryNodeType) throws RepositoryException {
268         return findPrincipalNode(principalName, session, primaryNodeType, null);
269     }
270 
271     /**
272      * Find principal nodes of type {@link NodeTypes.User#NAME}, {@link NodeTypes.Group#NAME} or {@link NodeTypes.Role#NAME}.
273      *
274      * 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.
275      */
276     protected Node findPrincipalNode(String principalName, Session session, String primaryNodeType, Node startNode) throws RepositoryException {
277         final boolean isInstallationPhase = InstallStatus.inProgress.equals(Components.getComponent(InstallContextImpl.class).getStatus());
278         final long start = System.currentTimeMillis();
279 
280         final Node principalNode = isInstallationPhase ? findPrincipalNodeByTraversal(principalName, session, primaryNodeType, startNode) : findPrincipalNodeByQuery(principalName, session, primaryNodeType, startNode);
281         log.debug("Retrieving node took {}ms (isInstallationPhase: {}): path = {}", System.currentTimeMillis() - start, isInstallationPhase, principalNode == null ? "<null>" : principalNode.getPath());
282 
283         if (principalNode == null) {
284             log.debug("Could not find principal node '{}' of primary type '{}' under startnode '{}' in workspace '{}'.", principalName, primaryNodeType, startNode == null ? "/" : startNode.getPath(), session.getWorkspace().getName());
285         }
286         return principalNode;
287     }
288 
289     /**
290      * 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).
291      */
292     Node findPrincipalNodeByQuery(String principalName, Session session, String primaryNodeType, Node startNode) throws RepositoryException {
293         final Node root = startNode == null ? session.getRootNode() : startNode;
294 
295         final StringBuilder builder = new StringBuilder("select * from [").append(primaryNodeType).append("] where name() = '").append(principalName).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         final Query query = session.getWorkspace().getQueryManager().createQuery(queryString, Query.JCR_SQL2);
305         final NodeIterator iterator = query.execute().getNodes();
306         final Node user = iterator.hasNext() ? iterator.nextNode() : null;
307         if (iterator.hasNext()) {
308             log.error("Query found more than one node of type \"{}\" with name \"{}\" under the root path \"{}\".", primaryNodeType, principalName, root.getPath());
309         }
310         return user;
311     }
312 
313     /**
314      * 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).
315      */
316     Node findPrincipalNodeByTraversal(String principalName, Session session, String primaryNodeType, Node startNode) throws RepositoryException {
317         final Node root = startNode == null ? session.getRootNode() : startNode;
318         log.debug("Traversing to find nodes of type \"{}\" with name \"{}\" under the root path \"{}\".", primaryNodeType, principalName, root.getPath());
319 
320         final LinkedList<Node> nodes = new LinkedList<Node>();
321 
322         for (NodeIterator iterator = root.getNodes(); iterator.hasNext(); ) {
323             final Node node = iterator.nextNode();
324             if (!node.getName().startsWith(NodeTypes.JCR_PREFIX) && !node.getName().startsWith(NodeTypes.REP_PREFIX)) {
325                 nodes.add(node);
326             }
327         }
328 
329         Node principalNode = null;
330         while (!nodes.isEmpty()) {
331             final Node node = nodes.removeFirst();
332 
333             if (node.getName().equals(principalName) && node.getPrimaryNodeType().getName().equals(primaryNodeType)) {
334                 if (principalNode != null) {
335                     log.error("Traversal found more than one node of type \"{}\" with name \"{}\" under the root path \"{}\".", primaryNodeType, principalName, root.getPath());
336                     break;
337                 }
338                 principalNode = node;
339             }
340 
341             int i = 0;
342             for (NodeIterator iterator = node.getNodes(); iterator.hasNext(); ) {
343                 nodes.add(i++, iterator.nextNode());
344             }
345         }
346         return principalNode;
347     }
348 
349     public Map<String, ACL> getACLs(final String principalName) {
350         return MgnlContext.doInSystemContext(new SilentSessionOp<Map<String, ACL>>(getRepositoryName()) {
351             @Override
352             public Map<String, ACL> doExec(Session session) throws Throwable {
353                 Node node = findPrincipalNode(principalName, session);
354                 if (node == null) {
355                     return Collections.emptyMap();
356                 }
357                 return getACLs(node);
358             }
359         });
360     }
361 
362     protected Map<String, ACL> getACLs(Node node) throws RepositoryException, ValueFormatException, PathNotFoundException {
363         Map<String, ACL> principalList = new HashMap<String, ACL>();
364         NodeIterator it = new FilteringNodeIterator(node.getNodes(), new NodeTypePredicate(NodeTypes.ContentNode.NAME, true));
365         while (it.hasNext()) {
366             Node aclEntry = it.nextNode();
367             if (!aclEntry.getName().startsWith("acl")) {
368                 continue;
369             }
370             String name = StringUtils.substringAfter(aclEntry.getName(), "acl_");
371 
372             List<Permission> permissionList = new ArrayList<Permission>();
373             // add acl
374             NodeIterator permissionIterator = new FilteringNodeIterator(aclEntry.getNodes(), new NodeTypePredicate(NodeTypes.ContentNode.NAME, true));
375             while (permissionIterator.hasNext()) {
376                 Node map = permissionIterator.nextNode();
377                 String path = map.getProperty("path").getString();
378                 UrlPattern p = new SimpleUrlPattern(path);
379                 Permission permission = new PermissionImpl();
380                 permission.setPattern(p);
381                 permission.setPermissions(map.getProperty("permissions").getLong());
382                 permissionList.add(permission);
383             }
384 
385             ACL acl;
386             // get the existing acl object if created before with some
387             // other role
388             if (principalList.containsKey(name)) {
389                 acl = principalList.get(name);
390                 permissionList.addAll(acl.getList());
391             }
392             acl = new ACLImpl(name, permissionList);
393             principalList.put(name, acl);
394 
395         }
396         return principalList;
397     }
398 
399     /**
400      * Find nodes located in the provided node or in sub-folders within it that have the provided nodeType.
401      * 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).
402      */
403     protected NodeIterator findPrincipalNodes(final Node node, final String nodeType) throws RepositoryException {
404         final StringBuilder builder = new StringBuilder("select * from [").append(nodeType).append("]");
405         if (!"/".equals(node.getPath())) {
406             builder.append(" where isdescendantnode(['").append(node.getPath()).append("'])");
407         }
408         final String queryString = builder.toString();
409         log.debug("Executing query \"{}\".", queryString);
410 
411         final Query query = node.getSession().getWorkspace().getQueryManager().createQuery(queryString, Query.JCR_SQL2);
412         return query.execute().getNodes();
413     }
414 
415     protected Collection<String> findUsersOrGroupsHavingAssignedGroupOrRoleWithUid(final Session session, final Node groupOrRoleNode, final String parentNodeName) throws RepositoryException {
416         if (groupOrRoleNode == null) {
417             return Collections.EMPTY_LIST;
418         }
419 
420         final StringBuilder builder = new StringBuilder("select * from [").append(NodeTypes.ContentNode.NAME).append("] as s");
421         builder.append(" where contains(s.*, '").append(groupOrRoleNode.getIdentifier()).append("')");
422         final String queryString = builder.toString();
423         log.debug("Executing query \"{}\".", queryString);
424 
425         final Query query = session.getWorkspace().getQueryManager().createQuery(queryString, Query.JCR_SQL2);
426         final NodeIterator nodeIterator = query.execute().getNodes();
427 
428         final Collection<String> matches = new ArrayList<String>();
429         Node current;
430         while (nodeIterator.hasNext()) {
431             current = nodeIterator.nextNode();
432             if (parentNodeName.equals(current.getName())) {
433                 matches.add(current.getParent().getName());
434             }
435         }
436         return matches;
437     }
438 
439 }