Clover coverage report - Acegi Security System for Spring - 1.0.0-RC1
Coverage timestamp: Mon Dec 5 2005 09:05:15 EST
file stats: LOC: 530   Methods: 20
NCLOC: 286   Classes: 1
 
 Source file Conditionals Statements Methods TOTAL
AbstractSecurityInterceptor.java 80.4% 86.4% 80% 83.8%
coverage coverage
 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 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    //~ Static fields/initializers =============================================
 139   
 140    protected static final Log logger = LogFactory.getLog(AbstractSecurityInterceptor.class);
 141   
 142    //~ Instance fields ========================================================
 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    //~ Methods ================================================================
 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  9 protected Object afterInvocation(InterceptorStatusToken token,
 169    Object returnedObject) {
 170  9 if (token == null) {
 171    // public object
 172  2 return returnedObject;
 173    }
 174   
 175  7 if (token.isContextHolderRefreshRequired()) {
 176  2 if (logger.isDebugEnabled()) {
 177  0 logger.debug("Reverting to original Authentication: "
 178    + token.getAuthentication().toString());
 179    }
 180   
 181  2 SecurityContextHolder.getContext()
 182    .setAuthentication(token.getAuthentication());
 183    }
 184   
 185  7 if (afterInvocationManager != null) {
 186  1 returnedObject = afterInvocationManager.decide(token
 187    .getAuthentication(), token.getSecureObject(),
 188    token.getAttr(), returnedObject);
 189    }
 190   
 191  7 return returnedObject;
 192    }
 193   
 194  28 public void afterPropertiesSet() throws Exception {
 195  28 Assert.notNull(getSecureObjectClass(),
 196    "Subclass must provide a non-null response to getSecureObjectClass()");
 197   
 198  27 Assert.notNull(this.messages, "A message source must be set");
 199  27 Assert.notNull(this.authenticationManager,
 200    "An AuthenticationManager is required");
 201   
 202  26 Assert.notNull(this.accessDecisionManager,
 203    "An AccessDecisionManager is required");
 204   
 205  25 Assert.notNull(this.runAsManager, "A RunAsManager is required");
 206   
 207  24 Assert.notNull(this.obtainObjectDefinitionSource(),
 208    "An ObjectDefinitionSource is required");
 209   
 210  23 if (!this.obtainObjectDefinitionSource()
 211    .supports(getSecureObjectClass())) {
 212  1 throw new IllegalArgumentException(
 213    "ObjectDefinitionSource does not support secure object class: "
 214    + getSecureObjectClass());
 215    }
 216   
 217  22 if (!this.runAsManager.supports(getSecureObjectClass())) {
 218  2 throw new IllegalArgumentException(
 219    "RunAsManager does not support secure object class: "
 220    + getSecureObjectClass());
 221    }
 222   
 223  20 if (!this.accessDecisionManager.supports(getSecureObjectClass())) {
 224  2 throw new IllegalArgumentException(
 225    "AccessDecisionManager does not support secure object class: "
 226    + getSecureObjectClass());
 227    }
 228   
 229  18 if ((this.afterInvocationManager != null)
 230    && !this.afterInvocationManager.supports(getSecureObjectClass())) {
 231  1 throw new IllegalArgumentException(
 232    "AfterInvocationManager does not support secure object class: "
 233    + getSecureObjectClass());
 234    }
 235   
 236  17 if (this.validateConfigAttributes) {
 237  16 Iterator iter = this.obtainObjectDefinitionSource()
 238    .getConfigAttributeDefinitions();
 239   
 240  16 if (iter == null) {
 241  3 if (logger.isWarnEnabled()) {
 242  3 logger.warn(
 243    "Could not validate configuration attributes as the MethodDefinitionSource did not return a ConfigAttributeDefinition Iterator");
 244    }
 245    } else {
 246  13 Set set = new HashSet();
 247   
 248  13 while (iter.hasNext()) {
 249  33 ConfigAttributeDefinition def = (ConfigAttributeDefinition) iter
 250    .next();
 251  33 Iterator attributes = def.getConfigAttributes();
 252   
 253  33 while (attributes.hasNext()) {
 254  56 ConfigAttribute attr = (ConfigAttribute) attributes
 255    .next();
 256   
 257  56 if (!this.runAsManager.supports(attr)
 258    && !this.accessDecisionManager.supports(attr)
 259    && ((this.afterInvocationManager == null)
 260    || !this.afterInvocationManager.supports(attr))) {
 261  2 set.add(attr);
 262    }
 263    }
 264    }
 265   
 266  13 if (set.size() == 0) {
 267  12 if (logger.isInfoEnabled()) {
 268  0 logger.info("Validated configuration attributes");
 269    }
 270    } else {
 271  1 throw new IllegalArgumentException(
 272    "Unsupported configuration attributes: "
 273    + set.toString());
 274    }
 275    }
 276    }
 277    }
 278   
 279  15 protected InterceptorStatusToken beforeInvocation(Object object) {
 280  15 Assert.notNull(object, "Object was null");
 281  14 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  13 ConfigAttributeDefinition attr = this.obtainObjectDefinitionSource()
 289    .getAttributes(object);
 290   
 291  13 if ((attr == null) && rejectPublicInvocations) {
 292  0 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  13 if (attr != null) {
 297  11 if (logger.isDebugEnabled()) {
 298  0 logger.debug("Secure object: " + object.toString()
 299    + "; ConfigAttributes: " + attr.toString());
 300    }
 301   
 302    // We check for just the property we're interested in (we do
 303    // not call Context.validate() like the ContextInterceptor)
 304  11 if (SecurityContextHolder.getContext().getAuthentication() == null) {
 305  1 credentialsNotFound(messages.getMessage(
 306    "AbstractSecurityInterceptor.authenticationNotFound",
 307    "An Authentication object was not found in the SecurityContext"),
 308    object, attr);
 309    }
 310   
 311    // Attempt authentication if not already authenticated, or user always wants reauthentication
 312  10 Authentication authenticated;
 313   
 314  10 if (!SecurityContextHolder.getContext().getAuthentication()
 315    .isAuthenticated()
 316    || alwaysReauthenticate) {
 317  3 try {
 318  3 authenticated = this.authenticationManager.authenticate(SecurityContextHolder.getContext()
 319    .getAuthentication());
 320    } catch (AuthenticationException authenticationException) {
 321  1 throw authenticationException;
 322    }
 323   
 324    // We don't authenticated.setAuthentication(true), because each provider should do that
 325  2 if (logger.isDebugEnabled()) {
 326  0 logger.debug("Successfully Authenticated: "
 327    + authenticated.toString());
 328    }
 329   
 330  2 SecurityContextHolder.getContext()
 331    .setAuthentication(authenticated);
 332    } else {
 333  7 authenticated = SecurityContextHolder.getContext()
 334    .getAuthentication();
 335   
 336  7 if (logger.isDebugEnabled()) {
 337  0 logger.debug("Previously Authenticated: "
 338    + authenticated.toString());
 339    }
 340    }
 341   
 342    // Attempt authorization
 343  9 try {
 344  9 this.accessDecisionManager.decide(authenticated, object,
 345    attr);
 346    } catch (AccessDeniedException accessDeniedException) {
 347  2 AuthorizationFailureEvent event = new AuthorizationFailureEvent(object,
 348    attr, authenticated, accessDeniedException);
 349  2 this.eventPublisher.publishEvent(event);
 350   
 351  2 throw accessDeniedException;
 352    }
 353   
 354  7 if (logger.isDebugEnabled()) {
 355  0 logger.debug("Authorization successful");
 356    }
 357   
 358  7 AuthorizedEvent event = new AuthorizedEvent(object, attr,
 359    authenticated);
 360  7 this.eventPublisher.publishEvent(event);
 361   
 362    // Attempt to run as a different user
 363  7 Authentication runAs = this.runAsManager.buildRunAs(authenticated,
 364    object, attr);
 365   
 366  7 if (runAs == null) {
 367  5 if (logger.isDebugEnabled()) {
 368  0 logger.debug(
 369    "RunAsManager did not change Authentication object");
 370    }
 371   
 372  5 return new InterceptorStatusToken(authenticated, false,
 373    attr, object); // no further work post-invocation
 374    } else {
 375  2 if (logger.isDebugEnabled()) {
 376  0 logger.debug("Switching to RunAs Authentication: "
 377    + runAs.toString());
 378    }
 379   
 380  2 SecurityContextHolder.getContext().setAuthentication(runAs);
 381   
 382  2 return new InterceptorStatusToken(authenticated, true,
 383    attr, object); // revert to token.Authenticated post-invocation
 384    }
 385    } else {
 386  2 if (logger.isDebugEnabled()) {
 387  0 logger.debug("Public object - authentication not attempted");
 388    }
 389   
 390  2 this.eventPublisher.publishEvent(new PublicInvocationEvent(
 391    object));
 392   
 393  2 return null; // no further work post-invocation
 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  1 private void credentialsNotFound(String reason, Object secureObject,
 410    ConfigAttributeDefinition configAttribs) {
 411  1 AuthenticationCredentialsNotFoundException exception = new AuthenticationCredentialsNotFoundException(reason);
 412   
 413  1 AuthenticationCredentialsNotFoundEvent event = new AuthenticationCredentialsNotFoundEvent(secureObject,
 414    configAttribs, exception);
 415  1 this.eventPublisher.publishEvent(event);
 416   
 417  1 throw exception;
 418    }
 419   
 420  5 public AccessDecisionManager getAccessDecisionManager() {
 421  5 return accessDecisionManager;
 422    }
 423   
 424  1 public AfterInvocationManager getAfterInvocationManager() {
 425  1 return afterInvocationManager;
 426    }
 427   
 428  1 public AuthenticationManager getAuthenticationManager() {
 429  1 return this.authenticationManager;
 430    }
 431   
 432  1 public RunAsManager getRunAsManager() {
 433  1 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  0 public boolean isAlwaysReauthenticate() {
 447  0 return alwaysReauthenticate;
 448    }
 449   
 450  0 public boolean isRejectPublicInvocations() {
 451  0 return rejectPublicInvocations;
 452    }
 453   
 454  4 public boolean isValidateConfigAttributes() {
 455  4 return validateConfigAttributes;
 456    }
 457   
 458    public abstract ObjectDefinitionSource obtainObjectDefinitionSource();
 459   
 460  31 public void setAccessDecisionManager(
 461    AccessDecisionManager accessDecisionManager) {
 462  31 this.accessDecisionManager = accessDecisionManager;
 463    }
 464   
 465  17 public void setAfterInvocationManager(
 466    AfterInvocationManager afterInvocationManager) {
 467  17 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  0 public void setAlwaysReauthenticate(boolean alwaysReauthenticate) {
 485  0 this.alwaysReauthenticate = alwaysReauthenticate;
 486    }
 487   
 488  15 public void setApplicationEventPublisher(
 489    ApplicationEventPublisher eventPublisher) {
 490  15 this.eventPublisher = eventPublisher;
 491    }
 492   
 493  32 public void setAuthenticationManager(AuthenticationManager newManager) {
 494  32 this.authenticationManager = newManager;
 495    }
 496   
 497  11 public void setMessageSource(MessageSource messageSource) {
 498  11 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  0 public void setRejectPublicInvocations(boolean rejectPublicInvocations) {
 519  0 this.rejectPublicInvocations = rejectPublicInvocations;
 520    }
 521   
 522  30 public void setRunAsManager(RunAsManager runAsManager) {
 523  30 this.runAsManager = runAsManager;
 524    }
 525   
 526  1 public void setValidateConfigAttributes(
 527    boolean validateConfigAttributes) {
 528  1 this.validateConfigAttributes = validateConfigAttributes;
 529    }
 530    }