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 }