Coverage Report - org.acegisecurity.intercept.AbstractSecurityInterceptor
 
Classes in this File Line Coverage Branch Coverage Complexity
AbstractSecurityInterceptor
84% 
100% 
2.696
 
 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  45
 public abstract class AbstractSecurityInterceptor implements InitializingBean, ApplicationEventPublisherAware,
 120  
                 MessageSourceAware {
 121  
         // ~ Static fields/initializers
 122  
         // =====================================================================================
 123  
 
 124  2
         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  45
         protected MessageSourceAccessor messages = AcegiMessageSource.getAccessor();
 138  
 
 139  45
         private RunAsManager runAsManager = new NullRunAsManager();
 140  
 
 141  45
         private boolean alwaysReauthenticate = false;
 142  
 
 143  45
         private boolean rejectPublicInvocations = false;
 144  
 
 145  45
         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  28
                 if (token == null) {
 164  
                         // public object
 165  3
                         return returnedObject;
 166  
                 }
 167  
 
 168  25
                 if (token.isContextHolderRefreshRequired()) {
 169  2
                         if (logger.isDebugEnabled()) {
 170  0
                                 logger.debug("Reverting to original Authentication: " + token.getAuthentication().toString());
 171  
                         }
 172  
 
 173  2
                         SecurityContextHolder.getContext().setAuthentication(token.getAuthentication());
 174  
                 }
 175  
 
 176  25
                 if (afterInvocationManager != null) {
 177  
                         // Attempt after invocation handling
 178  
                         try {
 179  1
                                 returnedObject = afterInvocationManager.decide(token.getAuthentication(), token.getSecureObject(),
 180  
                                                 token.getAttr(), returnedObject);
 181  
                         }
 182  0
                         catch (AccessDeniedException accessDeniedException) {
 183  0
                                 AuthorizationFailureEvent event = new AuthorizationFailureEvent(token.getSecureObject(), token
 184  
                                                 .getAttr(), token.getAuthentication(), accessDeniedException);
 185  0
                                 publishEvent(event);
 186  
 
 187  0
                                 throw accessDeniedException;
 188  1
                         }
 189  
                 }
 190  
 
 191  25
                 return returnedObject;
 192  
         }
 193  
 
 194  
         public void afterPropertiesSet() throws Exception {
 195  35
                 Assert.notNull(getSecureObjectClass(), "Subclass must provide a non-null response to getSecureObjectClass()");
 196  
 
 197  35
                 Assert.notNull(this.messages, "A message source must be set");
 198  
 
 199  35
                 Assert.notNull(this.authenticationManager, "An AuthenticationManager is required");
 200  
 
 201  34
                 Assert.notNull(this.accessDecisionManager, "An AccessDecisionManager is required");
 202  
 
 203  33
                 Assert.notNull(this.runAsManager, "A RunAsManager is required");
 204  
 
 205  32
                 Assert.notNull(this.obtainObjectDefinitionSource(), "An ObjectDefinitionSource is required");
 206  
 
 207  31
                 Assert.isTrue(this.obtainObjectDefinitionSource().supports(getSecureObjectClass()),
 208  
                                 "ObjectDefinitionSource does not support secure object class: " + getSecureObjectClass());
 209  
 
 210  30
                 Assert.isTrue(this.runAsManager.supports(getSecureObjectClass()),
 211  
                                 "RunAsManager does not support secure object class: " + getSecureObjectClass());
 212  
 
 213  28
                 Assert.isTrue(this.accessDecisionManager.supports(getSecureObjectClass()),
 214  
                                 "AccessDecisionManager does not support secure object class: " + getSecureObjectClass());
 215  
 
 216  26
                 if (this.afterInvocationManager != null) {
 217  14
                         Assert.isTrue(this.afterInvocationManager.supports(getSecureObjectClass()),
 218  
                                         "AfterInvocationManager does not support secure object class: " + getSecureObjectClass());
 219  
                 }
 220  
 
 221  25
                 if (this.validateConfigAttributes) {
 222  23
                         Iterator iter = this.obtainObjectDefinitionSource().getConfigAttributeDefinitions();
 223  
 
 224  23
                         if (iter == null) {
 225  3
                                 logger.warn("Could not validate configuration attributes as the MethodDefinitionSource did not return "
 226  
                                                 + "a ConfigAttributeDefinition Iterator");
 227  3
                                 return;
 228  
                         }
 229  
 
 230  20
                         Set unsupportedAttrs = new HashSet();
 231  
 
 232  74
                         while (iter.hasNext()) {
 233  54
                                 ConfigAttributeDefinition def = (ConfigAttributeDefinition) iter.next();
 234  54
                                 Iterator attributes = def.getConfigAttributes();
 235  
 
 236  139
                                 while (attributes.hasNext()) {
 237  85
                                         ConfigAttribute attr = (ConfigAttribute) attributes.next();
 238  
 
 239  85
                                         if (!this.runAsManager.supports(attr) && !this.accessDecisionManager.supports(attr)
 240  
                                                         && ((this.afterInvocationManager == null) || !this.afterInvocationManager.supports(attr))) {
 241  2
                                                 unsupportedAttrs.add(attr);
 242  
                                         }
 243  85
                                 }
 244  54
                         }
 245  
 
 246  20
                         if (unsupportedAttrs.size() != 0) {
 247  1
                                 throw new IllegalArgumentException("Unsupported configuration attributes: " + unsupportedAttrs);
 248  
                         }
 249  
 
 250  19
                         logger.info("Validated configuration attributes");
 251  
                 }
 252  21
         }
 253  
 
 254  
         protected InterceptorStatusToken beforeInvocation(Object object) {
 255  39
                 Assert.notNull(object, "Object was null");
 256  
 
 257  38
                 if (!getSecureObjectClass().isAssignableFrom(object.getClass())) {
 258  0
                         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  38
                 ConfigAttributeDefinition attr = this.obtainObjectDefinitionSource().getAttributes(object);
 265  
 
 266  38
                 if (attr == null) {
 267  3
                         if (rejectPublicInvocations) {
 268  0
                                 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  3
                         if (logger.isDebugEnabled()) {
 275  0
                                 logger.debug("Public object - authentication not attempted");
 276  
                         }
 277  
 
 278  3
                         publishEvent(new PublicInvocationEvent(object));
 279  
 
 280  3
                         return null; // no further work post-invocation
 281  
                 }
 282  
 
 283  35
                 if (logger.isDebugEnabled()) {
 284  0
                         logger.debug("Secure object: " + object.toString() + "; ConfigAttributes: " + attr.toString());
 285  
                 }
 286  
 
 287  35
                 if (SecurityContextHolder.getContext().getAuthentication() == null) {
 288  1
                         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  34
                 if (!SecurityContextHolder.getContext().getAuthentication().isAuthenticated() || alwaysReauthenticate) {
 297  
                         try {
 298  3
                                 authenticated = this.authenticationManager.authenticate(SecurityContextHolder.getContext()
 299  
                                                 .getAuthentication());
 300  
                         }
 301  1
                         catch (AuthenticationException authenticationException) {
 302  1
                                 throw authenticationException;
 303  2
                         }
 304  
 
 305  
                         // We don't authenticated.setAuthentication(true), because each
 306  
                         // provider should do that
 307  2
                         if (logger.isDebugEnabled()) {
 308  0
                                 logger.debug("Successfully Authenticated: " + authenticated.toString());
 309  
                         }
 310  
 
 311  2
                         SecurityContextHolder.getContext().setAuthentication(authenticated);
 312  
                 }
 313  
                 else {
 314  31
                         authenticated = SecurityContextHolder.getContext().getAuthentication();
 315  
 
 316  31
                         if (logger.isDebugEnabled()) {
 317  0
                                 logger.debug("Previously Authenticated: " + authenticated.toString());
 318  
                         }
 319  
                 }
 320  
 
 321  
                 // Attempt authorization
 322  
                 try {
 323  33
                         this.accessDecisionManager.decide(authenticated, object, attr);
 324  
                 }
 325  8
                 catch (AccessDeniedException accessDeniedException) {
 326  8
                         AuthorizationFailureEvent event = new AuthorizationFailureEvent(object, attr, authenticated,
 327  
                                         accessDeniedException);
 328  8
                         publishEvent(event);
 329  
 
 330  8
                         throw accessDeniedException;
 331  25
                 }
 332  
 
 333  25
                 if (logger.isDebugEnabled()) {
 334  0
                         logger.debug("Authorization successful");
 335  
                 }
 336  
 
 337  25
                 AuthorizedEvent event = new AuthorizedEvent(object, attr, authenticated);
 338  25
                 publishEvent(event);
 339  
 
 340  
                 // Attempt to run as a different user
 341  25
                 Authentication runAs = this.runAsManager.buildRunAs(authenticated, object, attr);
 342  
 
 343  25
                 if (runAs == null) {
 344  23
                         if (logger.isDebugEnabled()) {
 345  0
                                 logger.debug("RunAsManager did not change Authentication object");
 346  
                         }
 347  
 
 348  
                         // no further work post-invocation
 349  23
                         return new InterceptorStatusToken(authenticated, false, attr, object);
 350  
                 }
 351  
                 else {
 352  2
                         if (logger.isDebugEnabled()) {
 353  0
                                 logger.debug("Switching to RunAs Authentication: " + runAs.toString());
 354  
                         }
 355  
 
 356  2
                         SecurityContextHolder.getContext().setAuthentication(runAs);
 357  
 
 358  
                         // revert to token.Authenticated post-invocation
 359  2
                         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  1
                 AuthenticationCredentialsNotFoundException exception = new AuthenticationCredentialsNotFoundException(reason);
 376  
 
 377  1
                 AuthenticationCredentialsNotFoundEvent event = new AuthenticationCredentialsNotFoundEvent(secureObject,
 378  
                                 configAttribs, exception);
 379  1
                 publishEvent(event);
 380  
 
 381  1
                 throw exception;
 382  
         }
 383  
 
 384  
         public AccessDecisionManager getAccessDecisionManager() {
 385  15
                 return accessDecisionManager;
 386  
         }
 387  
 
 388  
         public AfterInvocationManager getAfterInvocationManager() {
 389  1
                 return afterInvocationManager;
 390  
         }
 391  
 
 392  
         public AuthenticationManager getAuthenticationManager() {
 393  1
                 return this.authenticationManager;
 394  
         }
 395  
 
 396  
         public RunAsManager getRunAsManager() {
 397  1
                 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  0
                 return alwaysReauthenticate;
 412  
         }
 413  
 
 414  
         public boolean isRejectPublicInvocations() {
 415  0
                 return rejectPublicInvocations;
 416  
         }
 417  
 
 418  
         public boolean isValidateConfigAttributes() {
 419  4
                 return validateConfigAttributes;
 420  
         }
 421  
 
 422  
         public abstract ObjectDefinitionSource obtainObjectDefinitionSource();
 423  
 
 424  
         public void setAccessDecisionManager(AccessDecisionManager accessDecisionManager) {
 425  37
                 this.accessDecisionManager = accessDecisionManager;
 426  37
         }
 427  
 
 428  
         public void setAfterInvocationManager(AfterInvocationManager afterInvocationManager) {
 429  19
                 this.afterInvocationManager = afterInvocationManager;
 430  19
         }
 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  0
                 this.alwaysReauthenticate = alwaysReauthenticate;
 447  0
         }
 448  
 
 449  
         public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
 450  23
                 this.eventPublisher = applicationEventPublisher;
 451  23
         }
 452  
 
 453  
         public void setAuthenticationManager(AuthenticationManager newManager) {
 454  38
                 this.authenticationManager = newManager;
 455  38
         }
 456  
 
 457  
         public void setMessageSource(MessageSource messageSource) {
 458  19
                 this.messages = new MessageSourceAccessor(messageSource);
 459  19
         }
 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  0
                 this.rejectPublicInvocations = rejectPublicInvocations;
 480  0
         }
 481  
 
 482  
         public void setRunAsManager(RunAsManager runAsManager) {
 483  35
                 this.runAsManager = runAsManager;
 484  35
         }
 485  
 
 486  
         public void setValidateConfigAttributes(boolean validateConfigAttributes) {
 487  2
                 this.validateConfigAttributes = validateConfigAttributes;
 488  2
         }
 489  
 
 490  
         private void publishEvent(ApplicationEvent event) {
 491  37
                 if (this.eventPublisher != null) {
 492  36
                         this.eventPublisher.publishEvent(event);
 493  
                 }
 494  37
         }
 495  
 }