View Javadoc

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