Browse Source

SEC-1635: Stop security interceptors from calling AfterInvocationManager if exception occurs during invocation

Luke Taylor 14 years ago
parent
commit
ce421f22bf

+ 3 - 8
core/src/main/java/org/springframework/security/access/intercept/aopalliance/MethodSecurityInterceptor.java

@@ -51,21 +51,16 @@ public class MethodSecurityInterceptor extends AbstractSecurityInterceptor imple
      *
      *
      * @param mi The method being invoked which requires a security decision
      * @param mi The method being invoked which requires a security decision
      *
      *
-     * @return The returned value from the method invocation
+     * @return The returned value from the method invocation (possibly modified by the {@code AfterInvocationManager}).
      *
      *
      * @throws Throwable if any error occurs
      * @throws Throwable if any error occurs
      */
      */
     public Object invoke(MethodInvocation mi) throws Throwable {
     public Object invoke(MethodInvocation mi) throws Throwable {
-        Object result = null;
         InterceptorStatusToken token = super.beforeInvocation(mi);
         InterceptorStatusToken token = super.beforeInvocation(mi);
 
 
-        try {
-            result = mi.proceed();
-        } finally {
-            result = super.afterInvocation(token, result);
-        }
+        Object result = mi.proceed();
 
 
-        return result;
+        return super.afterInvocation(token, result);
     }
     }
 
 
     public MethodSecurityMetadataSource getSecurityMetadataSource() {
     public MethodSecurityMetadataSource getSecurityMetadataSource() {

+ 2 - 7
core/src/main/java/org/springframework/security/access/intercept/aspectj/AspectJMethodSecurityInterceptor.java

@@ -37,15 +37,10 @@ public final class AspectJMethodSecurityInterceptor extends MethodSecurityInterc
      * @return The returned value from the method invocation
      * @return The returned value from the method invocation
      */
      */
     public Object invoke(JoinPoint jp, AspectJCallback advisorProceed) {
     public Object invoke(JoinPoint jp, AspectJCallback advisorProceed) {
-        Object result = null;
         InterceptorStatusToken token = super.beforeInvocation(new MethodInvocationAdapter(jp));
         InterceptorStatusToken token = super.beforeInvocation(new MethodInvocationAdapter(jp));
 
 
-        try {
-            result = advisorProceed.proceedWithObject();
-        } finally {
-            result = super.afterInvocation(token, result);
-        }
+        Object result = advisorProceed.proceedWithObject();
 
 
-        return result;
+        return super.afterInvocation(token, result);
     }
     }
 }
 }

+ 21 - 0
core/src/test/java/org/springframework/security/access/intercept/aopalliance/MethodSecurityInterceptorTests.java

@@ -259,6 +259,27 @@ public class MethodSecurityInterceptorTests {
         advisedTarget.makeUpperCase("hello");
         advisedTarget.makeUpperCase("hello");
     }
     }
 
 
+    @Test
+    public void afterInvocationManagerIsNotInvokedIfExceptionIsRaised() throws Throwable {
+        MethodInvocation mi = mock(MethodInvocation.class);
+        token.setAuthenticated(true);
+        SecurityContextHolder.getContext().setAuthentication(token);
+        mdsReturnsUserRole();
+
+        AfterInvocationManager aim = mock(AfterInvocationManager.class);
+        interceptor.setAfterInvocationManager(aim);
+
+        when(mi.proceed()).thenThrow(new Throwable());
+
+        try {
+            interceptor.invoke(mi);
+            fail("Expected exception");
+        } catch (Throwable expected) {
+        }
+
+        verifyZeroInteractions(aim);
+    }
+
     void mdsReturnsNull() {
     void mdsReturnsNull() {
         when(mds.getAttributes(any(MethodInvocation.class))).thenReturn(null);
         when(mds.getAttributes(any(MethodInvocation.class))).thenReturn(null);
     }
     }

+ 22 - 0
core/src/test/java/org/springframework/security/access/intercept/aspectj/AspectJMethodSecurityInterceptorTests.java

@@ -19,6 +19,7 @@ import static org.junit.Assert.*;
 import static org.mockito.Matchers.any;
 import static org.mockito.Matchers.any;
 import static org.mockito.Mockito.*;
 import static org.mockito.Mockito.*;
 
 
+import org.aopalliance.intercept.MethodInvocation;
 import org.aspectj.lang.JoinPoint;
 import org.aspectj.lang.JoinPoint;
 import org.aspectj.lang.ProceedingJoinPoint;
 import org.aspectj.lang.ProceedingJoinPoint;
 import org.aspectj.lang.Signature;
 import org.aspectj.lang.Signature;
@@ -32,6 +33,7 @@ import org.springframework.security.TargetObject;
 import org.springframework.security.access.AccessDecisionManager;
 import org.springframework.security.access.AccessDecisionManager;
 import org.springframework.security.access.AccessDeniedException;
 import org.springframework.security.access.AccessDeniedException;
 import org.springframework.security.access.SecurityConfig;
 import org.springframework.security.access.SecurityConfig;
