View Javadoc

    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     protected Object afterInvocation(InterceptorStatusToken token,
    169         Object returnedObject) {
    170         if (token == null) {
    171             // public object
    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                 // We check for just the property we're interested in (we do
    303                 // not call Context.validate() like the ContextInterceptor)
    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                 // Attempt authentication if not already authenticated, or user always wants reauthentication
    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                     // We don't authenticated.setAuthentication(true), because each provider should do that
    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                 // Attempt authorization
    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                 // Attempt to run as a different user
    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); // no further work post-invocation
    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); // revert to token.Authenticated post-invocation
    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; // 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         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     }