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