+import org.springframework.security.access.intercept.AfterInvocationManager;
 import org.springframework.security.access.method.MethodSecurityMetadataSource;
 import org.springframework.security.access.method.MethodSecurityMetadataSource;
 import org.springframework.security.authentication.AuthenticationManager;
 import org.springframework.security.authentication.AuthenticationManager;
 import org.springframework.security.authentication.TestingAuthenticationToken;
 import org.springframework.security.authentication.TestingAuthenticationToken;
@@ -129,4 +131,24 @@ public class AspectJMethodSecurityInterceptorTests {
         assertEquals(m, mia.getMethod());
         assertEquals(m, mia.getMethod());
         assertSame(to, mia.getThis());
         assertSame(to, mia.getThis());
     }
     }
+
+    @Test
+    public void afterInvocationManagerIsNotInvokedIfExceptionIsRaised() throws Throwable {
+        token.setAuthenticated(true);
+        SecurityContextHolder.getContext().setAuthentication(token);
+
+        AfterInvocationManager aim = mock(AfterInvocationManager.class);
+        interceptor.setAfterInvocationManager(aim);
+
+        when(aspectJCallback.proceedWithObject()).thenThrow(new RuntimeException());
+
+        try {
+            interceptor.invoke(joinPoint, aspectJCallback);
+            fail("Expected exception");
+        } catch (RuntimeException expected) {
+        }
+
+        verifyZeroInteractions(aim);
+    }
+
 }
 }

+ 8 - 3
docs/manual/src/docbook/technical-overview.xml

@@ -551,7 +551,9 @@ Successfully authenticated. Security context contains: \
                 </listitem>
                 </listitem>
                 <listitem>
                 <listitem>
                     <para>Call the <interfacename>AfterInvocationManager</interfacename> if
                     <para>Call the <interfacename>AfterInvocationManager</interfacename> if
-                        configured, once the invocation has returned.</para>
+                        configured, once the invocation has returned. If the invocation raised an
+                        exception, the <interfacename>AfterInvocationManager</interfacename>
+                        will not be invoked.</para>
                 </listitem>
                 </listitem>
                 </orderedlist></para>
                 </orderedlist></para>
             <section xml:id="tech-intro-config-attributes">
             <section xml:id="tech-intro-config-attributes">
@@ -602,7 +604,7 @@ Successfully authenticated. Security context contains: \
             </section>
             </section>
             <section>
             <section>
                 <title>AfterInvocationManager</title>
                 <title>AfterInvocationManager</title>
-                <para>Following the secure object proceeding and then returning - which may mean a
+                <para>Following the secure object invocation proceeding and then returning - which may mean a
                     method invocation completing or a filter chain proceeding - the
                     method invocation completing or a filter chain proceeding - the
                     <classname>AbstractSecurityInterceptor</classname> gets one final chance to
                     <classname>AbstractSecurityInterceptor</classname> gets one final chance to
                     handle the invocation. At this stage the
                     handle the invocation. At this stage the
@@ -613,7 +615,10 @@ Successfully authenticated. Security context contains: \
                     <classname>AbstractSecurityInterceptor</classname> will pass control to an
                     <classname>AbstractSecurityInterceptor</classname> will pass control to an
                     <literal>AfterInvocationManager</literal> to actually modify the object if
                     <literal>AfterInvocationManager</literal> to actually modify the object if
                     needed. This class can even entirely replace the object, or throw an exception,
                     needed. This class can even entirely replace the object, or throw an exception,
-                    or not change it in any way as it chooses.</para>
+                    or not change it in any way as it chooses. The after-invocation checks will only
+                    be executed if the invocation is successful. If an exception occurs, the additional
+                    checks will be skipped.
+                </para>
                 <para><classname>AbstractSecurityInterceptor</classname> and its related objects are
                 <para><classname>AbstractSecurityInterceptor</classname> and its related objects are
                     shown in <xref linkend="abstract-security-interceptor"/>. <figure
                     shown in <xref linkend="abstract-security-interceptor"/>. <figure
                     xml:id="abstract-security-interceptor">
                     xml:id="abstract-security-interceptor">

+ 3 - 5
web/src/main/java/org/springframework/security/web/access/intercept/FilterSecurityInterceptor.java

@@ -113,11 +113,9 @@ public class FilterSecurityInterceptor extends AbstractSecurityInterceptor imple
 
 
             InterceptorStatusToken token = super.beforeInvocation(fi);
             InterceptorStatusToken token = super.beforeInvocation(fi);
 
 
-            try {
-                fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
-            } finally {
-                super.afterInvocation(token, null);
-            }
+            fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
+
+            super.afterInvocation(token, null);
         }
         }
     }
     }
 
 

+ 46 - 18
web/src/test/java/org/springframework/security/web/access/intercept/FilterSecurityInterceptorTests.java

