View Javadoc

1   /* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
2    *
3    * Licensed under the Apache License, Version 2.0 (the "License");
4    * you may not use this file except in compliance with the License.
5    * You may obtain a copy of the License at
6    *
7    *     http://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
14   */
15  
16  package org.acegisecurity.vote;
17  
18  import org.acegisecurity.Authentication;
19  import org.acegisecurity.AuthorizationServiceException;
20  import org.acegisecurity.ConfigAttribute;
21  import org.acegisecurity.ConfigAttributeDefinition;
22  
23  import org.acegisecurity.acl.AclEntry;
24  import org.acegisecurity.acl.AclManager;
25  import org.acegisecurity.acl.basic.BasicAclEntry;
26  import org.acegisecurity.acl.basic.SimpleAclEntry;
27  
28  import org.apache.commons.logging.Log;
29  import org.apache.commons.logging.LogFactory;
30  
31  import org.springframework.beans.factory.InitializingBean;
32  
33  import org.springframework.util.Assert;
34  
35  import java.lang.reflect.InvocationTargetException;
36  import java.lang.reflect.Method;
37  
38  import java.util.Iterator;
39  
40  
41  /**
42   * <p>Given a domain object instance passed as a method argument, ensures the principal has appropriate permission
43   * as defined by the {@link AclManager}.</p>
44   *  <p>The <code>AclManager</code> is used to retrieve the access control list (ACL) permissions associated with a
45   * domain object instance for the current <code>Authentication</code> object. This class is designed to process {@link
46   * AclEntry}s that are subclasses of {@link org.acegisecurity.acl.basic.BasicAclEntry} only. Generally these are
47   * obtained by using the {@link org.acegisecurity.acl.basic.BasicAclProvider}.</p>
48   *  <p>The voter will vote if any  {@link ConfigAttribute#getAttribute()} matches the {@link
49   * #processConfigAttribute}. The provider will then locate the first method argument of type {@link
50   * #processDomainObjectClass}. Assuming that method argument is non-null, the provider will then lookup the ACLs from
51   * the <code>AclManager</code> and ensure the principal is {@link
52   * org.acegisecurity.acl.basic.BasicAclEntry#isPermitted(int)} for at least one of the {@link #requirePermission}s.</p>
53   *  <p>If the method argument is <code>null</code>, the voter will abstain from voting. If the method argument
54   * could not be found, an {@link org.acegisecurity.AuthorizationServiceException} will be thrown.</p>
55   *  <p>In practical terms users will typically setup a number of <code>BasicAclEntryVoter</code>s. Each will have a
56   * different {@link #processDomainObjectClass}, {@link #processConfigAttribute} and {@link #requirePermission}
57   * combination. For example, a small application might employ the following instances of
58   * <code>BasicAclEntryVoter</code>:
59   *  <ul>
60   *      <li>Process domain object class <code>BankAccount</code>, configuration attribute
61   *      <code>VOTE_ACL_BANK_ACCONT_READ</code>, require permission <code>SimpleAclEntry.READ</code></li>
62   *      <li>Process domain object class <code>BankAccount</code>, configuration attribute
63   *      <code>VOTE_ACL_BANK_ACCOUNT_WRITE</code>, require permission list <code>SimpleAclEntry.WRITE</code> and
64   *      <code>SimpleAclEntry.CREATE</code> (allowing the principal to have <b>either</b> of these two permissions</li>
65   *      <li>Process domain object class <code>Customer</code>, configuration attribute
66   *      <code>VOTE_ACL_CUSTOMER_READ</code>, require permission <code>SimpleAclEntry.READ</code></li>
67   *      <li>Process domain object class <code>Customer</code>, configuration attribute
68   *      <code>VOTE_ACL_CUSTOMER_WRITE</code>, require permission list <code>SimpleAclEntry.WRITE</code> and
69   *      <code>SimpleAclEntry.CREATE</code></li>
70   *  </ul>
71   *  Alternatively, you could have used a common superclass or interface for the {@link #processDomainObjectClass}
72   * if both <code>BankAccount</code> and <code>Customer</code> had common parents.</p>
73   *  <p>If the principal does not have sufficient permissions, the voter will vote to deny access.</p>
74   *  <p>The <code>AclManager</code> is allowed to return any implementations of <code>AclEntry</code> it wishes.
75   * However, this provider will only be able to validate against <code>AbstractBasicAclEntry</code>s, and thus a vote
76   * to deny access will be made if no <code>AclEntry</code> is of type <code>AbstractBasicAclEntry</code>.</p>
77   *  <p>All comparisons and prefixes are case sensitive.</p>
78   *
79   * @author Ben Alex
80   * @version $Id: BasicAclEntryVoter.java 1766 2006-11-26 04:47:43Z benalex $
81   */
82  public class BasicAclEntryVoter extends AbstractAclVoter implements InitializingBean {
83      //~ Static fields/initializers =====================================================================================
84  
85      private static final Log logger = LogFactory.getLog(BasicAclEntryVoter.class);
86  
87      //~ Instance fields ================================================================================================
88  
89      private AclManager aclManager;
90      private String internalMethod;
91      private String processConfigAttribute;
92      private int[] requirePermission;
93  
94      //~ Methods ========================================================================================================
95  
96      public void afterPropertiesSet() throws Exception {
97          Assert.notNull(processConfigAttribute, "A processConfigAttribute is mandatory");
98          Assert.notNull(aclManager, "An aclManager is mandatory");
99  
100         if ((requirePermission == null) || (requirePermission.length == 0)) {
101             throw new IllegalArgumentException("One or more requirePermission entries is mandatory");
102         }
103     }
104 
105     public AclManager getAclManager() {
106         return aclManager;
107     }
108 
109     /**
110      * Optionally specifies a method of the domain object that will be used to obtain a contained domain
111      * object. That contained domain object will be used for the ACL evaluation. This is useful if a domain object
112      * contains a parent that an ACL evaluation should be targeted for, instead of the child domain object (which
113      * perhaps is being created and as such does not yet have any ACL permissions)
114      *
115      * @return <code>null</code> to use the domain object, or the name of a method (that requires no arguments) that
116      *         should be invoked to obtain an <code>Object</code> which will be the domain object used for ACL
117      *         evaluation
118      */
119     public String getInternalMethod() {
120         return internalMethod;
121     }
122 
123     public String getProcessConfigAttribute() {
124         return processConfigAttribute;
125     }
126 
127     public int[] getRequirePermission() {
128         return requirePermission;
129     }
130 
131     public void setAclManager(AclManager aclManager) {
132         this.aclManager = aclManager;
133     }
134 
135     public void setInternalMethod(String internalMethod) {
136         this.internalMethod = internalMethod;
137     }
138 
139     public void setProcessConfigAttribute(String processConfigAttribute) {
140         this.processConfigAttribute = processConfigAttribute;
141     }
142 
143     public void setRequirePermission(int[] requirePermission) {
144         this.requirePermission = requirePermission;
145     }
146 
147     /**
148      * Allow setting permissions with String literals instead of integers as {@link #setRequirePermission(int[])}
149      * 
150      * @param requirePermission Permission literals
151      * @see SimpleAclEntry#parsePermissions(String[]) for valid values
152      */
153     public void setRequirePermissionFromString(String[] requirePermission) {
154         setRequirePermission(SimpleAclEntry.parsePermissions(requirePermission));
155     }
156 
157     public boolean supports(ConfigAttribute attribute) {
158         if ((attribute.getAttribute() != null) && attribute.getAttribute().equals(getProcessConfigAttribute())) {
159             return true;
160         } else {
161             return false;
162         }
163     }
164 
165     public int vote(Authentication authentication, Object object, ConfigAttributeDefinition config) {
166         Iterator iter = config.getConfigAttributes();
167 
168         while (iter.hasNext()) {
169             ConfigAttribute attr = (ConfigAttribute) iter.next();
170 
171             if (this.supports(attr)) {
172                 // Need to make an access decision on this invocation
173                 // Attempt to locate the domain object instance to process
174                 Object domainObject = getDomainObjectInstance(object);
175 
176                 // If domain object is null, vote to abstain
177                 if (domainObject == null) {
178                     if (logger.isDebugEnabled()) {
179                         logger.debug("Voting to abstain - domainObject is null");
180                     }
181 
182                     return AccessDecisionVoter.ACCESS_ABSTAIN;
183                 }
184 
185                 // Evaluate if we are required to use an inner domain object
186                 if ((internalMethod != null) && !"".equals(internalMethod)) {
187                     try {
188                         Class clazz = domainObject.getClass();
189                         Method method = clazz.getMethod(internalMethod, new Class[] {});
190                         domainObject = method.invoke(domainObject, new Object[] {});
191                     } catch (NoSuchMethodException nsme) {
192                         throw new AuthorizationServiceException("Object of class '" + domainObject.getClass()
193                             + "' does not provide the requested internalMethod: " + internalMethod);
194                     } catch (IllegalAccessException iae) {
195                         if (logger.isDebugEnabled()) {
196                             logger.debug("IllegalAccessException", iae);
197 
198                             if (iae.getCause() != null) {
199                                 logger.debug("Cause: " + iae.getCause().getMessage(), iae.getCause());
200                             }
201                         }
202 
203                         throw new AuthorizationServiceException("Problem invoking internalMethod: " + internalMethod
204                             + " for object: " + domainObject);
205                     } catch (InvocationTargetException ite) {
206                         if (logger.isDebugEnabled()) {
207                             logger.debug("InvocationTargetException", ite);
208 
209                             if (ite.getCause() != null) {
210                                 logger.debug("Cause: " + ite.getCause().getMessage(), ite.getCause());
211                             }
212                         }
213 
214                         throw new AuthorizationServiceException("Problem invoking internalMethod: " + internalMethod
215                             + " for object: " + domainObject);
216                     }
217                 }
218 
219                 // Obtain the ACLs applicable to the domain object
220                 AclEntry[] acls = aclManager.getAcls(domainObject, authentication);
221 
222                 // If principal has no permissions for domain object, deny
223                 if ((acls == null) || (acls.length == 0)) {
224                     if (logger.isDebugEnabled()) {
225                         logger.debug("Voting to deny access - no ACLs returned for this principal");
226                     }
227 
228                     return AccessDecisionVoter.ACCESS_DENIED;
229                 }
230 
231                 // Principal has some permissions for domain object, check them
232                 for (int i = 0; i < acls.length; i++) {
233                     // Locate processable AclEntrys
234                     if (acls[i] instanceof BasicAclEntry) {
235                         BasicAclEntry processableAcl = (BasicAclEntry) acls[i];
236 
237                         // See if principal has any of the required permissions
238                         for (int y = 0; y < requirePermission.length; y++) {
239                             if (processableAcl.isPermitted(requirePermission[y])) {
240                                 if (logger.isDebugEnabled()) {
241                                     logger.debug("Voting to grant access");
242                                 }
243 
244                                 return AccessDecisionVoter.ACCESS_GRANTED;
245                             }
246                         }
247                     }
248                 }
249 
250                 // No permissions match
251                 if (logger.isDebugEnabled()) {
252                     logger.debug(
253                         "Voting to deny access - ACLs returned, but insufficient permissions for this principal");
254                 }
255 
256                 return AccessDecisionVoter.ACCESS_DENIED;
257             }
258         }
259 
260         // No configuration attribute matched, so abstain
261         return AccessDecisionVoter.ACCESS_ABSTAIN;
262     }
263 }