View Javadoc

1   /**
2    * This file Copyright (c) 2011-2014 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.ItemType;
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.repository.RepositoryConstants;
45  
46  import java.util.ArrayList;
47  import java.util.Collection;
48  import java.util.Collections;
49  import java.util.HashMap;
50  import java.util.LinkedList;
51  import java.util.List;
52  import java.util.Map;
53  
54  import javax.jcr.ItemNotFoundException;
55  import javax.jcr.Node;
56  import javax.jcr.NodeIterator;
57  import javax.jcr.PathNotFoundException;
58  import javax.jcr.Property;
59  import javax.jcr.PropertyIterator;
60  import javax.jcr.RepositoryException;
61  import javax.jcr.Session;
62  import javax.jcr.ValueFormatException;
63  
64  import org.apache.commons.lang.StringUtils;
65  import org.apache.jackrabbit.commons.iterator.FilteringNodeIterator;
66  import org.apache.jackrabbit.commons.predicate.NodeTypePredicate;
67  import org.slf4j.Logger;
68  import org.slf4j.LoggerFactory;
69  
70  /**
71   * Common parent class for repo based security managers.
72   */
73  public abstract class RepositoryBackedSecurityManager {
74  
75      private static final Logger log = LoggerFactory.getLogger(RepositoryBackedSecurityManager.class);
76  
77      public boolean hasAny(final String principalName, final String resourceName, final String resourceTypeName) {
78          long start = System.currentTimeMillis();
79          try {
80              String sessionName;
81              if (StringUtils.equalsIgnoreCase(resourceTypeName, NODE_ROLES)) {
82                  sessionName = RepositoryConstants.USER_ROLES;
83              } else {
84                  sessionName = RepositoryConstants.USER_GROUPS;
85              }
86  
87              // this is an original code from old ***Managers.
88              // 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.
89              final Collection<String> groupsOrRoles = MgnlContext.doInSystemContext(new JCRSessionOp<Collection<String>>(getRepositoryName()) {
90  
91                  @Override
92                  public Collection<String> exec(Session session) throws RepositoryException {
93                      List<String> list = new ArrayList<String>();
94                      Node principal = findPrincipalNode(principalName, session);
95                      if(principal == null) {
96                          log.debug("No User '"+principalName+"' found in repository");
97                          return list;
98                      }
99                      Node groupsOrRoles = principal.getNode(resourceTypeName);
100 
101                     for (PropertyIterator props = groupsOrRoles.getProperties(); props.hasNext();) {
102                         Property property = props.nextProperty();
103                         try {
104                             // just get all the IDs of given type assigned to the principal
105                             list.add(property.getString());
106                         } catch (ItemNotFoundException e) {
107                             log.debug("Role [{}] does not exist in the {} repository", resourceName, resourceTypeName);
108                         } catch (IllegalArgumentException e) {
109                             log.debug("{} has invalid value", property.getPath());
110                         } catch (ValueFormatException e) {
111                             log.debug("{} has invalid value", property.getPath());
112                         }
113                     }
114                     return list;
115                 }
116             });
117 
118 
119             // check if any of the assigned IDs match the requested name
120             return MgnlContext.doInSystemContext(new JCRSessionOp<Boolean>(sessionName) {
121 
122                 @Override
123                 public Boolean exec(Session session) throws RepositoryException {
124                     for (String groupOrRole : groupsOrRoles) {
125                         // check for the existence of this ID
126                         try {
127                             if (session.getNodeByIdentifier(groupOrRole).getName().equalsIgnoreCase(resourceName)) {
128                                 return true;
129                             }
130                         } catch (RepositoryException e) {
131                             log.debug("Role [{}] does not exist in the ROLES repository", resourceName);
132                         }
133                     }
134                     return false;
135                 }});
136 
137         } catch (RepositoryException e) {
138             // Item not found or access denied ...
139             log.debug(e.getMessage(), e);
140         } finally {
141             log.debug("checked {} for {} in {}ms.", new Object[] {resourceName, resourceTypeName, (System.currentTimeMillis() - start)});
142         }
143         return false;
144     }
145 
146     /**
147      * Adds link to a resource (group or role) to the principal (user or group).
148      * This call is lenient and will not throw exception in case principal doesn't exist! Instead it will simply return without making any change.
149      * @param principalName name of the user or group to be updated
150      * @param resourceName name of the group or role to be added
151      * @param resourceTypeName type of the added resource (group or role) {@link #NODE_ROLES}, {@link #NODE_GROUPS}
152      */
153     protected void add(final String principalName, final String resourceName, final String resourceTypeName) throws PrincipalNotFoundException {
154         try {
155             final String nodeID = getLinkedResourceId(resourceName, resourceTypeName);
156 
157             if (!hasAny(principalName, resourceName, resourceTypeName)) {
158                 Session session = MgnlContext.getJCRSession(getRepositoryName());
159                 Node principalNode = findPrincipalNode(principalName, session);
160                 if (principalNode == null) {
161                     throw new PrincipalNotFoundException("Principal " + principalName + " of type " + resourceTypeName + " was not found.");
162                 }
163                 if (!principalNode.hasNode(resourceTypeName)) {
164                     principalNode.addNode(resourceTypeName, ItemType.CONTENTNODE.getSystemName());
165                 }
166                 Node node = principalNode.getNode(resourceTypeName);
167                 // add corresponding ID
168                 // used only to get the unique label
169                 String newName = Path.getUniqueLabel(session, node.getPath(), "0");
170                 node.setProperty(newName, nodeID);
171                 session.save();
172             }
173         }
174         catch (RepositoryException e) {
175             log.error("failed to add " + resourceTypeName + " "+ resourceName + " to  [" + principalName + "]", e);
176         }
177     }
178 
179     private String getLinkedResourceId(final String resourceName, final String resourceTypeName) throws AccessDeniedException {
180         final String nodeID;
181         if (StringUtils.equalsIgnoreCase(resourceTypeName, NODE_ROLES)) {
182             Role role = SecuritySupport.Factory.getInstance().getRoleManager().getRole(resourceName);
183             if (role == null) {
184                 log.warn("Invalid role requested: {}", resourceName);
185                 nodeID = null;
186             }
187             else {
188                 nodeID = role.getId();
189             }
190         }
191         else {
192             Group group = SecuritySupport.Factory.getInstance().getGroupManager().getGroup(resourceName);
193             if (group == null) {
194                 log.warn("Invalid group requested: {}", resourceName);
195                 nodeID = null;
196             }
197             else {
198                 nodeID = group.getId();
199             }
200         }
201         return nodeID;
202     }
203 
204     protected String getResourceName(final String resourceId) {
205         try {
206             return MgnlContext.getJCRSession(getRepositoryName()).getNodeByIdentifier(resourceId).getName();
207         } catch (ItemNotFoundException e) {
208             // referenced node doesn't exist
209             return null;
210         }
211         catch (RepositoryException e) {
212             log.error(e.getMessage(), e);
213         }
214         return null;
215     }
216 
217     /**
218      * This call is lenient and will not throw exception in case principal doesn't exist! Instead it will simply return without making any change.
219      *
220      * @param principalName name of the user or group to be updated
221      * @param resourceName name of the group or role to be added
222      * @param resourceTypeName type of the added resource (group or role) {@link #NODE_ROLES}, {@link #NODE_GROUPS}
223      */
224     protected void remove(final String principalName, final String resourceName, final String resourceTypeName) throws PrincipalNotFoundException {
225         try {
226             final String nodeID = getLinkedResourceId(resourceName, resourceTypeName);
227 
228             if (hasAny(principalName, resourceName, resourceTypeName)) {
229                 Session session = MgnlContext.getJCRSession(getRepositoryName());
230                 Node principalNode = findPrincipalNode(principalName, session);
231                 if (!principalNode.hasNode(resourceTypeName)) {
232                     throw new PrincipalNotFoundException("Principal " + principalName + " of type " + resourceTypeName + " was not found.");
233                 }
234                 Node node = principalNode.getNode(resourceTypeName);
235                 for (PropertyIterator iter = node.getProperties(); iter.hasNext();) {
236                     Property nodeData = iter.nextProperty();
237                     // check for the existence of this ID
238                     try {
239                         if (nodeData.getString().equals(nodeID)) {
240                             nodeData.remove();
241                             session.save();
242                             // do not break here ... if resource was ever added multiple times remove all occurrences
243                         }
244                     } catch (IllegalArgumentException e) {
245                         log.debug("{} has invalid value", nodeData.getPath());
246                     } catch (ValueFormatException e) {
247                         log.debug("{} has invalid value", nodeData.getPath());
248                     }
249                 }
250             }
251         }
252         catch (RepositoryException e) {
253             log.error("failed to remove " + resourceTypeName + " "+ resourceName + " from [" + principalName + "]", e);
254         }
255     }
256 
257     protected abstract String getRepositoryName();
258 
259     protected abstract Node findPrincipalNode(String principalName, Session session) throws RepositoryException;
260 
261     protected Node findPrincipalNode(String principalName, Session session, String primaryNodeType) throws RepositoryException {
262         return findPrincipalNode(principalName, session, primaryNodeType, null);
263     }
264 
265     /**
266      * Helper method to find principal node of type {@link NodeTypes.User#NAME}, {@link NodeTypes.Group#NAME} or
267      * {@link NodeTypes.Role#NAME}.
268      *
269      * Not using JCR queries as they require an updated index, which might not been given, i.e. during
270      * installation of a module (bootstrapped resources).
271      */
272     protected Node findPrincipalNode(String principalName, Session session, String primaryNodeType, Node startNode) throws RepositoryException {
273         LinkedList<Node> nodes = new LinkedList<Node>();
274 
275         final Node rootNode = startNode == null ? session.getRootNode() : startNode;
276 
277         for (NodeIterator iterator = rootNode.getNodes(); iterator.hasNext(); ) {
278             final Node node = iterator.nextNode();
279             if (!node.getName().startsWith(NodeTypes.JCR_PREFIX) && !node.getName().startsWith(NodeTypes.REP_PREFIX)) {
280                 nodes.add(node);
281             }
282         }
283 
284         Node principalNode = null;
285         while (!nodes.isEmpty()) {
286             final Node node = nodes.removeFirst();
287 
288             if (node.getName().equals(principalName) && node.getPrimaryNodeType().getName().equals(primaryNodeType)) {
289                 if (principalNode != null) {
290                     log.error("More than one principal node found of type \"{}\" with name \"{}\"", primaryNodeType, principalName);
291                     break;
292                 }
293                 principalNode = node;
294             }
295 
296             int i = 0;
297             for (NodeIterator iterator = node.getNodes(); iterator.hasNext(); ) {
298                 nodes.add(i++, iterator.nextNode());
299             }
300         }
301         return principalNode;
302     }
303 
304     public Map<String, ACL> getACLs(final String principalName) {
305         return MgnlContext.doInSystemContext(new SilentSessionOp<Map<String,ACL>>(getRepositoryName()) {
306             @Override
307             public Map<String, ACL> doExec(Session session) throws Throwable {
308                 Node node = findPrincipalNode(principalName, session);
309                 if(node == null){
310                     return Collections.emptyMap();
311                 }
312                 return getACLs(node);
313             }});
314     }
315 
316     protected Map<String, ACL> getACLs(Node node) throws RepositoryException, ValueFormatException, PathNotFoundException {
317         Map<String, ACL> principalList = new HashMap<String, ACL>();
318         NodeIterator it = new FilteringNodeIterator(node.getNodes(), new NodeTypePredicate(ItemType.CONTENTNODE.getSystemName(), true));
319         while (it.hasNext()) {
320             Node aclEntry = it.nextNode();
321             if (!aclEntry.getName().startsWith("acl")) {
322                 continue;
323             }
324             String name = StringUtils.substringAfter(aclEntry.getName(), "acl_");
325 
326             List<Permission> permissionList = new ArrayList<Permission>();
327             // add acl
328             NodeIterator permissionIterator = new FilteringNodeIterator(aclEntry.getNodes(), new NodeTypePredicate(ItemType.CONTENTNODE.getSystemName(), true));
329             while (permissionIterator.hasNext()) {
330                 Node map = permissionIterator.nextNode();
331                 String path = map.getProperty("path").getString();
332                 UrlPattern p = new SimpleUrlPattern(path);
333                 Permission permission = new PermissionImpl();
334                 permission.setPattern(p);
335                 permission.setPermissions(map.getProperty("permissions").getLong());
336                 permissionList.add(permission);
337             }
338 
339             ACL acl;
340             // get the existing acl object if created before with some
341             // other role
342             if (principalList.containsKey(name)) {
343                 acl = principalList.get(name);
344                 permissionList.addAll(acl.getList());
345             }
346             acl = new ACLImpl(name, permissionList);
347             principalList.put(name, acl);
348 
349         }
350         return principalList;
351     }
352 
353 }