001: /* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
002: *
003: * Licensed under the Apache License, Version 2.0 (the "License");
004: * you may not use this file except in compliance with the License.
005: * You may obtain a copy of the License at
006: *
007: * http://www.apache.org/licenses/LICENSE-2.0
008: *
009: * Unless required by applicable law or agreed to in writing, software
010: * distributed under the License is distributed on an "AS IS" BASIS,
011: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012: * See the License for the specific language governing permissions and
013: * limitations under the License.
014: */
015:
016: package org.acegisecurity.intercept;
017:
018: import org.acegisecurity.AccessDecisionManager;
019: import org.acegisecurity.AccessDeniedException;
020: import org.acegisecurity.AcegiMessageSource;
021: import org.acegisecurity.AfterInvocationManager;
022: import org.acegisecurity.Authentication;
023: import org.acegisecurity.AuthenticationCredentialsNotFoundException;
024: import org.acegisecurity.AuthenticationException;
025: import org.acegisecurity.AuthenticationManager;
026: import org.acegisecurity.ConfigAttribute;
027: import org.acegisecurity.ConfigAttributeDefinition;
028: import org.acegisecurity.RunAsManager;
029:
030: import org.acegisecurity.context.SecurityContextHolder;
031:
032: import org.acegisecurity.event.authorization.AuthenticationCredentialsNotFoundEvent;
033: import org.acegisecurity.event.authorization.AuthorizationFailureEvent;
034: import org.acegisecurity.event.authorization.AuthorizedEvent;
035: import org.acegisecurity.event.authorization.PublicInvocationEvent;
036:
037: import org.acegisecurity.runas.NullRunAsManager;
038:
039: import org.apache.commons.logging.Log;
040: import org.apache.commons.logging.LogFactory;
041:
042: import org.springframework.beans.factory.InitializingBean;
043:
044: import org.springframework.context.ApplicationEvent;
045: import org.springframework.context.ApplicationEventPublisher;
046: import org.springframework.context.ApplicationEventPublisherAware;
047: import org.springframework.context.MessageSource;
048: import org.springframework.context.MessageSourceAware;
049: import org.springframework.context.support.MessageSourceAccessor;
050:
051: import org.springframework.util.Assert;
052:
053: import java.util.HashSet;
054: import java.util.Iterator;
055: import java.util.Set;
056:
057: /**
058: * Abstract class that implements security interception for secure objects.
059: * <p>
060: * The <code>AbstractSecurityInterceptor</code> will ensure the proper startup
061: * configuration of the security interceptor. It will also implement the proper
062: * handling of secure object invocations, being:
063: * <ol>
064: * <li>Obtain the {@link Authentication} object from the
065: * {@link SecurityContextHolder}.</li>
066: * <li>Determine if the request relates to a secured or public invocation by
067: * looking up the secure object request against the
068: * {@link ObjectDefinitionSource}.</li>
069: * <li>For an invocation that is secured (there is a
070: * <code>ConfigAttributeDefinition</code> for the secure object invocation):
071: * <ol type="a">
072: * <li>If either the {@link org.acegisecurity.Authentication#isAuthenticated()}
073: * returns <code>false</code>, or the {@link #alwaysReauthenticate} is
074: * <code>true</code>, authenticate the request against the configured
075: * {@link AuthenticationManager}. When authenticated, replace the
076: * <code>Authentication</code> object on the
077: * <code>SecurityContextHolder</code> with the returned value.</li>
078: * <li>Authorize the request against the configured
079: * {@link AccessDecisionManager}.</li>
080: * <li>Perform any run-as replacement via the configured {@link RunAsManager}.</li>
081: * <li>Pass control back to the concrete subclass, which will actually proceed
082: * with executing the object. A {@link InterceptorStatusToken} is returned so
083: * that after the subclass has finished proceeding with execution of the object,
084: * its finally clause can ensure the <code>AbstractSecurityInterceptor</code>
085: * is re-called and tidies up correctly.</li>
086: * <li>The concrete subclass will re-call the
087: * <code>AbstractSecurityInterceptor</code> via the
088: * {@link #afterInvocation(InterceptorStatusToken, Object)} method.</li>
089: * <li>If the <code>RunAsManager</code> replaced the
090: * <code>Authentication</code> object, return the
091: * <code>SecurityContextHolder</code> to the object that existed after the
092: * call to <code>AuthenticationManager</code>.</li>
093: * <li>If an <code>AfterInvocationManager</code> is defined, invoke the
094: * invocation manager and allow it to replace the object due to be returned to
095: * the caller.</li>
096: * </ol>
097: * </li>
098: * <li>For an invocation that is public (there is no
099: * <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: public abstract class AbstractSecurityInterceptor implements
120: InitializingBean, ApplicationEventPublisherAware,
121: MessageSourceAware {
122: // ~ Static fields/initializers
123: // =====================================================================================
124:
125: protected static final Log logger = LogFactory
126: .getLog(AbstractSecurityInterceptor.class);
127:
128: // ~ Instance fields
129: // ================================================================================================
130:
131: private AccessDecisionManager accessDecisionManager;
132:
133: private AfterInvocationManager afterInvocationManager;
134:
135: private ApplicationEventPublisher eventPublisher;
136:
137: private AuthenticationManager authenticationManager;
138:
139: protected MessageSourceAccessor messages = AcegiMessageSource
140: .getAccessor();
141:
142: private RunAsManager runAsManager = new NullRunAsManager();
143:
144: private boolean alwaysReauthenticate = false;
145:
146: private boolean rejectPublicInvocations = false;
147:
148: private boolean validateConfigAttributes = true;
149:
150: // ~ Methods
151: // ========================================================================================================
152:
153: /**
154: * Completes the work of the <code>AbstractSecurityInterceptor</code>
155: * after the secure object invocation has been complete
156: *
157: * @param token as returned by the {@link #beforeInvocation(Object)}}
158: * method
159: * @param returnedObject any object returned from the secure object
160: * invocation (may be<code>null</code>)
161: *
162: * @return the object the secure object invocation should ultimately return
163: * to its caller (may be <code>null</code>)
164: */
165: protected Object afterInvocation(InterceptorStatusToken token,
166: Object returnedObject) {
167: if (token == null) {
168: // public object
169: return returnedObject;
170: }
171:
172: if (token.isContextHolderRefreshRequired()) {
173: if (logger.isDebugEnabled()) {
174: logger.debug("Reverting to original Authentication: "
175: + token.getAuthentication().toString());
176: }
177:
178: SecurityContextHolder.getContext().setAuthentication(
179: token.getAuthentication());
180: }
181:
182: if (afterInvocationManager != null) {
183: // Attempt after invocation handling
184: try {
185: returnedObject = afterInvocationManager.decide(token
186: .getAuthentication(), token.getSecureObject(),
187: token.getAttr(), returnedObject);
188: } catch (AccessDeniedException accessDeniedException) {
189: AuthorizationFailureEvent event = new AuthorizationFailureEvent(
190: token.getSecureObject(), token.getAttr(), token
191: .getAuthentication(),
192: accessDeniedException);
193: publishEvent(event);
194:
195: throw accessDeniedException;
196: }
197: }
198:
199: return returnedObject;
200: }
201:
202: public void afterPropertiesSet() throws Exception {
203: Assert
204: .notNull(getSecureObjectClass(),
205: "Subclass must provide a non-null response to getSecureObjectClass()");
206:
207: Assert.notNull(this .messages, "A message source must be set");
208:
209: Assert.notNull(this .authenticationManager,
210: "An AuthenticationManager is required");
211:
212: Assert.notNull(this .accessDecisionManager,
213: "An AccessDecisionManager is required");
214:
215: Assert.notNull(this .runAsManager, "A RunAsManager is required");
216:
217: Assert.notNull(this .obtainObjectDefinitionSource(),
218: "An ObjectDefinitionSource is required");
219:
220: Assert.isTrue(this .obtainObjectDefinitionSource().supports(
221: getSecureObjectClass()),
222: "ObjectDefinitionSource does not support secure object class: "
223: + getSecureObjectClass());
224:
225: Assert.isTrue(this .runAsManager
226: .supports(getSecureObjectClass()),
227: "RunAsManager does not support secure object class: "
228: + getSecureObjectClass());
229:
230: Assert.isTrue(this .accessDecisionManager
231: .supports(getSecureObjectClass()),
232: "AccessDecisionManager does not support secure object class: "
233: + getSecureObjectClass());
234:
235: if (this .afterInvocationManager != null) {
236: Assert.isTrue(this .afterInvocationManager
237: .supports(getSecureObjectClass()),
238: "AfterInvocationManager does not support secure object class: "
239: + getSecureObjectClass());
240: }
241:
242: if (this .validateConfigAttributes) {
243: Iterator iter = this .obtainObjectDefinitionSource()
244: .getConfigAttributeDefinitions();
245:
246: if (iter == null) {
247: logger
248: .warn("Could not validate configuration attributes as the MethodDefinitionSource did not return "
249: + "a ConfigAttributeDefinition Iterator");
250: return;
251: }
252:
253: Set unsupportedAttrs = new HashSet();
254:
255: while (iter.hasNext()) {
256: ConfigAttributeDefinition def = (ConfigAttributeDefinition) iter
257: .next();
258: Iterator attributes = def.getConfigAttributes();
259:
260: while (attributes.hasNext()) {
261: ConfigAttribute attr = (ConfigAttribute) attributes
262: .next();
263:
264: if (!this .runAsManager.supports(attr)
265: && !this .accessDecisionManager
266: .supports(attr)
267: && ((this .afterInvocationManager == null) || !this .afterInvocationManager
268: .supports(attr))) {
269: unsupportedAttrs.add(attr);
270: }
271: }
272: }
273:
274: if (unsupportedAttrs.size() != 0) {
275: throw new IllegalArgumentException(
276: "Unsupported configuration attributes: "
277: + unsupportedAttrs);
278: }
279:
280: logger.info("Validated configuration attributes");
281: }
282: }
283:
284: protected InterceptorStatusToken beforeInvocation(Object object) {
285: Assert.notNull(object, "Object was null");
286:
287: if (!getSecureObjectClass().isAssignableFrom(object.getClass())) {
288: throw new IllegalArgumentException(
289: "Security invocation attempted for object "
290: + object.getClass().getName()
291: + " but AbstractSecurityInterceptor only configured to support secure objects of type: "
292: + getSecureObjectClass());
293: }
294:
295: ConfigAttributeDefinition attr = this
296: .obtainObjectDefinitionSource().getAttributes(object);
297:
298: if (attr == null) {
299: if (rejectPublicInvocations) {
300: throw new IllegalArgumentException(
301: "No public invocations are allowed via this AbstractSecurityInterceptor. "
302: + "This indicates a configuration error because the "
303: + "AbstractSecurityInterceptor.rejectPublicInvocations property is set to 'true'");
304: }
305:
306: if (logger.isDebugEnabled()) {
307: logger
308: .debug("Public object - authentication not attempted");
309: }
310:
311: publishEvent(new PublicInvocationEvent(object));
312:
313: return null; // no further work post-invocation
314: }
315:
316: if (logger.isDebugEnabled()) {
317: logger.debug("Secure object: " + object.toString()
318: + "; ConfigAttributes: " + attr.toString());
319: }
320:
321: if (SecurityContextHolder.getContext().getAuthentication() == null) {
322: credentialsNotFound(
323: messages
324: .getMessage(
325: "AbstractSecurityInterceptor.authenticationNotFound",
326: "An Authentication object was not found in the SecurityContext"),
327: object, attr);
328: }
329:
330: // Attempt authentication if not already authenticated, or user always
331: // wants reauthentication
332: Authentication authenticated;
333:
334: if (!SecurityContextHolder.getContext().getAuthentication()
335: .isAuthenticated()
336: || alwaysReauthenticate) {
337: try {
338: authenticated = this .authenticationManager
339: .authenticate(SecurityContextHolder
340: .getContext().getAuthentication());
341: } catch (AuthenticationException authenticationException) {
342: throw authenticationException;
343: }
344:
345: // We don't authenticated.setAuthentication(true), because each
346: // provider should do that
347: if (logger.isDebugEnabled()) {
348: logger.debug("Successfully Authenticated: "
349: + authenticated.toString());
350: }
351:
352: SecurityContextHolder.getContext().setAuthentication(
353: authenticated);
354: } else {
355: authenticated = SecurityContextHolder.getContext()
356: .getAuthentication();
357:
358: if (logger.isDebugEnabled()) {
359: logger.debug("Previously Authenticated: "
360: + authenticated.toString());
361: }
362: }
363:
364: // Attempt authorization
365: try {
366: this .accessDecisionManager.decide(authenticated, object,
367: attr);
368: } catch (AccessDeniedException accessDeniedException) {
369: AuthorizationFailureEvent event = new AuthorizationFailureEvent(
370: object, attr, authenticated, accessDeniedException);
371: publishEvent(event);
372:
373: throw accessDeniedException;
374: }
375:
376: if (logger.isDebugEnabled()) {
377: logger.debug("Authorization successful");
378: }
379:
380: AuthorizedEvent event = new AuthorizedEvent(object, attr,
381: authenticated);
382: publishEvent(event);
383:
384: // Attempt to run as a different user
385: Authentication runAs = this .runAsManager.buildRunAs(
386: authenticated, object, attr);
387:
388: if (runAs == null) {
389: if (logger.isDebugEnabled()) {
390: logger
391: .debug("RunAsManager did not change Authentication object");
392: }
393:
394: // no further work post-invocation
395: return new InterceptorStatusToken(authenticated, false,
396: attr, object);
397: } else {
398: if (logger.isDebugEnabled()) {
399: logger.debug("Switching to RunAs Authentication: "
400: + runAs.toString());
401: }
402:
403: SecurityContextHolder.getContext().setAuthentication(runAs);
404:
405: // revert to token.Authenticated post-invocation
406: return new InterceptorStatusToken(authenticated, true,
407: attr, object);
408: }
409: }
410:
411: /**
412: * Helper method which generates an exception containing the passed reason,
413: * and publishes an event to the application context.
414: * <p>
415: * Always throws an exception.
416: * </p>
417: *
418: * @param reason to be provided in the exception detail
419: * @param secureObject that was being called
420: * @param configAttribs that were defined for the secureObject
421: */
422: private void credentialsNotFound(String reason,
423: Object secureObject, ConfigAttributeDefinition configAttribs) {
424: AuthenticationCredentialsNotFoundException exception = new AuthenticationCredentialsNotFoundException(
425: reason);
426:
427: AuthenticationCredentialsNotFoundEvent event = new AuthenticationCredentialsNotFoundEvent(
428: secureObject, configAttribs, exception);
429: publishEvent(event);
430:
431: throw exception;
432: }
433:
434: public AccessDecisionManager getAccessDecisionManager() {
435: return accessDecisionManager;
436: }
437:
438: public AfterInvocationManager getAfterInvocationManager() {
439: return afterInvocationManager;
440: }
441:
442: public AuthenticationManager getAuthenticationManager() {
443: return this .authenticationManager;
444: }
445:
446: public RunAsManager getRunAsManager() {
447: return runAsManager;
448: }
449:
450: /**
451: * Indicates the type of secure objects the subclass will be presenting to
452: * the abstract parent for processing. This is used to ensure collaborators
453: * wired to the <code>AbstractSecurityInterceptor</code> all support the
454: * indicated secure object class.
455: *
456: * @return the type of secure object the subclass provides services for
457: */
458: public abstract Class getSecureObjectClass();
459:
460: public boolean isAlwaysReauthenticate() {
461: return alwaysReauthenticate;
462: }
463:
464: public boolean isRejectPublicInvocations() {
465: return rejectPublicInvocations;
466: }
467:
468: public boolean isValidateConfigAttributes() {
469: return validateConfigAttributes;
470: }
471:
472: public abstract ObjectDefinitionSource obtainObjectDefinitionSource();
473:
474: public void setAccessDecisionManager(
475: AccessDecisionManager accessDecisionManager) {
476: this .accessDecisionManager = accessDecisionManager;
477: }
478:
479: public void setAfterInvocationManager(
480: AfterInvocationManager afterInvocationManager) {
481: this .afterInvocationManager = afterInvocationManager;
482: }
483:
484: /**
485: * Indicates whether the <code>AbstractSecurityInterceptor</code> should
486: * ignore the {@link Authentication#isAuthenticated()} property. Defaults to
487: * <code>false</code>, meaning by default the
488: * <code>Authentication.isAuthenticated()</code> property is trusted and
489: * re-authentication will not occur if the principal has already been
490: * authenticated.
491: *
492: * @param alwaysReauthenticate <code>true</code> to force
493: * <code>AbstractSecurityInterceptor</code> to disregard the value of
494: * <code>Authentication.isAuthenticated()</code> and always
495: * re-authenticate the request (defaults to <code>false</code>).
496: */
497: public void setAlwaysReauthenticate(boolean alwaysReauthenticate) {
498: this .alwaysReauthenticate = alwaysReauthenticate;
499: }
500:
501: public void setApplicationEventPublisher(
502: ApplicationEventPublisher applicationEventPublisher) {
503: this .eventPublisher = applicationEventPublisher;
504: }
505:
506: public void setAuthenticationManager(
507: AuthenticationManager newManager) {
508: this .authenticationManager = newManager;
509: }
510:
511: public void setMessageSource(MessageSource messageSource) {
512: this .messages = new MessageSourceAccessor(messageSource);
513: }
514:
515: /**
516: * By rejecting public invocations (and setting this property to
517: * <code>true</code>), essentially you are ensuring that every secure
518: * object invocation advised by <code>AbstractSecurityInterceptor</code>
519: * has a configuration attribute defined. This is useful to ensure a "fail
520: * safe" mode where undeclared secure objects will be rejected and
521: * configuration omissions detected early. An
522: * <code>IllegalArgumentException</code> will be thrown by the
523: * <code>AbstractSecurityInterceptor</code> if you set this property to
524: * <code>true</code> and an attempt is made to invoke a secure object that
525: * has no configuration attributes.
526: *
527: * @param rejectPublicInvocations set to <code>true</code> to reject
528: * invocations of secure objects that have no configuration attributes (by
529: * default it is <code>false</code> which treats undeclared secure objects
530: * as "public" or unauthorized)
531: */
532: public void setRejectPublicInvocations(
533: boolean rejectPublicInvocations) {
534: this .rejectPublicInvocations = rejectPublicInvocations;
535: }
536:
537: public void setRunAsManager(RunAsManager runAsManager) {
538: this .runAsManager = runAsManager;
539: }
540:
541: public void setValidateConfigAttributes(
542: boolean validateConfigAttributes) {
543: this .validateConfigAttributes = validateConfigAttributes;
544: }
545:
546: private void publishEvent(ApplicationEvent event) {
547: if (this.eventPublisher != null) {
548: this.eventPublisher.publishEvent(event);
549: }
550: }
551: }
|