diff --git a/core/src/main/java/org/acegisecurity/AfterInvocationManager.java b/core/src/main/java/org/acegisecurity/AfterInvocationManager.java new file mode 100644 index 0000000000..a4ffde4447 --- /dev/null +++ b/core/src/main/java/org/acegisecurity/AfterInvocationManager.java @@ -0,0 +1,103 @@ +/* Copyright 2004 Acegi Technology Pty Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.sf.acegisecurity; + +/** + * Reviews the Object returned from a secure object invocation, + * being able to modify the Object or throw an {@link + * AccessDeniedException}. + * + *

+ * Typically used to ensure the principal is permitted to access the domain + * object instance returned by a service layer bean. Can also be used to + * mutate the domain object instance so the principal is only able to access + * authorised bean properties or Collection elements. Often used + * in conjunction with an {@link net.sf.acegisecurity.acl.AclManager} to + * obtain the access control list applicable for the domain object instance. + *

+ * + *

+ * Special consideration should be given to using an + * AfterInvocationManager on bean methods that modify a database. + * Typically an AfterInvocationManager is used with read-only + * methods, such as public DomainObject getById(id). If used with + * methods that modify a database, a transaction manager should be used to + * ensure any AccessDeniedException will cause a rollback of the + * changes made by the transaction. + *

+ * + * @author Ben Alex + * @version $Id$ + */ +public interface AfterInvocationManager { + //~ Methods ================================================================ + + /** + * Given the details of a secure object invocation including its returned + * Object, make an access control decision or optionally + * modify the returned Object. + * + * @param authentication the caller that invoked the method + * @param object the secured object that was called + * @param config the configuration attributes associated with the secured + * object that was invoked + * @param returnedObject the Object that was returned from the + * secure object invocation + * + * @return the Object that will ultimately be returned to the + * caller (if an implementation does not wish to modify the object + * to be returned to the caller, the implementation should simply + * return the same object it was passed by the + * returnedObject method argument) + * + * @throws AccessDeniedException if access is denied + */ + public Object decide(Authentication authentication, Object object, + ConfigAttributeDefinition config, Object returnedObject) + throws AccessDeniedException; + + /** + * Indicates whether this AfterInvocationManager is able to + * process "after invocation" requests presented with the passed + * ConfigAttribute. + * + *

+ * This allows the AbstractSecurityInterceptor to check every + * configuration attribute can be consumed by the configured + * AccessDecisionManager and/or RunAsManager + * and/or AfterInvocationManager. + *

+ * + * @param attribute a configuration attribute that has been configured + * against the AbstractSecurityInterceptor + * + * @return true if this AfterInvocationManager can support the + * passed configuration attribute + */ + public boolean supports(ConfigAttribute attribute); + + /** + * Indicates whether the AfterInvocationManager implementation + * is able to provide access control decisions for the indicated secured + * object type. + * + * @param clazz the class that is being queried + * + * @return true if the implementation can process the + * indicated class + */ + public boolean supports(Class clazz); +} diff --git a/core/src/main/java/org/acegisecurity/intercept/AbstractSecurityInterceptor.java b/core/src/main/java/org/acegisecurity/intercept/AbstractSecurityInterceptor.java index 5711ade986..384414b296 100644 --- a/core/src/main/java/org/acegisecurity/intercept/AbstractSecurityInterceptor.java +++ b/core/src/main/java/org/acegisecurity/intercept/AbstractSecurityInterceptor.java @@ -17,6 +17,7 @@ package net.sf.acegisecurity.intercept; import net.sf.acegisecurity.AccessDecisionManager; import net.sf.acegisecurity.AccessDeniedException; +import net.sf.acegisecurity.AfterInvocationManager; import net.sf.acegisecurity.Authentication; import net.sf.acegisecurity.AuthenticationCredentialsNotFoundException; import net.sf.acegisecurity.AuthenticationException; @@ -74,7 +75,7 @@ import java.util.Set; * For an invocation that is secured (there is a * ConfigAttributeDefinition for the secure object invocation): * - *
    + *
      *
    1. * Authenticate the request against the configured {@link * AuthenticationManager}, replacing the Authentication object on @@ -103,6 +104,11 @@ import java.util.Set; * object, return the ContextHolder to the object that existed * after the call to AuthenticationManager. *
    2. + *
    3. + * If an AfterInvocationManager is defined, invoke the invocation + * manager and allow it to replace the object due to be returned to the + * caller. + *
    4. *
    * * @@ -110,7 +116,7 @@ import java.util.Set; * For an invocation that is public (there is no * ConfigAttributeDefinition for the secure object invocation): * - *
      + *
        *
      1. * If the ContextHolder contains a SecureContext, set * the isAuthenticated flag on the Authentication @@ -128,9 +134,9 @@ import java.util.Set; * *
      2. *
      3. - * Control again returns to the concrete subclass, which will return to the - * caller any result or exception that occurred when it proceeded with the - * execution of the secure object. + * Control again returns to the concrete subclass, along with the + * Object that should be returned to the caller. The subclass + * will then return that result or exception to the original caller. *
      4. *
      *

      @@ -147,6 +153,7 @@ public abstract class AbstractSecurityInterceptor implements InitializingBean, //~ Instance fields ======================================================== private AccessDecisionManager accessDecisionManager; + private AfterInvocationManager afterInvocationManager; private ApplicationContext context; private AuthenticationManager authenticationManager; private RunAsManager runAsManager = new NullRunAsManager(); @@ -154,11 +161,30 @@ public abstract class AbstractSecurityInterceptor implements InitializingBean, //~ Methods ================================================================ + public void setAfterInvocationManager( + AfterInvocationManager afterInvocationManager) { + this.afterInvocationManager = afterInvocationManager; + } + + public AfterInvocationManager getAfterInvocationManager() { + return afterInvocationManager; + } + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.context = applicationContext; } + /** + * Indicates the type of secure objects the subclass will be presenting to + * the abstract parent for processing. This is used to ensure + * collaborators wired to the AbstractSecurityInterceptor all + * support the indicated secure object class. + * + * @return the type of secure object the subclass provides services for + */ + public abstract Class getSecureObjectClass(); + public abstract ObjectDefinitionSource obtainObjectDefinitionSource(); public void setAccessDecisionManager( @@ -223,51 +249,116 @@ public abstract class AbstractSecurityInterceptor implements InitializingBean, logger.warn( "Could not validate configuration attributes as the MethodDefinitionSource did not return a ConfigAttributeDefinition Iterator"); } + } else { + Set set = new HashSet(); + + while (iter.hasNext()) { + ConfigAttributeDefinition def = (ConfigAttributeDefinition) iter + .next(); + Iterator attributes = def.getConfigAttributes(); + + while (attributes.hasNext()) { + ConfigAttribute attr = (ConfigAttribute) attributes + .next(); + + if (!this.runAsManager.supports(attr) + && !this.accessDecisionManager.supports(attr) + && ((this.afterInvocationManager == null) + || !this.afterInvocationManager.supports(attr))) { + set.add(attr); + } + } + } - return; + if (set.size() == 0) { + if (logger.isInfoEnabled()) { + logger.info("Validated configuration attributes"); + } + } else { + throw new IllegalArgumentException( + "Unsupported configuration attributes: " + + set.toString()); + } } + } - Set set = new HashSet(); + if (getSecureObjectClass() == null) { + throw new IllegalArgumentException( + "Subclass must provide a non-null response to getSecureObjectClass()"); + } - while (iter.hasNext()) { - ConfigAttributeDefinition def = (ConfigAttributeDefinition) iter - .next(); - Iterator attributes = def.getConfigAttributes(); + if (!this.accessDecisionManager.supports(getSecureObjectClass())) { + throw new IllegalArgumentException( + "AccessDecisionManager does not support secure object class: " + + getSecureObjectClass()); + } - while (attributes.hasNext()) { - ConfigAttribute attr = (ConfigAttribute) attributes.next(); + boolean result = this.obtainObjectDefinitionSource().supports(getSecureObjectClass()); - if (!this.runAsManager.supports(attr) - && !this.accessDecisionManager.supports(attr)) { - set.add(attr); - } - } - } + if (!result) { + throw new IllegalArgumentException( + "ObjectDefinitionSource does not support secure object class: " + + getSecureObjectClass()); + } - if (set.size() == 0) { - if (logger.isInfoEnabled()) { - logger.info("Validated configuration attributes"); - } - } else { - throw new IllegalArgumentException( - "Unsupported configuration attributes: " + set.toString()); - } + if (!this.runAsManager.supports(getSecureObjectClass())) { + throw new IllegalArgumentException( + "RunAsManager does not support secure object class: " + + getSecureObjectClass()); + } + + if ((this.afterInvocationManager != null) + && !this.afterInvocationManager.supports(getSecureObjectClass())) { + throw new IllegalArgumentException( + "AfterInvocationManager does not support secure object class: " + + getSecureObjectClass()); + } + + if (!this.obtainObjectDefinitionSource().supports(getSecureObjectClass())) { + throw new IllegalArgumentException( + "ObjectDefinitionSource does not support secure object class: " + + getSecureObjectClass()); } } - protected void afterInvocation(InterceptorStatusToken token) { + /** + * Completes the work of the AbstractSecurityInterceptor after + * the secure object invocation has been complete + * + * @param token as returned by the {@link #beforeInvocation(Object)}} + * method + * @param returnedObject any object returned from the secure object + * invocation (may benull) + * + * @return the object the secure object invocation should ultimately return + * to its caller (may be null) + */ + protected Object afterInvocation(InterceptorStatusToken token, + Object returnedObject) { if (token == null) { - return; + // public object + return returnedObject; } - if (logger.isDebugEnabled()) { - logger.debug("Reverting to original Authentication: " - + token.getAuthenticated().toString()); + if (token.isContextHolderRefreshRequired()) { + if (logger.isDebugEnabled()) { + logger.debug("Reverting to original Authentication: " + + token.getAuthentication().toString()); + } + + SecureContext secureContext = (SecureContext) ContextHolder + .getContext(); + secureContext.setAuthentication(token.getAuthentication()); + ContextHolder.setContext(secureContext); } - SecureContext secureContext = (SecureContext) ContextHolder.getContext(); - secureContext.setAuthentication(token.getAuthenticated()); - ContextHolder.setContext(secureContext); + if (afterInvocationManager != null) { + returnedObject = afterInvocationManager.decide(token + .getAuthentication(), token.getSecureObject(), + token.getAttr(), returnedObject); + } + + return returnedObject; } protected InterceptorStatusToken beforeInvocation(Object object) { @@ -275,10 +366,11 @@ public abstract class AbstractSecurityInterceptor implements InitializingBean, throw new IllegalArgumentException("Object was null"); } - if (!this.obtainObjectDefinitionSource().supports(object.getClass())) { + if (!getSecureObjectClass().isAssignableFrom(object.getClass())) { throw new IllegalArgumentException( - "ObjectDefinitionSource does not support objects of type " - + object.getClass()); + "Security invocation attempted for object " + object + + " but AbstractSecurityInterceptor only configured to support secure objects of type: " + + getSecureObjectClass()); } ConfigAttributeDefinition attr = this.obtainObjectDefinitionSource() @@ -365,7 +457,8 @@ public abstract class AbstractSecurityInterceptor implements InitializingBean, "RunAsManager did not change Authentication object"); } - return null; // no further work post-invocation + return new InterceptorStatusToken(authenticated, false, attr, + object); // no further work post-invocation } else { if (logger.isDebugEnabled()) { logger.debug("Switching to RunAs Authentication: " @@ -375,10 +468,8 @@ public abstract class AbstractSecurityInterceptor implements InitializingBean, context.setAuthentication(runAs); ContextHolder.setContext((Context) context); - InterceptorStatusToken token = new InterceptorStatusToken(); - token.setAuthenticated(authenticated); - - return token; // revert to token.Authenticated post-invocation + return new InterceptorStatusToken(authenticated, true, attr, + object); // revert to token.Authenticated post-invocation } } else { if (logger.isDebugEnabled()) { diff --git a/core/src/main/java/org/acegisecurity/intercept/InterceptorStatusToken.java b/core/src/main/java/org/acegisecurity/intercept/InterceptorStatusToken.java index 0d73384dde..95eda0e501 100644 --- a/core/src/main/java/org/acegisecurity/intercept/InterceptorStatusToken.java +++ b/core/src/main/java/org/acegisecurity/intercept/InterceptorStatusToken.java @@ -16,6 +16,7 @@ package net.sf.acegisecurity.intercept; import net.sf.acegisecurity.Authentication; +import net.sf.acegisecurity.ConfigAttributeDefinition; /** @@ -23,14 +24,9 @@ import net.sf.acegisecurity.Authentication; * *

      * This class reflects the status of the security interception, so that the - * final call to AbstractSecurityInterceptor can tidy up - * correctly. - *

      - * - *

      - * Whilst this class currently only wraps a single object, it has been modelled - * as a class so that future changes to the operation of - * AbstractSecurityInterceptor are abstracted from subclasses. + * final call to {@link + * net.sf.acegisecurity.intercept.AbstractSecurityInterceptor#afterInvocation(InterceptorStatusToken, + * Object)} can tidy up correctly. *

      * * @author Ben Alex @@ -39,15 +35,41 @@ import net.sf.acegisecurity.Authentication; public class InterceptorStatusToken { //~ Instance fields ======================================================== - private Authentication authenticated; + private Authentication authentication; + private ConfigAttributeDefinition attr; + private Object secureObject; + private boolean contextHolderRefreshRequired; + + //~ Constructors =========================================================== + + public InterceptorStatusToken(Authentication authentication, + boolean contextHolderRefreshRequired, ConfigAttributeDefinition attr, + Object secureObject) { + this.authentication = authentication; + this.contextHolderRefreshRequired = contextHolderRefreshRequired; + this.attr = attr; + this.secureObject = secureObject; + } + + protected InterceptorStatusToken() { + throw new IllegalArgumentException("Cannot use default constructor"); + } //~ Methods ================================================================ - public void setAuthenticated(Authentication authenticated) { - this.authenticated = authenticated; + public ConfigAttributeDefinition getAttr() { + return attr; + } + + public Authentication getAuthentication() { + return authentication; + } + + public boolean isContextHolderRefreshRequired() { + return contextHolderRefreshRequired; } - public Authentication getAuthenticated() { - return authenticated; + public Object getSecureObject() { + return secureObject; } } diff --git a/core/src/main/java/org/acegisecurity/intercept/method/aopalliance/MethodSecurityInterceptor.java b/core/src/main/java/org/acegisecurity/intercept/method/aopalliance/MethodSecurityInterceptor.java index f0a4a3ccce..f6460b76b4 100644 --- a/core/src/main/java/org/acegisecurity/intercept/method/aopalliance/MethodSecurityInterceptor.java +++ b/core/src/main/java/org/acegisecurity/intercept/method/aopalliance/MethodSecurityInterceptor.java @@ -58,18 +58,8 @@ public class MethodSecurityInterceptor extends AbstractSecurityInterceptor return this.objectDefinitionSource; } - public void afterPropertiesSet() { - super.afterPropertiesSet(); - - if (!this.getAccessDecisionManager().supports(MethodInvocation.class)) { - throw new IllegalArgumentException( - "AccessDecisionManager does not support MethodInvocation"); - } - - if (!this.getRunAsManager().supports(MethodInvocation.class)) { - throw new IllegalArgumentException( - "RunAsManager does not support MethodInvocation"); - } + public Class getSecureObjectClass() { + return MethodInvocation.class; } /** @@ -83,13 +73,13 @@ public class MethodSecurityInterceptor extends AbstractSecurityInterceptor * @throws Throwable if any error occurs */ public Object invoke(MethodInvocation mi) throws Throwable { - Object result; + Object result = null; InterceptorStatusToken token = super.beforeInvocation(mi); try { result = mi.proceed(); } finally { - super.afterInvocation(token); + result = super.afterInvocation(token, result); } return result; diff --git a/core/src/main/java/org/acegisecurity/intercept/method/aspectj/AspectJSecurityInterceptor.java b/core/src/main/java/org/acegisecurity/intercept/method/aspectj/AspectJSecurityInterceptor.java index c325de320f..ce9c3e7796 100644 --- a/core/src/main/java/org/acegisecurity/intercept/method/aspectj/AspectJSecurityInterceptor.java +++ b/core/src/main/java/org/acegisecurity/intercept/method/aspectj/AspectJSecurityInterceptor.java @@ -35,7 +35,7 @@ import org.aspectj.lang.JoinPoint; *

      * *

      - * The secure object type is org.aspectj.lang.JointPoint, which is + * The secure object type is org.aspectj.lang.JoinPoint, which is * passed from the relevant around() advice. The * around() advice also passes an anonymous implementation of * {@link AspectJCallback} which contains the call for AspectJ to continue @@ -64,18 +64,8 @@ public class AspectJSecurityInterceptor extends AbstractSecurityInterceptor { return this.objectDefinitionSource; } - public void afterPropertiesSet() { - super.afterPropertiesSet(); - - if (!this.getAccessDecisionManager().supports(JoinPoint.class)) { - throw new IllegalArgumentException( - "AccessDecisionManager does not support JointPoint"); - } - - if (!this.getRunAsManager().supports(JoinPoint.class)) { - throw new IllegalArgumentException( - "RunAsManager does not support JointPoint"); - } + public Class getSecureObjectClass() { + return JoinPoint.class; } /** @@ -91,13 +81,13 @@ public class AspectJSecurityInterceptor extends AbstractSecurityInterceptor { * @return The returned value from the method invocation */ public Object invoke(JoinPoint jp, AspectJCallback advisorProceed) { - Object result; + Object result = null; InterceptorStatusToken token = super.beforeInvocation(jp); try { result = advisorProceed.proceedWithObject(); } finally { - super.afterInvocation(token); + result = super.afterInvocation(token, result); } return result; diff --git a/core/src/main/java/org/acegisecurity/intercept/web/FilterSecurityInterceptor.java b/core/src/main/java/org/acegisecurity/intercept/web/FilterSecurityInterceptor.java index c54b00314a..e288a651d7 100644 --- a/core/src/main/java/org/acegisecurity/intercept/web/FilterSecurityInterceptor.java +++ b/core/src/main/java/org/acegisecurity/intercept/web/FilterSecurityInterceptor.java @@ -59,18 +59,8 @@ public class FilterSecurityInterceptor extends AbstractSecurityInterceptor { return this.objectDefinitionSource; } - public void afterPropertiesSet() { - super.afterPropertiesSet(); - - if (!this.getAccessDecisionManager().supports(FilterInvocation.class)) { - throw new IllegalArgumentException( - "AccessDecisionManager does not support FilterInvocation"); - } - - if (!this.getRunAsManager().supports(FilterInvocation.class)) { - throw new IllegalArgumentException( - "RunAsManager does not support FilterInvocation"); - } + public Class getSecureObjectClass() { + return FilterInvocation.class; } public void invoke(FilterInvocation fi) throws Throwable { @@ -79,7 +69,7 @@ public class FilterSecurityInterceptor extends AbstractSecurityInterceptor { try { fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); } finally { - super.afterInvocation(token); + super.afterInvocation(token, null); } }