Source code: net/sf/acegisecurity/vote/BasicAclEntryVoter.java
1 /* Copyright 2004 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 net.sf.acegisecurity.vote;
17
18 import net.sf.acegisecurity.Authentication;
19 import net.sf.acegisecurity.AuthorizationServiceException;
20 import net.sf.acegisecurity.ConfigAttribute;
21 import net.sf.acegisecurity.ConfigAttributeDefinition;
22 import net.sf.acegisecurity.acl.AclEntry;
23 import net.sf.acegisecurity.acl.AclManager;
24 import net.sf.acegisecurity.acl.basic.AbstractBasicAclEntry;
25
26 import org.aopalliance.intercept.MethodInvocation;
27
28 import org.apache.commons.logging.Log;
29 import org.apache.commons.logging.LogFactory;
30
31 import org.springframework.beans.factory.InitializingBean;
32 import org.springframework.util.Assert;
33
34 import java.lang.reflect.InvocationTargetException;
35 import java.lang.reflect.Method;
36
37 import java.util.Iterator;
38
39
40 /**
41 * <p>
42 * Given a domain object instance passed as a method argument, ensures the
43 * principal has appropriate permission as defined by the {@link AclManager}.
44 * </p>
45 *
46 * <p>
47 * The <code>AclManager</code> is used to retrieve the access control list
48 * (ACL) permissions associated with a domain object instance for the current
49 * <code>Authentication</code> object. This class is designed to process
50 * {@link AclEntry}s that are subclasses of {@link
51 * net.sf.acegisecurity.acl.basic.AbstractBasicAclEntry} only. Generally these
52 * are obtained by using the {@link
53 * net.sf.acegisecurity.acl.basic.BasicAclProvider}.
54 * </p>
55 *
56 * <p>
57 * The voter will vote if any {@link ConfigAttribute#getAttribute()} matches
58 * the {@link #processConfigAttribute}. The provider will then locate the
59 * first method argument of type {@link #processDomainObjectClass}. Assuming
60 * that method argument is non-null, the provider will then lookup the ACLs
61 * from the <code>AclManager</code> and ensure the principal is {@link
62 * net.sf.acegisecurity.acl.basic.AbstractBasicAclEntry#isPermitted(int)} for
63 * at least one of the {@link #requirePermission}s.
64 * </p>
65 *
66 * <p>
67 * If the method argument is <code>null</code>, the voter will abstain from
68 * voting. If the method argument could not be found, an {@link
69 * net.sf.acegisecurity.AuthorizationServiceException} will be thrown.
70 * </p>
71 *
72 * <p>
73 * In practical terms users will typically setup a number of
74 * <code>BasicAclEntryVoter</code>s. Each will have a different {@link
75 * #processDomainObjectClass}, {@link #processConfigAttribute} and {@link
76 * #requirePermission} combination. For example, a small application might
77 * employ the following instances of <code>BasicAclEntryVoter</code>:
78 *
79 * <ul>
80 * <li>
81 * Process domain object class <code>BankAccount</code>, configuration
82 * attribute <code>VOTE_ACL_BANK_ACCONT_READ</code>, require permission
83 * <code>SimpleAclEntry.READ</code>
84 * </li>
85 * <li>
86 * Process domain object class <code>BankAccount</code>, configuration
87 * attribute <code>VOTE_ACL_BANK_ACCOUNT_WRITE</code>, require permission list
88 * <code>SimpleAclEntry.WRITE</code> and <code>SimpleAclEntry.CREATE</code>
89 * (allowing the principal to have <b>either</b> of these two permissions
90 * </li>
91 * <li>
92 * Process domain object class <code>Customer</code>, configuration attribute
93 * <code>VOTE_ACL_CUSTOMER_READ</code>, require permission
94 * <code>SimpleAclEntry.READ</code>
95 * </li>
96 * <li>
97 * Process domain object class <code>Customer</code>, configuration attribute
98 * <code>VOTE_ACL_CUSTOMER_WRITE</code>, require permission list
99 * <code>SimpleAclEntry.WRITE</code> and <code>SimpleAclEntry.CREATE</code>
100 * </li>
101 * </ul>
102 *
103 * Alternatively, you could have used a common superclass or interface for the
104 * {@link #processDomainObjectClass} if both <code>BankAccount</code> and
105 * <code>Customer</code> had common parents.
106 * </p>
107 *
108 * <p>
109 * If the principal does not have sufficient permissions, the voter will vote
110 * to deny access.
111 * </p>
112 *
113 * <p>
114 * The <code>AclManager</code> is allowed to return any implementations of
115 * <code>AclEntry</code> it wishes. However, this provider will only be able
116 * to validate against <code>AbstractBasicAclEntry</code>s, and thus a vote to
117 * deny access will be made if no <code>AclEntry</code> is of type
118 * <code>AbstractBasicAclEntry</code>.
119 * </p>
120 *
121 * <p>
122 * All comparisons and prefixes are case sensitive.
123 * </p>
124 *
125 * @author Ben Alex
126 * @version $Id: BasicAclEntryVoter.java,v 1.4 2005/04/15 01:21:41 luke_t Exp $
127 */
128 public class BasicAclEntryVoter implements AccessDecisionVoter,
129 InitializingBean {
130 //~ Static fields/initializers =============================================
131
132 private static final Log logger = LogFactory.getLog(BasicAclEntryVoter.class);
133
134 //~ Instance fields ========================================================
135
136 private AclManager aclManager;
137 private Class processDomainObjectClass;
138 private String internalMethod;
139 private String processConfigAttribute;
140 private int[] requirePermission;
141
142 //~ Methods ================================================================
143
144 public void setAclManager(AclManager aclManager) {
145 this.aclManager = aclManager;
146 }
147
148 public AclManager getAclManager() {
149 return aclManager;
150 }
151
152 public void setInternalMethod(String internalMethod) {
153 this.internalMethod = internalMethod;
154 }
155
156 /**
157 * Optionally specifies a method of the domain object that will be used to
158 * obtain a contained domain object. That contained domain object will be
159 * used for the ACL evaluation. This is useful if a domain object contains
160 * a parent that an ACL evaluation should be targeted for, instead of the
161 * child domain object (which perhaps is being created and as such does
162 * not yet have any ACL permissions)
163 *
164 * @return <code>null</code> to use the domain object, or the name of a
165 * method (that requires no arguments) that should be invoked to
166 * obtain an <code>Object</code> which will be the domain object
167 * used for ACL evaluation
168 */
169 public String getInternalMethod() {
170 return internalMethod;
171 }
172
173 public void setProcessConfigAttribute(String processConfigAttribute) {
174 this.processConfigAttribute = processConfigAttribute;
175 }
176
177 public String getProcessConfigAttribute() {
178 return processConfigAttribute;
179 }
180
181 public void setProcessDomainObjectClass(Class processDomainObjectClass) {
182 this.processDomainObjectClass = processDomainObjectClass;
183 }
184
185 public Class getProcessDomainObjectClass() {
186 return processDomainObjectClass;
187 }
188
189 public void setRequirePermission(int[] requirePermission) {
190 this.requirePermission = requirePermission;
191 }
192
193 public int[] getRequirePermission() {
194 return requirePermission;
195 }
196
197 public void afterPropertiesSet() throws Exception {
198 Assert.notNull(processConfigAttribute, "A processConfigAttribute is mandatory");
199 Assert.notNull(aclManager, "An aclManager is mandatory");
200 Assert.notNull(processDomainObjectClass, "A processDomainObjectClass is mandatory");
201
202 if ((requirePermission == null) || (requirePermission.length == 0)) {
203 throw new IllegalArgumentException("One or more requirePermission entries is mandatory");
204 }
205 }
206
207 public boolean supports(ConfigAttribute attribute) {
208 if ((attribute.getAttribute() != null)
209 && attribute.getAttribute().startsWith(getProcessConfigAttribute())) {
210 return true;
211 } else {
212 return false;
213 }
214 }
215
216 /**
217 * This implementation supports only
218 * <code>MethodSecurityInterceptor</code>, because it queries the
219 * presented <code>MethodInvocation</code>.
220 *
221 * @param clazz the secure object
222 *
223 * @return <code>true</code> if the secure object is
224 * <code>MethodInvocation</code>, <code>false</code> otherwise
225 */
226 public boolean supports(Class clazz) {
227 return (MethodInvocation.class.isAssignableFrom(clazz));
228 }
229
230 public int vote(Authentication authentication, Object object,
231 ConfigAttributeDefinition config) {
232 Iterator iter = config.getConfigAttributes();
233
234 while (iter.hasNext()) {
235 ConfigAttribute attr = (ConfigAttribute) iter.next();
236
237 if (this.supports(attr)) {
238 // Need to make an access decision on this invocation
239 // Attempt to locate the domain object instance to process
240 Object domainObject = getDomainObjectInstance(object);
241
242 // If domain object is null, vote to abstain
243 if (domainObject == null) {
244 return AccessDecisionVoter.ACCESS_ABSTAIN;
245 }
246
247 // Evaluate if we are required to use an inner domain object
248 if ((internalMethod != null) && !"".equals(internalMethod)) {
249 try {
250 Class clazz = domainObject.getClass();
251 Method method = clazz.getMethod(internalMethod, null);
252 domainObject = method.invoke(domainObject, null);
253 } catch (NoSuchMethodException nsme) {
254 throw new AuthorizationServiceException(
255 "Object of class '" + domainObject.getClass()
256 + "' does not provide the requested internalMethod: "
257 + internalMethod);
258 } catch (IllegalAccessException iae) {
259 if (logger.isDebugEnabled()) {
260 logger.debug("IllegalAccessException", iae);
261
262 if (iae.getCause() != null) {
263 logger.debug("Cause: "
264 + iae.getCause().getMessage(),
265 iae.getCause());
266 }
267 }
268
269 throw new AuthorizationServiceException(
270 "Problem invoking internalMethod: "
271 + internalMethod + " for object: " + domainObject);
272 } catch (InvocationTargetException ite) {
273 if (logger.isDebugEnabled()) {
274 logger.debug("InvocationTargetException", ite);
275
276 if (ite.getCause() != null) {
277 logger.debug("Cause: "
278 + ite.getCause().getMessage(),
279 ite.getCause());
280 }
281 }
282
283 throw new AuthorizationServiceException(
284 "Problem invoking internalMethod: "
285 + internalMethod + " for object: " + domainObject);
286 }
287 }
288
289 // Obtain the ACLs applicable to the domain object
290 AclEntry[] acls = aclManager.getAcls(domainObject,
291 authentication);
292
293 // If principal has no permissions for domain object, deny
294 if ((acls == null) || (acls.length == 0)) {
295 return AccessDecisionVoter.ACCESS_DENIED;
296 }
297
298 // Principal has some permissions for domain object, check them
299 for (int i = 0; i < acls.length; i++) {
300 // Locate processable AclEntrys
301 if (acls[i] instanceof AbstractBasicAclEntry) {
302 AbstractBasicAclEntry processableAcl = (AbstractBasicAclEntry) acls[i];
303
304 // See if principal has any of the required permissions
305 for (int y = 0; y < requirePermission.length; y++) {
306 if (processableAcl.isPermitted(requirePermission[y])) {
307 return AccessDecisionVoter.ACCESS_GRANTED;
308 }
309 }
310 }
311 }
312
313 // No permissions match
314 return AccessDecisionVoter.ACCESS_DENIED;
315 }
316 }
317
318 // No configuration attribute matched, so abstain
319 return AccessDecisionVoter.ACCESS_ABSTAIN;
320 }
321
322 private Object getDomainObjectInstance(Object secureObject) {
323 MethodInvocation invocation = (MethodInvocation) secureObject;
324
325 // Check if this MethodInvocation provides the required argument
326 Method method = invocation.getMethod();
327 Class[] params = method.getParameterTypes();
328
329 for (int i = 0; i < params.length; i++) {
330 if (processDomainObjectClass.isAssignableFrom(params[i])) {
331 return invocation.getArguments()[i];
332 }
333 }
334
335 throw new AuthorizationServiceException("MethodInvocation: "
336 + invocation + " did not provide any argument of type: "
337 + processDomainObjectClass);
338 }
339 }