@@ -15,22 +15,18 @@
 
 
 package org.springframework.security.web.access.intercept;
 package org.springframework.security.web.access.intercept;
 
 
+import static org.junit.Assert.fail;
 import static org.mockito.Matchers.any;
 import static org.mockito.Matchers.any;
 import static org.mockito.Mockito.*;
 import static org.mockito.Mockito.*;
 
 
-import java.util.*;
-import javax.servlet.FilterChain;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
+import org.junit.*;
 import org.springframework.context.ApplicationEventPublisher;
 import org.springframework.context.ApplicationEventPublisher;
 import org.springframework.mock.web.MockHttpServletRequest;
 import org.springframework.mock.web.MockHttpServletRequest;
 import org.springframework.mock.web.MockHttpServletResponse;
 import org.springframework.mock.web.MockHttpServletResponse;
 import org.springframework.security.access.AccessDecisionManager;
 import org.springframework.security.access.AccessDecisionManager;
-import org.springframework.security.access.ConfigAttribute;
 import org.springframework.security.access.SecurityConfig;
 import org.springframework.security.access.SecurityConfig;
 import org.springframework.security.access.event.AuthorizedEvent;
 import org.springframework.security.access.event.AuthorizedEvent;
+import org.springframework.security.access.intercept.AfterInvocationManager;
 import org.springframework.security.access.intercept.RunAsManager;
 import org.springframework.security.access.intercept.RunAsManager;
 import org.springframework.security.authentication.AuthenticationManager;
 import org.springframework.security.authentication.AuthenticationManager;
 import org.springframework.security.authentication.TestingAuthenticationToken;
 import org.springframework.security.authentication.TestingAuthenticationToken;
@@ -38,6 +34,10 @@ import org.springframework.security.core.Authentication;
 import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.security.web.FilterInvocation;
 import org.springframework.security.web.FilterInvocation;
 
 
+import javax.servlet.FilterChain;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
 
 
 /**
 /**
  * Tests {@link FilterSecurityInterceptor}.
  * Tests {@link FilterSecurityInterceptor}.
@@ -95,23 +95,51 @@ public class FilterSecurityInterceptorTests {
      */
      */
     @Test
     @Test
     public void testSuccessfulInvocation() throws Throwable {
     public void testSuccessfulInvocation() throws Throwable {
-        final MockHttpServletResponse response = new MockHttpServletResponse();
-        final MockHttpServletRequest request = new MockHttpServletRequest();
-        request.setServletPath("/secure/page.html");
-
         // Setup a Context
         // Setup a Context
-        final Authentication token = new TestingAuthenticationToken("Test", "Password", "NOT_USED");
+        Authentication token = new TestingAuthenticationToken("Test", "Password", "NOT_USED");
         SecurityContextHolder.getContext().setAuthentication(token);
         SecurityContextHolder.getContext().setAuthentication(token);
 
 
-        // Create and test our secure object
-        final FilterChain chain = mock(FilterChain.class);
-        final FilterInvocation fi = new FilterInvocation(request, response, chain);
-        final List<ConfigAttribute> attributes = SecurityConfig.createList("MOCK_OK");
+        FilterInvocation fi = createinvocation();
 
 
-        when(ods.getAttributes(fi)).thenReturn(attributes);
+        when(ods.getAttributes(fi)).thenReturn(SecurityConfig.createList("MOCK_OK"));
 
 
         interceptor.invoke(fi);
         interceptor.invoke(fi);
 
 
-        verify(publisher).publishEvent(any(AuthorizedEvent.class));        
+        verify(publisher).publishEvent(any(AuthorizedEvent.class));
     }
     }
+
+    @Test
+    public void afterInvocationIsNotInvokedIfExceptionThrown() throws Exception {
+        Authentication token = new TestingAuthenticationToken("Test", "Password", "NOT_USED");
+        SecurityContextHolder.getContext().setAuthentication(token);
+
+        FilterInvocation fi = createinvocation();
+        FilterChain chain = fi.getChain();
+
+        doThrow(new RuntimeException()).when(chain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
+        when(ods.getAttributes(fi)).thenReturn(SecurityConfig.createList("MOCK_OK"));
+
+        AfterInvocationManager aim = mock(AfterInvocationManager.class);
+        interceptor.setAfterInvocationManager(aim);
+
+        try {
+            interceptor.invoke(fi);
+            fail("Expected exception");
+        } catch (RuntimeException expected) {
+        }
+
+        verifyZeroInteractions(aim);
+    }
+
+    private FilterInvocation createinvocation() {
+        MockHttpServletResponse response = new MockHttpServletResponse();
+        MockHttpServletRequest request = new MockHttpServletRequest();
+        request.setServletPath("/secure/page.html");
+
+        FilterChain chain = mock(FilterChain.class);
+        FilterInvocation fi = new FilterInvocation(request, response, chain);
+
+        return fi;
+    }
+
 }
 }