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     }