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  package org.acegisecurity.vote;
16  
17  import java.lang.reflect.InvocationTargetException;
18  import java.lang.reflect.Method;
19  import java.util.Iterator;
20  
21  import org.acegisecurity.Authentication;
22  import org.acegisecurity.AuthorizationServiceException;
23  import org.acegisecurity.ConfigAttribute;
24  import org.acegisecurity.ConfigAttributeDefinition;
25  import org.acegisecurity.acls.Acl;
26  import org.acegisecurity.acls.AclService;
27  import org.acegisecurity.acls.NotFoundException;
28  import org.acegisecurity.acls.Permission;
29  import org.acegisecurity.acls.objectidentity.ObjectIdentity;
30  import org.acegisecurity.acls.objectidentity.ObjectIdentityRetrievalStrategy;
31  import org.acegisecurity.acls.objectidentity.ObjectIdentityRetrievalStrategyImpl;
32  import org.acegisecurity.acls.sid.Sid;
33  import org.acegisecurity.acls.sid.SidRetrievalStrategy;
34  import org.acegisecurity.acls.sid.SidRetrievalStrategyImpl;
35  import org.apache.commons.logging.Log;
36  import org.apache.commons.logging.LogFactory;
37  import org.springframework.util.Assert;
38  
39  
40  /**
41   * <p>Given a domain object instance passed as a method argument, ensures the principal has appropriate permission
42   * as indicated by the {@link AclService}.</p>
43   *  <p>The <code>AclService</code> is used to retrieve the access control list (ACL) permissions associated with a
44   * domain object instance for the current <code>Authentication</code> object.</p>
45   *  <p>The voter will vote if any  {@link ConfigAttribute#getAttribute()} matches the {@link
46   * #processConfigAttribute}. The provider will then locate the first method argument of type {@link
47   * #processDomainObjectClass}. Assuming that method argument is non-null, the provider will then lookup the ACLs from
48   * the <code>AclManager</code> and ensure the principal is  {@link Acl#isGranted(org.acegisecurity.acls.Permission[],
49   * org.acegisecurity.acls.sid.Sid[], boolean)}  when presenting the {@link #requirePermission} array to that method.</p>
50   *  <p>If the method argument is <code>null</code>, the voter will abstain from voting. If the method argument
51   * could not be found, an {@link org.acegisecurity.AuthorizationServiceException} will be thrown.</p>
52   *  <p>In practical terms users will typically setup a number of <code>AclEntryVoter</code>s. Each will have a
53   * different {@link #processDomainObjectClass}, {@link #processConfigAttribute} and {@link #requirePermission}
54   * combination. For example, a small application might employ the following instances of <code>AclEntryVoter</code>:
55   *  <ul>
56   *      <li>Process domain object class <code>BankAccount</code>, configuration attribute
57   *      <code>VOTE_ACL_BANK_ACCONT_READ</code>, require permission <code>BasePermission.READ</code></li>
58   *      <li>Process domain object class <code>BankAccount</code>, configuration attribute
59   *      <code>VOTE_ACL_BANK_ACCOUNT_WRITE</code>, require permission list <code>BasePermission.WRITE</code> and
60   *      <code>BasePermission.CREATE</code> (allowing the principal to have <b>either</b> of these two permissions</li>
61   *      <li>Process domain object class <code>Customer</code>, configuration attribute
62   *      <code>VOTE_ACL_CUSTOMER_READ</code>, require permission <code>BasePermission.READ</code></li>
63   *      <li>Process domain object class <code>Customer</code>, configuration attribute
64   *      <code>VOTE_ACL_CUSTOMER_WRITE</code>, require permission list <code>BasePermission.WRITE</code> and
65   *      <code>BasePermission.CREATE</code></li>
66   *  </ul>
67   *  Alternatively, you could have used a common superclass or interface for the {@link #processDomainObjectClass}
68   * if both <code>BankAccount</code> and <code>Customer</code> had common parents.</p>
69   *  <p>If the principal does not have sufficient permissions, the voter will vote to deny access.</p>
70   *  <p>All comparisons and prefixes are case sensitive.</p>
71   *
72   * @author Ben Alex
73   * @version $Id: AclEntryVoter.java 1784 2007-02-24 21:00:24Z luke_t $
74   */
75  public class AclEntryVoter extends AbstractAclVoter {
76      //~ Static fields/initializers =====================================================================================
77  
78      private static final Log logger = LogFactory.getLog(AclEntryVoter.class);
79  
80      //~ Instance fields ================================================================================================
81  
82      private AclService aclService;
83      private ObjectIdentityRetrievalStrategy objectIdentityRetrievalStrategy = new ObjectIdentityRetrievalStrategyImpl();
84      private SidRetrievalStrategy sidRetrievalStrategy = new SidRetrievalStrategyImpl();
85      private String internalMethod;
86      private String processConfigAttribute;
87      private Permission[] requirePermission;
88  
89      //~ Constructors ===================================================================================================
90  
91      public AclEntryVoter(AclService aclService, String processConfigAttribute, Permission[] requirePermission) {
92          Assert.notNull(processConfigAttribute, "A processConfigAttribute is mandatory");
93          Assert.notNull(aclService, "An AclService is mandatory");
94  
95          if ((requirePermission == null) || (requirePermission.length == 0)) {
96              throw new IllegalArgumentException("One or more requirePermission entries is mandatory");
97          }
98  
99          this.aclService = aclService;
100         this.processConfigAttribute = processConfigAttribute;
101         this.requirePermission = requirePermission;
102     }
103 
104     //~ Methods ========================================================================================================
105 
106     /**
107      * Optionally specifies a method of the domain object that will be used to obtain a contained domain
108      * object. That contained domain object will be used for the ACL evaluation. This is useful if a domain object
109      * contains a parent that an ACL evaluation should be targeted for, instead of the child domain object (which
110      * perhaps is being created and as such does not yet have any ACL permissions)
111      *
112      * @return <code>null</code> to use the domain object, or the name of a method (that requires no arguments) that
113      *         should be invoked to obtain an <code>Object</code> which will be the domain object used for ACL
114      *         evaluation
115      */
116     public String getInternalMethod() {
117         return internalMethod;
118     }
119 
120     public String getProcessConfigAttribute() {
121         return processConfigAttribute;
122     }
123 
124     public void setInternalMethod(String internalMethod) {
125         this.internalMethod = internalMethod;
126     }
127 
128     public void setObjectIdentityRetrievalStrategy(ObjectIdentityRetrievalStrategy objectIdentityRetrievalStrategy) {
129         Assert.notNull(objectIdentityRetrievalStrategy, "ObjectIdentityRetrievalStrategy required");
130         this.objectIdentityRetrievalStrategy = objectIdentityRetrievalStrategy;
131     }
132 
133     public void setSidRetrievalStrategy(SidRetrievalStrategy sidRetrievalStrategy) {
134         Assert.notNull(sidRetrievalStrategy, "SidRetrievalStrategy required");
135         this.sidRetrievalStrategy = sidRetrievalStrategy;
136     }
137 
138     public boolean supports(ConfigAttribute attribute) {
139         if ((attribute.getAttribute() != null) && attribute.getAttribute().equals(getProcessConfigAttribute())) {
140             return true;
141         } else {
142             return false;
143         }
144     }
145 
146     public int vote(Authentication authentication, Object object, ConfigAttributeDefinition config) {
147         Iterator iter = config.getConfigAttributes();
148 
149         while (iter.hasNext()) {
150             ConfigAttribute attr = (ConfigAttribute) iter.next();
151 
152             if (this.supports(attr)) {
153                 // Need to make an access decision on this invocation
154                 // Attempt to locate the domain object instance to process
155                 Object domainObject = getDomainObjectInstance(object);
156 
157                 // Evaluate if we are required to use an inner domain object
158                 if (domainObject != null && internalMethod != null && (!"".equals(internalMethod))) {
159                     try {
160                         Class clazz = domainObject.getClass();
161                         Method method = clazz.getMethod(internalMethod, new Class[] {});
162                         domainObject = method.invoke(domainObject, new Object[] {});
163                     } catch (NoSuchMethodException nsme) {
164                         throw new AuthorizationServiceException("Object of class '" + domainObject.getClass()
165                             + "' does not provide the requested internalMethod: " + internalMethod);
166                     } catch (IllegalAccessException iae) {
167                         if (logger.isDebugEnabled()) {
168                             logger.debug("IllegalAccessException", iae);
169 
170                             if (iae.getCause() != null) {
171                                 logger.debug("Cause: " + iae.getCause().getMessage(), iae.getCause());
172                             }
173                         }
174 
175                         throw new AuthorizationServiceException("Problem invoking internalMethod: " + internalMethod
176                             + " for object: " + domainObject);
177                     } catch (InvocationTargetException ite) {
178                         if (logger.isDebugEnabled()) {
179                             logger.debug("InvocationTargetException", ite);
180 
181                             if (ite.getCause() != null) {
182                                 logger.debug("Cause: " + ite.getCause().getMessage(), ite.getCause());
183                             }
184                         }
185 
186                         throw new AuthorizationServiceException("Problem invoking internalMethod: " + internalMethod
187                             + " for object: " + domainObject);
188                     }
189                 }
190 
191                 // If domain object is null, vote to abstain
192                 if (domainObject == null) {
193                     if (logger.isDebugEnabled()) {
194                         logger.debug("Voting to abstain - domainObject is null");
195                     }
196 
197                     return AccessDecisionVoter.ACCESS_ABSTAIN;
198                 }
199 
200                 // Obtain the OID applicable to the domain object
201                 ObjectIdentity objectIdentity = objectIdentityRetrievalStrategy.getObjectIdentity(domainObject);
202 
203                 // Obtain the SIDs applicable to the principal
204                 Sid[] sids = sidRetrievalStrategy.getSids(authentication);
205 
206                 Acl acl;
207 
208                 try {
209                     // Lookup only ACLs for SIDs we're interested in
210                     acl = aclService.readAclById(objectIdentity, sids);
211                 } catch (NotFoundException nfe) {
212                     if (logger.isDebugEnabled()) {
213                         logger.debug("Voting to deny access - no ACLs apply for this principal");
214                     }
215 
216                     return AccessDecisionVoter.ACCESS_DENIED;
217                 }
218 
219                 try {
220                     if (acl.isGranted(requirePermission, sids, false)) {
221                         if (logger.isDebugEnabled()) {
222                             logger.debug("Voting to grant access");
223                         }
224 
225                         return AccessDecisionVoter.ACCESS_GRANTED;
226                     } else {
227                         if (logger.isDebugEnabled()) {
228                             logger.debug(
229                                 "Voting to deny access - ACLs returned, but insufficient permissions for this principal");
230                         }
231 
232                         return AccessDecisionVoter.ACCESS_DENIED;
233                     }
234                 } catch (NotFoundException nfe) {
235                     if (logger.isDebugEnabled()) {
236                         logger.debug("Voting to deny access - no ACLs apply for this principal");
237                     }
238 
239                     return AccessDecisionVoter.ACCESS_DENIED;
240                 }
241             }
242         }
243 
244         // No configuration attribute matched, so abstain
245         return AccessDecisionVoter.ACCESS_ABSTAIN;
246     }
247 }