Source code: net/sf/acegisecurity/intercept/AbstractSecurityInterceptor.java
1 /* Copyright 2004, 2005 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.intercept;
17
18 import net.sf.acegisecurity.AccessDecisionManager;
19 import net.sf.acegisecurity.AccessDeniedException;
20 import net.sf.acegisecurity.AfterInvocationManager;
21 import net.sf.acegisecurity.Authentication;
22 import net.sf.acegisecurity.AuthenticationCredentialsNotFoundException;
23 import net.sf.acegisecurity.AuthenticationException;
24 import net.sf.acegisecurity.AuthenticationManager;
25 import net.sf.acegisecurity.ConfigAttribute;
26 import net.sf.acegisecurity.ConfigAttributeDefinition;
27 import net.sf.acegisecurity.RunAsManager;
28 import net.sf.acegisecurity.context.Context;
29 import net.sf.acegisecurity.context.ContextHolder;
30 import net.sf.acegisecurity.context.security.SecureContext;
31 import net.sf.acegisecurity.intercept.event.AuthenticationCredentialsNotFoundEvent;
32 import net.sf.acegisecurity.intercept.event.AuthenticationFailureEvent;
33 import net.sf.acegisecurity.intercept.event.AuthorizationFailureEvent;
34 import net.sf.acegisecurity.intercept.event.AuthorizedEvent;
35 import net.sf.acegisecurity.intercept.event.PublicInvocationEvent;
36 import net.sf.acegisecurity.runas.NullRunAsManager;
37
38 import org.apache.commons.logging.Log;
39 import org.apache.commons.logging.LogFactory;
40
41 import org.springframework.beans.BeansException;
42 import org.springframework.beans.factory.InitializingBean;
43
44 import org.springframework.context.ApplicationContext;
45 import org.springframework.context.ApplicationContextAware;
46 import org.springframework.util.Assert;
47
48 import java.util.HashSet;
49 import java.util.Iterator;
50 import java.util.Set;
51
52
53 /**
54 * Abstract class that implements security interception for secure objects.
55 *
56 * <P>
57 * The <code>AbstractSecurityInterceptor</code> will ensure the proper startup
58 * configuration of the security interceptor. It will also implement the
59 * proper handling of secure object invocations, being:
60 *
61 * <ol>
62 * <li>
63 * Extract the {@link SecureContext} from the {@link ContextHolder}, handling
64 * any errors such as invalid or <code>null</code> objects.
65 * </li>
66 * <li>
67 * Obtain the {@link Authentication} object from the extracted
68 * <code>SecureContext</code>.
69 * </li>
70 * <li>
71 * Determine if the request relates to a secured or public invocation by
72 * looking up the secure object request against the {@link
73 * ObjectDefinitionSource}.
74 * </li>
75 * <li>
76 * For an invocation that is secured (there is a
77 * <code>ConfigAttributeDefinition</code> for the secure object invocation):
78 *
79 * <ol type="a">
80 * <li>
81 * Authenticate the request against the configured {@link
82 * AuthenticationManager}, replacing the <code>Authentication</code> object on
83 * the <code>ContextHolder</code> with the returned value.
84 * </li>
85 * <li>
86 * Authorize the request against the configured {@link AccessDecisionManager}.
87 * </li>
88 * <li>
89 * Perform any run-as replacement via the configured {@link RunAsManager}.
90 * </li>
91 * <li>
92 * Pass control back to the concrete subclass, which will actually proceed with
93 * executing the object. A {@link InterceptorStatusToken} is returned so that
94 * after the subclass has finished proceeding with execution of the object,
95 * its finally clause can ensure the <code>AbstractSecurityInterceptor</code>
96 * is re-called and tidies up correctly.
97 * </li>
98 * <li>
99 * The concrete subclass will re-call the
100 * <code>AbstractSecurityInterceptor</code> via the {@link
101 * #afterInvocation(InterceptorStatusToken, Object)} method.
102 * </li>
103 * <li>
104 * If the <code>RunAsManager</code> replaced the <code>Authentication</code>
105 * object, return the <code>ContextHolder</code> to the object that existed
106 * after the call to <code>AuthenticationManager</code>.
107 * </li>
108 * <li>
109 * If an <code>AfterInvocationManager</code> is defined, invoke the invocation
110 * manager and allow it to replace the object due to be returned to the
111 * caller.
112 * </li>
113 * </ol>
114 *
115 * </li>
116 * <li>
117 * For an invocation that is public (there is no
118 * <code>ConfigAttributeDefinition</code> for the secure object invocation):
119 *
120 * <ol type="a">
121 * <li>
122 * If the <code>ContextHolder</code> contains a <code>SecureContext</code>, set
123 * the <code>isAuthenticated</code> flag on the <code>Authentication</code>
124 * object to false.
125 * </li>
126 * <li>
127 * As described above, the concrete subclass will be returned an
128 * <code>InterceptorStatusToken</code> which is subsequently re-presented to
129 * the <code>AbstractSecurityInterceptor</code> after the secure object has
130 * been executed. The <code>AbstractSecurityInterceptor</code> will take no
131 * further action when its {@link #afterInvocation(InterceptorStatusToken,
132 * Object)} is called.
133 * </li>
134 * </ol>
135 *
136 * </li>
137 * <li>
138 * Control again returns to the concrete subclass, along with the
139 * <code>Object</code> that should be returned to the caller. The subclass
140 * will then return that result or exception to the original caller.
141 * </li>
142 * </ol>
143 * </p>
144 *
145 * @author Ben Alex
146 * @version $Id: AbstractSecurityInterceptor.java,v 1.14 2005/04/15 01:21:34 luke_t Exp $
147 */
148 public abstract class AbstractSecurityInterceptor implements InitializingBean,
149 ApplicationContextAware {
150 //~ Static fields/initializers =============================================
151
152 protected static final Log logger = LogFactory.getLog(AbstractSecurityInterceptor.class);
153
154 //~ Instance fields ========================================================
155
156 private AccessDecisionManager accessDecisionManager;
157 private AfterInvocationManager afterInvocationManager;
158 private ApplicationContext context;
159 private AuthenticationManager authenticationManager;
160 private RunAsManager runAsManager = new NullRunAsManager();
161 private boolean validateConfigAttributes = true;
162
163 //~ Methods ================================================================
164
165 public void setAfterInvocationManager(
166 AfterInvocationManager afterInvocationManager) {
167 this.afterInvocationManager = afterInvocationManager;
168 }
169
170 public AfterInvocationManager getAfterInvocationManager() {
171 return afterInvocationManager;
172 }
173
174 public void setApplicationContext(ApplicationContext applicationContext)
175 throws BeansException {
176 this.context = applicationContext;
177 }
178
179 /**
180 * Indicates the type of secure objects the subclass will be presenting to
181 * the abstract parent for processing. This is used to ensure
182 * collaborators wired to the <code>AbstractSecurityInterceptor</code> all
183 * support the indicated secure object class.
184 *
185 * @return the type of secure object the subclass provides services for
186 */
187 public abstract Class getSecureObjectClass();
188
189 public abstract ObjectDefinitionSource obtainObjectDefinitionSource();
190
191 public void setAccessDecisionManager(
192 AccessDecisionManager accessDecisionManager) {
193 this.accessDecisionManager = accessDecisionManager;
194 }
195
196 public AccessDecisionManager getAccessDecisionManager() {
197 return accessDecisionManager;
198 }
199
200 public void setAuthenticationManager(AuthenticationManager newManager) {
201 this.authenticationManager = newManager;
202 }
203
204 public AuthenticationManager getAuthenticationManager() {
205 return this.authenticationManager;
206 }
207
208 public void setRunAsManager(RunAsManager runAsManager) {
209 this.runAsManager = runAsManager;
210 }
211
212 public RunAsManager getRunAsManager() {
213 return runAsManager;
214 }
215
216 public void setValidateConfigAttributes(boolean validateConfigAttributes) {
217 this.validateConfigAttributes = validateConfigAttributes;
218 }
219
220 public boolean isValidateConfigAttributes() {
221 return validateConfigAttributes;
222 }
223
224 public void afterPropertiesSet() throws Exception {
225 Assert.notNull(getSecureObjectClass(), "Subclass must provide a non-null response to getSecureObjectClass()");
226
227 Assert.notNull(this.authenticationManager, "An AuthenticationManager is required");
228
229 Assert.notNull(this.accessDecisionManager, "An AccessDecisionManager is required");
230
231 Assert.notNull(this.runAsManager, "A RunAsManager is required");
232
233 Assert.notNull(this.obtainObjectDefinitionSource(), "An ObjectDefinitionSource is required");
234
235 if (!this.obtainObjectDefinitionSource().supports(getSecureObjectClass())) {
236 throw new IllegalArgumentException("ObjectDefinitionSource does not support secure object class: "
237 + getSecureObjectClass());
238 }
239
240 if (!this.runAsManager.supports(getSecureObjectClass())) {
241 throw new IllegalArgumentException("RunAsManager does not support secure object class: "
242 + getSecureObjectClass());
243 }
244
245 if (!this.accessDecisionManager.supports(getSecureObjectClass())) {
246 throw new IllegalArgumentException("AccessDecisionManager does not support secure object class: "
247 + getSecureObjectClass());
248 }
249
250 if ((this.afterInvocationManager != null)
251 && !this.afterInvocationManager.supports(getSecureObjectClass())) {
252 throw new IllegalArgumentException("AfterInvocationManager does not support secure object class: "
253 + getSecureObjectClass());
254 }
255
256 if (this.validateConfigAttributes) {
257 Iterator iter = this.obtainObjectDefinitionSource()
258 .getConfigAttributeDefinitions();
259
260 if (iter == null) {
261 if (logger.isWarnEnabled()) {
262 logger.warn("Could not validate configuration attributes as the MethodDefinitionSource did not return a ConfigAttributeDefinition Iterator");
263 }
264 } else {
265 Set set = new HashSet();
266
267 while (iter.hasNext()) {
268 ConfigAttributeDefinition def = (ConfigAttributeDefinition) iter
269 .next();
270 Iterator attributes = def.getConfigAttributes();
271
272 while (attributes.hasNext()) {
273 ConfigAttribute attr = (ConfigAttribute) attributes
274 .next();
275
276 if (!this.runAsManager.supports(attr)
277 && !this.accessDecisionManager.supports(attr)
278 && ((this.afterInvocationManager == null)
279 || !this.afterInvocationManager.supports(attr))) {
280 set.add(attr);
281 }
282 }
283 }
284
285 if (set.size() == 0) {
286 if (logger.isInfoEnabled()) {
287 logger.info("Validated configuration attributes");
288 }
289 } else {
290 throw new IllegalArgumentException("Unsupported configuration attributes: "
291 + set.toString());
292 }
293 }
294 }
295 }
296
297 /**
298 * Completes the work of the <code>AbstractSecurityInterceptor</code> after
299 * the secure object invocation has been complete
300 *
301 * @param token as returned by the {@link #beforeInvocation(Object)}}
302 * method
303 * @param returnedObject any object returned from the secure object
304 * invocation (may be<code>null</code>)
305 *
306 * @return the object the secure object invocation should ultimately return
307 * to its caller (may be <code>null</code>)
308 */
309 protected Object afterInvocation(InterceptorStatusToken token,
310 Object returnedObject) {
311 if (token == null) {
312 // public object
313 return returnedObject;
314 }
315
316 if (token.isContextHolderRefreshRequired()) {
317 if (logger.isDebugEnabled()) {
318 logger.debug("Reverting to original Authentication: "
319 + token.getAuthentication().toString());
320 }
321
322 SecureContext secureContext = (SecureContext) ContextHolder
323 .getContext();
324 secureContext.setAuthentication(token.getAuthentication());
325 ContextHolder.setContext(secureContext);
326 }
327
328 if (afterInvocationManager != null) {
329 returnedObject = afterInvocationManager.decide(token
330 .getAuthentication(), token.getSecureObject(),
331 token.getAttr(), returnedObject);
332 }
333
334 return returnedObject;
335 }
336
337 protected InterceptorStatusToken beforeInvocation(Object object) {
338 Assert.notNull(object, "Object was null");
339 Assert.isTrue(getSecureObjectClass().isAssignableFrom(object.getClass()), "Security invocation attempted for object " + object
340 + " but AbstractSecurityInterceptor only configured to support secure objects of type: "
341 + getSecureObjectClass());
342
343 ConfigAttributeDefinition attr = this.obtainObjectDefinitionSource()
344 .getAttributes(object);
345
346 if (attr != null) {
347 if (logger.isDebugEnabled()) {
348 logger.debug("Secure object: " + object.toString()
349 + "; ConfigAttributes: " + attr.toString());
350 }
351
352 // Ensure ContextHolder presents a populated SecureContext
353 if ((ContextHolder.getContext() == null)
354 || !(ContextHolder.getContext() instanceof SecureContext)) {
355 credentialsNotFound("A valid SecureContext was not provided in the RequestContext",
356 object, attr);
357 }
358
359 SecureContext context = (SecureContext) ContextHolder.getContext();
360
361 // We check for just the property we're interested in (we do
362 // not call Context.validate() like the ContextInterceptor)
363 if (context.getAuthentication() == null) {
364 credentialsNotFound("Authentication credentials were not found in the SecureContext",
365 object, attr);
366 }
367
368 // Attempt authentication
369 Authentication authenticated;
370
371 try {
372 authenticated = this.authenticationManager.authenticate(context
373 .getAuthentication());
374 } catch (AuthenticationException authenticationException) {
375 AuthenticationFailureEvent event = new AuthenticationFailureEvent(object,
376 attr, context.getAuthentication(),
377 authenticationException);
378 this.context.publishEvent(event);
379
380 throw authenticationException;
381 }
382
383 authenticated.setAuthenticated(true);
384
385 if (logger.isDebugEnabled()) {
386 logger.debug("Authenticated: " + authenticated.toString());
387 }
388
389 context.setAuthentication(authenticated);
390 ContextHolder.setContext((Context) context);
391
392 // Attempt authorization
393 try {
394 this.accessDecisionManager.decide(authenticated, object, attr);
395 } catch (AccessDeniedException accessDeniedException) {
396 AuthorizationFailureEvent event = new AuthorizationFailureEvent(object,
397 attr, authenticated, accessDeniedException);
398 this.context.publishEvent(event);
399
400 throw accessDeniedException;
401 }
402
403 if (logger.isDebugEnabled()) {
404 logger.debug("Authorization successful");
405 }
406
407 AuthorizedEvent event = new AuthorizedEvent(object, attr,
408 authenticated);
409 this.context.publishEvent(event);
410
411 // Attempt to run as a different user
412 Authentication runAs = this.runAsManager.buildRunAs(authenticated,
413 object, attr);
414
415 if (runAs == null) {
416 if (logger.isDebugEnabled()) {
417 logger.debug("RunAsManager did not change Authentication object");
418 }
419
420 return new InterceptorStatusToken(authenticated, false, attr,
421 object); // no further work post-invocation
422 } else {
423 if (logger.isDebugEnabled()) {
424 logger.debug("Switching to RunAs Authentication: "
425 + runAs.toString());
426 }
427
428 context.setAuthentication(runAs);
429 ContextHolder.setContext((Context) context);
430
431 return new InterceptorStatusToken(authenticated, true, attr,
432 object); // revert to token.Authenticated post-invocation
433 }
434 } else {
435 if (logger.isDebugEnabled()) {
436 logger.debug("Public object - authentication not attempted");
437 }
438
439 this.context.publishEvent(new PublicInvocationEvent(object));
440
441 // Set Authentication object (if it exists) to be unauthenticated
442 if ((ContextHolder.getContext() != null)
443 && ContextHolder.getContext() instanceof SecureContext) {
444 SecureContext context = (SecureContext) ContextHolder
445 .getContext();
446
447 if (context.getAuthentication() != null) {
448 if (logger.isDebugEnabled()) {
449 logger.debug("Authentication object detected and tagged as unauthenticated");
450 }
451
452 Authentication authenticated = context.getAuthentication();
453 authenticated.setAuthenticated(false);
454 context.setAuthentication(authenticated);
455 ContextHolder.setContext((Context) context);
456 }
457 }
458
459 return null; // no further work post-invocation
460 }
461 }
462
463 /**
464 * Helper method which generates an exception containing the passed reason,
465 * and publishes an event to the application context.
466 *
467 * <P>
468 * Always throws an exception.
469 * </p>
470 *
471 * @param reason to be provided in the exception detail
472 * @param secureObject that was being called
473 * @param configAttribs that were defined for the secureObject
474 */
475 private void credentialsNotFound(String reason, Object secureObject,
476 ConfigAttributeDefinition configAttribs) {
477 AuthenticationCredentialsNotFoundException exception = new AuthenticationCredentialsNotFoundException(reason);
478
479 AuthenticationCredentialsNotFoundEvent event = new AuthenticationCredentialsNotFoundEvent(secureObject,
480 configAttribs, exception);
481 this.context.publishEvent(event);
482
483 throw exception;
484 }
485 }