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