From 1ed643ca1f1a23ddd06762e4092480fe13f66bf4 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Wed, 28 Nov 2012 17:56:03 -0600 Subject: [PATCH] SEC-1998: Provide integration with WebAsyncManager#startCallableProcessing Support integration of the Spring SecurityContext on Callable's used with WebAsyncManager by registering SecurityContextCallableProcessingInterceptor. --- cas/cas.gradle | 4 +- config/config.gradle | 2 +- .../config/http/HttpConfigurationBuilder.java | 18 ++- .../security/config/http/SecurityFilters.java | 17 +++ .../http/AbstractHttpConfigTests.groovy | 19 ++- .../config/http/MiscHttpConfigTests.groovy | 9 +- .../http/SessionManagementConfigTests.groovy | 2 +- .../security/core/JavaVersionTests.java | 49 +++++++ gradle/ide-integration.gradle | 25 +++- gradle/javaprojects.gradle | 1 + itest/context/itest-context.gradle | 2 +- itest/web/itest-web.gradle | 2 +- openid/openid.gradle | 2 +- samples/cas/sample/cassample.gradle | 2 +- samples/contacts/contacts.gradle | 2 +- samples/gae/gae.gradle | 2 +- samples/jaas/jaas.gradle | 2 +- samples/openid/openid.gradle | 2 +- samples/tutorial/tutorial.gradle | 2 +- sandbox/heavyduty/build.gradle | 4 +- taglibs/taglibs.gradle | 5 +- .../security/web/FilterInvocation.java | 82 ++++++++++- ...yContextCallableProcessingInterceptor.java | 79 +++++++++++ .../WebAsyncManagerIntegrationFilter.java | 52 +++++++ .../web/savedrequest/DefaultSavedRequest.java | 2 +- .../AbstractRememberMeServicesTests.java | 24 +++- ...extCallableProcessingInterceptorTests.java | 77 +++++++++++ ...WebAsyncManagerIntegrationFilterTests.java | 127 ++++++++++++++++++ web/web.gradle | 2 +- 29 files changed, 583 insertions(+), 35 deletions(-) create mode 100644 core/src/test/java/org/springframework/security/core/JavaVersionTests.java create mode 100644 web/src/main/java/org/springframework/security/web/context/request/async/SecurityContextCallableProcessingInterceptor.java create mode 100644 web/src/main/java/org/springframework/security/web/context/request/async/WebAsyncManagerIntegrationFilter.java create mode 100644 web/src/test/java/org/springframework/security/web/context/request/async/SecurityContextCallableProcessingInterceptorTests.java create mode 100644 web/src/test/java/org/springframework/security/web/context/request/async/WebAsyncManagerIntegrationFilterTests.java diff --git a/cas/cas.gradle b/cas/cas.gradle index b3e75a3fb0..7a6f210c49 100644 --- a/cas/cas.gradle +++ b/cas/cas.gradle @@ -8,5 +8,5 @@ dependencies { "org.jasig.cas.client:cas-client-core:3.1.12", "net.sf.ehcache:ehcache:$ehcacheVersion" - provided 'javax.servlet:servlet-api:2.5' -} \ No newline at end of file + provided "org.apache.tomcat:tomcat-servlet-api:$servletApiVersion" +} \ No newline at end of file diff --git a/config/config.gradle b/config/config.gradle index a60ad1a130..6d16f62edb 100644 --- a/config/config.gradle +++ b/config/config.gradle @@ -21,7 +21,7 @@ dependencies { "org.springframework:spring-web:$springVersion", "org.springframework:spring-beans:$springVersion" - provided "javax.servlet:servlet-api:2.5" + provided "org.apache.tomcat:tomcat-servlet-api:$servletApiVersion" groovy 'org.codehaus.groovy:groovy:1.8.7' diff --git a/config/src/main/java/org/springframework/security/config/http/HttpConfigurationBuilder.java b/config/src/main/java/org/springframework/security/config/http/HttpConfigurationBuilder.java index cd959528e8..fc748b85de 100644 --- a/config/src/main/java/org/springframework/security/config/http/HttpConfigurationBuilder.java +++ b/config/src/main/java/org/springframework/security/config/http/HttpConfigurationBuilder.java @@ -21,6 +21,8 @@ import static org.springframework.security.config.http.SecurityFilters.*; import java.util.ArrayList; import java.util.List; +import javax.servlet.ServletRequest; + import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanReference; import org.springframework.beans.factory.config.RuntimeBeanReference; @@ -53,6 +55,7 @@ import org.springframework.security.web.authentication.session.SessionFixationPr import org.springframework.security.web.context.HttpSessionSecurityContextRepository; import org.springframework.security.web.context.NullSecurityContextRepository; import org.springframework.security.web.context.SecurityContextPersistenceFilter; +import org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter; import org.springframework.security.web.jaasapi.JaasApiIntegrationFilter; import org.springframework.security.web.savedrequest.HttpSessionRequestCache; import org.springframework.security.web.savedrequest.NullRequestCache; @@ -61,6 +64,7 @@ import org.springframework.security.web.servletapi.SecurityContextHolderAwareReq import org.springframework.security.web.session.ConcurrentSessionFilter; import org.springframework.security.web.session.SessionManagementFilter; import org.springframework.security.web.session.SimpleRedirectInvalidSessionStrategy; +import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; import org.springframework.util.xml.DomUtils; import org.w3c.dom.Element; @@ -102,6 +106,7 @@ class HttpConfigurationBuilder { private BeanReference contextRepoRef; private BeanReference sessionRegistryRef; private BeanDefinition concurrentSessionFilter; + private BeanDefinition webAsyncManagerFilter; private BeanDefinition requestCacheAwareFilter; private BeanReference sessionStrategyRef; private RootBeanDefinition sfpf; @@ -114,7 +119,6 @@ class HttpConfigurationBuilder { public HttpConfigurationBuilder(Element element, ParserContext pc, BeanReference portMapper, BeanReference portResolver, BeanReference authenticationManager) { - this.httpElt = element; this.pc = pc; this.portMapper = portMapper; @@ -140,6 +144,7 @@ class HttpConfigurationBuilder { createSecurityContextPersistenceFilter(); createSessionManagementFilters(); + createWebAsyncManagerFilter(); createRequestCacheFilter(); createServletApiFilter(); createJaasApiFilter(); @@ -350,6 +355,13 @@ class HttpConfigurationBuilder { sessionRegistryRef = new RuntimeBeanReference(sessionRegistryId); } + private void createWebAsyncManagerFilter() { + boolean asyncSupported = ClassUtils.hasMethod(ServletRequest.class, "startAsync"); + if(asyncSupported) { + webAsyncManagerFilter = new RootBeanDefinition(WebAsyncManagerIntegrationFilter.class); + } + } + // Adds the servlet-api integration filter if required private void createServletApiFilter() { final String ATT_SERVLET_API_PROVISION = "servlet-api-provision"; @@ -552,6 +564,10 @@ class HttpConfigurationBuilder { filters.add(new OrderDecorator(concurrentSessionFilter, CONCURRENT_SESSION_FILTER)); } + if (webAsyncManagerFilter != null) { + filters.add(new OrderDecorator(webAsyncManagerFilter, WEB_ASYNC_MANAGER_FILTER)); + } + filters.add(new OrderDecorator(securityContextPersistenceFilter, SECURITY_CONTEXT_FILTER)); if (servApiFilter != null) { diff --git a/config/src/main/java/org/springframework/security/config/http/SecurityFilters.java b/config/src/main/java/org/springframework/security/config/http/SecurityFilters.java index f65c20f0fe..58a9bc491a 100644 --- a/config/src/main/java/org/springframework/security/config/http/SecurityFilters.java +++ b/config/src/main/java/org/springframework/security/config/http/SecurityFilters.java @@ -1,10 +1,25 @@ +/* + * Copyright 2002-2012 the original author or authors. + * + * 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 org.springframework.security.config.http; +import org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter; + /** * Stores the default order numbers of all Spring Security filters for use in configuration. * * @author Luke Taylor + * @author Rob Winch */ enum SecurityFilters { @@ -12,6 +27,8 @@ enum SecurityFilters { CHANNEL_FILTER, SECURITY_CONTEXT_FILTER, CONCURRENT_SESSION_FILTER, + /** {@link WebAsyncManagerIntegrationFilter} */ + WEB_ASYNC_MANAGER_FILTER, LOGOUT_FILTER, X509_FILTER, PRE_AUTH_FILTER, diff --git a/config/src/test/groovy/org/springframework/security/config/http/AbstractHttpConfigTests.groovy b/config/src/test/groovy/org/springframework/security/config/http/AbstractHttpConfigTests.groovy index 8f842e382a..416c17ecd5 100644 --- a/config/src/test/groovy/org/springframework/security/config/http/AbstractHttpConfigTests.groovy +++ b/config/src/test/groovy/org/springframework/security/config/http/AbstractHttpConfigTests.groovy @@ -1,3 +1,15 @@ +/* + * Copyright 2002-2012 the original author or authors. + * + * 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 org.springframework.security.config.http import javax.servlet.Filter @@ -8,8 +20,13 @@ import org.springframework.security.config.AbstractXmlConfigTests import org.springframework.security.config.BeanIds import org.springframework.security.web.FilterInvocation +/** + * + * @author Rob Winch + * + */ abstract class AbstractHttpConfigTests extends AbstractXmlConfigTests { - final int AUTO_CONFIG_FILTERS = 11; + final int AUTO_CONFIG_FILTERS = 12; def httpAutoConfig(Closure c) { xml.http('auto-config': 'true', c) diff --git a/config/src/test/groovy/org/springframework/security/config/http/MiscHttpConfigTests.groovy b/config/src/test/groovy/org/springframework/security/config/http/MiscHttpConfigTests.groovy index e9d32ed8a5..3a4432ef4f 100644 --- a/config/src/test/groovy/org/springframework/security/config/http/MiscHttpConfigTests.groovy +++ b/config/src/test/groovy/org/springframework/security/config/http/MiscHttpConfigTests.groovy @@ -54,6 +54,7 @@ import org.springframework.security.web.authentication.www.BasicAuthenticationEn import org.springframework.security.web.authentication.www.BasicAuthenticationFilter import org.springframework.security.web.context.HttpSessionSecurityContextRepository import org.springframework.security.web.context.SecurityContextPersistenceFilter +import org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter; import org.springframework.security.web.jaasapi.JaasApiIntegrationFilter import org.springframework.security.web.savedrequest.HttpSessionRequestCache import org.springframework.security.web.savedrequest.RequestCacheAwareFilter @@ -79,6 +80,7 @@ import org.springframework.security.authentication.AuthenticationManager * @author Rob Winch */ class MiscHttpConfigTests extends AbstractHttpConfigTests { + def 'Minimal configuration parses'() { setup: xml.http { @@ -101,6 +103,7 @@ class MiscHttpConfigTests extends AbstractHttpConfigTests { Iterator filters = filterList.iterator(); assert filters.next() instanceof SecurityContextPersistenceFilter + assert filters.next() instanceof WebAsyncManagerIntegrationFilter assert filters.next() instanceof LogoutFilter Object authProcFilter = filters.next(); assert authProcFilter instanceof UsernamePasswordAuthenticationFilter @@ -181,7 +184,7 @@ class MiscHttpConfigTests extends AbstractHttpConfigTests { createAppContext() expect: - getFilters("/anything")[5] instanceof AnonymousAuthenticationFilter + getFilters("/anything")[6] instanceof AnonymousAuthenticationFilter } def anonymousFilterIsRemovedIfDisabledFlagSet() { @@ -354,7 +357,7 @@ class MiscHttpConfigTests extends AbstractHttpConfigTests { AUTO_CONFIG_FILTERS + 3 == filters.size(); filters[0] instanceof SecurityContextHolderAwareRequestFilter filters[1] instanceof SecurityContextPersistenceFilter - filters[4] instanceof SecurityContextHolderAwareRequestFilter + filters[5] instanceof SecurityContextHolderAwareRequestFilter filters[1] instanceof SecurityContextPersistenceFilter } @@ -377,7 +380,7 @@ class MiscHttpConfigTests extends AbstractHttpConfigTests { createAppContext() expect: - getFilters("/someurl")[2] instanceof X509AuthenticationFilter + getFilters("/someurl")[3] instanceof X509AuthenticationFilter } def x509SubjectPrincipalRegexCanBeSetUsingPropertyPlaceholder() { diff --git a/config/src/test/groovy/org/springframework/security/config/http/SessionManagementConfigTests.groovy b/config/src/test/groovy/org/springframework/security/config/http/SessionManagementConfigTests.groovy index 09c35ef96d..c4c27a2358 100644 --- a/config/src/test/groovy/org/springframework/security/config/http/SessionManagementConfigTests.groovy +++ b/config/src/test/groovy/org/springframework/security/config/http/SessionManagementConfigTests.groovy @@ -305,7 +305,7 @@ class SessionManagementConfigTests extends AbstractHttpConfigTests { 'session-management'('session-fixation-protection': 'none', 'invalid-session-url': '/timeoutUrl') } createAppContext() - def filter = getFilters("/someurl")[8] + def filter = getFilters("/someurl")[9] expect: filter instanceof SessionManagementFilter diff --git a/core/src/test/java/org/springframework/security/core/JavaVersionTests.java b/core/src/test/java/org/springframework/security/core/JavaVersionTests.java new file mode 100644 index 0000000000..7501b2b7cb --- /dev/null +++ b/core/src/test/java/org/springframework/security/core/JavaVersionTests.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2012 the original author or authors. + * + * 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 org.springframework.security.core; + +import static org.fest.assertions.Assertions.assertThat; + +import java.io.DataInputStream; +import java.io.InputStream; + +import org.junit.Test; + +/** + * + * @author Rob Winch + * + */ +public class JavaVersionTests { + + private static final int JDK5_CLASS_VERSION = 49; + + @Test + public void authenticationCorrectJdkCompatibility() throws Exception { + assertClassVersion(Authentication.class); + } + + private void assertClassVersion(Class clazz) throws Exception { + String classResourceName = clazz.getName().replaceAll("\\.", "/") + ".class"; + InputStream input = Thread.currentThread().getContextClassLoader().getResourceAsStream(classResourceName); + try { + DataInputStream data = new DataInputStream(input); + data.readInt(); + data.readShort(); // minor + int major = data.readShort(); + assertThat(major).isEqualTo(JDK5_CLASS_VERSION); + } finally { + try { input.close(); } catch(Exception e) {} + } + } +} diff --git a/gradle/ide-integration.gradle b/gradle/ide-integration.gradle index 141608f76b..f1dd242fb0 100644 --- a/gradle/ide-integration.gradle +++ b/gradle/ide-integration.gradle @@ -39,8 +39,8 @@ configure(javaProjects) { } } -// STS-2723 -project(':spring-security-samples-aspectj') { +// STS-3057 +configure(allprojects) { task afterEclipseImport { ext.srcFile = file('.classpath') inputs.file srcFile @@ -48,6 +48,25 @@ project(':spring-security-samples-aspectj') { onlyIf { srcFile.exists() } + doLast { + def classpath = new XmlParser().parse(srcFile) + classpath.classpathentry.findAll{ it.@path == 'GROOVY_SUPPORT' }.each { classpath.remove(it) } + + def writer = new FileWriter(srcFile) + new XmlNodePrinter(new PrintWriter(writer)).print(classpath) + } + } +} + +// STS-2723 +project(':spring-security-samples-aspectj') { + task afterEclipseImportAjdtFix { + ext.srcFile = afterEclipseImport.srcFile + inputs.file srcFile + outputs.dir srcFile + + onlyIf { srcFile.exists() } + doLast { def classpath = new XmlParser().parse(srcFile) @@ -63,4 +82,6 @@ project(':spring-security-samples-aspectj') { new XmlNodePrinter(new PrintWriter(writer)).print(classpath) } } + afterEclipseImport.dependsOn afterEclipseImportAjdtFix } + diff --git a/gradle/javaprojects.gradle b/gradle/javaprojects.gradle index ffa3323d4b..ef51dd5220 100644 --- a/gradle/javaprojects.gradle +++ b/gradle/javaprojects.gradle @@ -16,6 +16,7 @@ ext.slf4jVersion = '1.6.1' ext.logbackVersion = '0.9.29' ext.cglibVersion = '2.2' ext.powerMockVersion = '1.4.12' +ext.servletApiVersion = '7.0.33' ext.powerMockDependencies = [ "org.powermock:powermock-core:$powerMockVersion", diff --git a/itest/context/itest-context.gradle b/itest/context/itest-context.gradle index 9a35296564..75c13048c8 100644 --- a/itest/context/itest-context.gradle +++ b/itest/context/itest-context.gradle @@ -10,7 +10,7 @@ dependencies { "org.springframework:spring-beans:$springVersion" testCompile project(':spring-security-web'), - 'javax.servlet:servlet-api:2.5', + "org.apache.tomcat:tomcat-servlet-api:$servletApiVersion", "org.springframework:spring-web:$springVersion" testRuntime project(':spring-security-config') diff --git a/itest/web/itest-web.gradle b/itest/web/itest-web.gradle index 4849b58bfc..44680193ff 100644 --- a/itest/web/itest-web.gradle +++ b/itest/web/itest-web.gradle @@ -3,7 +3,7 @@ dependencies { compile "org.springframework:spring-context:$springVersion", "org.springframework:spring-web:$springVersion" - provided 'javax.servlet:servlet-api:2.5' + provided "org.apache.tomcat:tomcat-servlet-api:$servletApiVersion" testCompile project(':spring-security-core'), project(':spring-security-web'), diff --git a/openid/openid.gradle b/openid/openid.gradle index 293b717817..8521b060c7 100644 --- a/openid/openid.gradle +++ b/openid/openid.gradle @@ -16,7 +16,7 @@ dependencies { } compile 'com.google.inject:guice:2.0' - provided 'javax.servlet:servlet-api:2.5' + provided "org.apache.tomcat:tomcat-servlet-api:$servletApiVersion" runtime 'org.apache.httpcomponents:httpclient:4.1.1' } diff --git a/samples/cas/sample/cassample.gradle b/samples/cas/sample/cassample.gradle index 79d7fc5e1e..9b368b0396 100644 --- a/samples/cas/sample/cassample.gradle +++ b/samples/cas/sample/cassample.gradle @@ -30,7 +30,7 @@ eclipse.classpath.plusConfigurations += configurations.integrationTestRuntime dependencies { groovy 'org.codehaus.groovy:groovy:1.8.7' - providedCompile 'javax.servlet:servlet-api:2.5@jar' + providedCompile "org.apache.tomcat:tomcat-servlet-api:$servletApiVersion" compile project(':spring-security-core'), project(':spring-security-cas'), diff --git a/samples/contacts/contacts.gradle b/samples/contacts/contacts.gradle index bd4b8974c8..b28a28d940 100644 --- a/samples/contacts/contacts.gradle +++ b/samples/contacts/contacts.gradle @@ -9,7 +9,7 @@ configurations { } dependencies { - providedCompile 'javax.servlet:servlet-api:2.5@jar' + providedCompile "org.apache.tomcat:tomcat-servlet-api:$servletApiVersion" compile project(':spring-security-core'), project(':spring-security-acl'), diff --git a/samples/gae/gae.gradle b/samples/gae/gae.gradle index bb6710c7a0..927614935a 100644 --- a/samples/gae/gae.gradle +++ b/samples/gae/gae.gradle @@ -21,7 +21,7 @@ repositories { configurations.runtime.exclude(group: 'ch.qos.logback') dependencies { - providedCompile 'javax.servlet:servlet-api:2.5@jar' + providedCompile "org.apache.tomcat:tomcat-servlet-api:$servletApiVersion" compile project(':spring-security-core'), project(':spring-security-web'), diff --git a/samples/jaas/jaas.gradle b/samples/jaas/jaas.gradle index db238d098d..f4eb1d50db 100644 --- a/samples/jaas/jaas.gradle +++ b/samples/jaas/jaas.gradle @@ -14,7 +14,7 @@ configurations { } dependencies { - providedCompile 'javax.servlet:servlet-api:2.5@jar' + providedCompile "org.apache.tomcat:tomcat-servlet-api:$servletApiVersion" compile project(':spring-security-core'), "org.springframework:spring-beans:$springVersion", diff --git a/samples/openid/openid.gradle b/samples/openid/openid.gradle index 2a3d83af56..2a7b1842e7 100644 --- a/samples/openid/openid.gradle +++ b/samples/openid/openid.gradle @@ -7,7 +7,7 @@ dependencies { compile project(':spring-security-core'), project(':spring-security-openid') - providedCompile 'javax.servlet:servlet-api:2.5@jar' + providedCompile "org.apache.tomcat:tomcat-servlet-api:$servletApiVersion" runtime project(':spring-security-config'), project(':spring-security-taglibs'), diff --git a/samples/tutorial/tutorial.gradle b/samples/tutorial/tutorial.gradle index 60397a41b3..d880a7cb0a 100644 --- a/samples/tutorial/tutorial.gradle +++ b/samples/tutorial/tutorial.gradle @@ -14,7 +14,7 @@ configurations { } dependencies { - providedCompile 'javax.servlet:servlet-api:2.5@jar' + providedCompile "org.apache.tomcat:tomcat-servlet-api:$servletApiVersion" compile project(':spring-security-core'), "org.springframework:spring-beans:$springVersion", diff --git a/sandbox/heavyduty/build.gradle b/sandbox/heavyduty/build.gradle index 75341a155a..379e2edcee 100644 --- a/sandbox/heavyduty/build.gradle +++ b/sandbox/heavyduty/build.gradle @@ -24,9 +24,9 @@ dependencies { 'org.aspectj:aspectjrt:1.6.8', 'org.hibernate:ejb3-persistence:1.0.2.GA', 'javax.persistence:persistence-api:1.0', - 'org.slf4j:jcl-over-slf4j:1.5.11' + 'org.slf4j:jcl-over-slf4j:1.5.11' - providedCompile 'javax.servlet:servlet-api:2.5' + providedCompile "org.apache.tomcat:tomcat-servlet-api:$servletApiVersion" runtime 'org.hibernate:hibernate-entitymanager:3.4.0.GA', "org.springframework:spring-context-support:$springVersion", diff --git a/taglibs/taglibs.gradle b/taglibs/taglibs.gradle index d631a4550f..ab83d03ca4 100644 --- a/taglibs/taglibs.gradle +++ b/taglibs/taglibs.gradle @@ -10,7 +10,8 @@ dependencies { "org.springframework:spring-expression:$springVersion", "org.springframework:spring-web:$springVersion" - provided 'javax.servlet:jsp-api:2.0', 'javax.servlet:servlet-api:2.5' - + provided 'javax.servlet:jsp-api:2.0', + "org.apache.tomcat:tomcat-servlet-api:$servletApiVersion" + testRuntime "javax.servlet:jstl:$jstlVersion" } \ No newline at end of file diff --git a/web/src/main/java/org/springframework/security/web/FilterInvocation.java b/web/src/main/java/org/springframework/security/web/FilterInvocation.java index f7006d500b..558272de0a 100644 --- a/web/src/main/java/org/springframework/security/web/FilterInvocation.java +++ b/web/src/main/java/org/springframework/security/web/FilterInvocation.java @@ -1,4 +1,5 @@ -/* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited +/* Copyright 2002-2012 the original author or authors. + * Copyright 2004, 2005, 2006 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. @@ -20,12 +21,16 @@ import java.io.IOException; import java.io.PrintWriter; import java.io.UnsupportedEncodingException; import java.security.Principal; +import java.util.Collection; import java.util.Enumeration; import java.util.Locale; import java.util.Map; +import javax.servlet.AsyncContext; +import javax.servlet.DispatcherType; import javax.servlet.FilterChain; import javax.servlet.RequestDispatcher; +import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.ServletInputStream; import javax.servlet.ServletOutputStream; @@ -35,6 +40,7 @@ import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; +import javax.servlet.http.Part; import org.springframework.security.web.util.UrlUtils; @@ -50,6 +56,7 @@ import org.springframework.security.web.util.UrlUtils; * @author Ben Alex * @author colin sampaleanu * @author Luke Taylor + * @author Rob Winch */ public class FilterInvocation { //~ Static fields ================================================================================================== @@ -147,7 +154,7 @@ public class FilterInvocation { } } -@SuppressWarnings({"unchecked", "deprecation"}) +@SuppressWarnings({"unchecked"}) class DummyRequest implements HttpServletRequest { private String requestURI; private String contextPath = ""; @@ -221,10 +228,12 @@ class DummyRequest implements HttpServletRequest { throw new UnsupportedOperationException(); } + @SuppressWarnings("rawtypes") public Enumeration getHeaderNames() { throw new UnsupportedOperationException(); } + @SuppressWarnings("rawtypes") public Enumeration getHeaders(String name) { throw new UnsupportedOperationException(); } @@ -285,6 +294,7 @@ class DummyRequest implements HttpServletRequest { throw new UnsupportedOperationException(); } + @SuppressWarnings("rawtypes") public Enumeration getAttributeNames() { throw new UnsupportedOperationException(); } @@ -322,6 +332,7 @@ class DummyRequest implements HttpServletRequest { throw new UnsupportedOperationException(); } + @SuppressWarnings("rawtypes") public Enumeration getLocales() { throw new UnsupportedOperationException(); } @@ -330,10 +341,12 @@ class DummyRequest implements HttpServletRequest { throw new UnsupportedOperationException(); } + @SuppressWarnings("rawtypes") public Map getParameterMap() { throw new UnsupportedOperationException(); } + @SuppressWarnings("rawtypes") public Enumeration getParameterNames() { throw new UnsupportedOperationException(); } @@ -397,9 +410,56 @@ class DummyRequest implements HttpServletRequest { public void setCharacterEncoding(String env) throws UnsupportedEncodingException { throw new UnsupportedOperationException(); } + + public ServletContext getServletContext() { + throw new UnsupportedOperationException(); + } + + public AsyncContext startAsync() { + throw new UnsupportedOperationException(); + } + + public AsyncContext startAsync(ServletRequest servletRequest, ServletResponse servletResponse) { + throw new UnsupportedOperationException(); + } + + public boolean isAsyncStarted() { + throw new UnsupportedOperationException(); + } + + public boolean isAsyncSupported() { + throw new UnsupportedOperationException(); + } + + public AsyncContext getAsyncContext() { + throw new UnsupportedOperationException(); + } + + public DispatcherType getDispatcherType() { + throw new UnsupportedOperationException(); + } + + public boolean authenticate(HttpServletResponse response) throws IOException, ServletException { + throw new UnsupportedOperationException(); + } + + public void login(String username, String password) throws ServletException { + throw new UnsupportedOperationException(); + } + + public void logout() throws ServletException { + throw new UnsupportedOperationException(); + } + + public Collection getParts() throws IOException, IllegalStateException, ServletException { + throw new UnsupportedOperationException(); + } + + public Part getPart(String name) throws IOException, IllegalStateException, ServletException { + throw new UnsupportedOperationException(); + } } -@SuppressWarnings({"deprecation"}) class DummyResponse implements HttpServletResponse { public void addCookie(Cookie cookie) { throw new UnsupportedOperationException(); @@ -529,4 +589,20 @@ class DummyResponse implements HttpServletResponse { public void setLocale(Locale loc) { throw new UnsupportedOperationException(); } + + public int getStatus() { + throw new UnsupportedOperationException(); + } + + public String getHeader(String name) { + throw new UnsupportedOperationException(); + } + + public Collection getHeaders(String name) { + throw new UnsupportedOperationException(); + } + + public Collection getHeaderNames() { + throw new UnsupportedOperationException(); + } } diff --git a/web/src/main/java/org/springframework/security/web/context/request/async/SecurityContextCallableProcessingInterceptor.java b/web/src/main/java/org/springframework/security/web/context/request/async/SecurityContextCallableProcessingInterceptor.java new file mode 100644 index 0000000000..d9e04372c3 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/context/request/async/SecurityContextCallableProcessingInterceptor.java @@ -0,0 +1,79 @@ +/* + * Copyright 2002-2012 the original author or authors. + * + * 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 org.springframework.security.web.context.request.async; + +import java.util.concurrent.Callable; + +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.util.Assert; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.context.request.async.CallableProcessingInterceptor; +import org.springframework.web.context.request.async.CallableProcessingInterceptorAdapter; + +/** + *

+ * Allows for integration with Spring MVC's {@link Callable} support. + *

+ *

+ * A {@link CallableProcessingInterceptor} that establishes the injected {@link SecurityContext} on the + * {@link SecurityContextHolder} when {@link #preProcess(NativeWebRequest, Callable)} is invoked. It also clear out the + * {@link SecurityContextHolder} by invoking {@link SecurityContextHolder#clearContext()} in the + * {@link #afterCompletion(NativeWebRequest, Callable)} method. + *

+ * + * @author Rob Winch + * @since 3.2 + */ +public final class SecurityContextCallableProcessingInterceptor extends CallableProcessingInterceptorAdapter { + private SecurityContext securityContext; + + /** + * Create a new {@link SecurityContextCallableProcessingInterceptor} that uses the {@link SecurityContext} from the + * {@link SecurityContextHolder} at the time {@link #beforeConcurrentHandling(NativeWebRequest, Callable)} is invoked. + */ + public SecurityContextCallableProcessingInterceptor() { + } + + /** + * Creates a new {@link SecurityContextCallableProcessingInterceptor} with the specified {@link SecurityContext}. + * @param securityContext the {@link SecurityContext} to set on the {@link SecurityContextHolder} in + * {@link #preProcess(NativeWebRequest, Callable)}. Cannot be null. + * @throws IllegalArgumentException if {@link SecurityContext} is null. + */ + public SecurityContextCallableProcessingInterceptor(SecurityContext securityContext) { + Assert.notNull(securityContext, "securityContext cannot be null"); + setSecurityContext(securityContext); + } + + @Override + public void beforeConcurrentHandling(NativeWebRequest request, Callable task) throws Exception { + if(securityContext == null) { + setSecurityContext(SecurityContextHolder.getContext()); + } + } + + @Override + public void afterCompletion(NativeWebRequest request, Callable task) throws Exception { + SecurityContextHolder.clearContext(); + } + + @Override + public void preProcess(NativeWebRequest request, Callable task) throws Exception { + SecurityContextHolder.setContext(securityContext); + } + + private void setSecurityContext(SecurityContext securityContext) { + this.securityContext = securityContext; + } +} diff --git a/web/src/main/java/org/springframework/security/web/context/request/async/WebAsyncManagerIntegrationFilter.java b/web/src/main/java/org/springframework/security/web/context/request/async/WebAsyncManagerIntegrationFilter.java new file mode 100644 index 0000000000..b6de62b8fe --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/context/request/async/WebAsyncManagerIntegrationFilter.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2012 the original author or authors. + * + * 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 org.springframework.security.web.context.request.async; + +import java.io.IOException; +import java.util.concurrent.Callable; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.security.core.context.SecurityContext; +import org.springframework.web.context.request.async.WebAsyncManager; +import org.springframework.web.context.request.async.WebAsyncUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +/** + * Provides integration between the {@link SecurityContext} and Spring Web's {@link WebAsyncManager} by using the + * {@link SecurityContextCallableProcessingInterceptor#beforeConcurrentHandling(org.springframework.web.context.request.NativeWebRequest, Callable)} + * to populate the {@link SecurityContext} on the {@link Callable}. + * + * @author Rob Winch + * @see SecurityContextCallableProcessingInterceptor + */ +public final class WebAsyncManagerIntegrationFilter extends OncePerRequestFilter { + private static final Object CALLABLE_INTERCEPTOR_KEY = new Object(); + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); + + SecurityContextCallableProcessingInterceptor securityProcessingInterceptor = (SecurityContextCallableProcessingInterceptor) asyncManager.getCallableInterceptor(CALLABLE_INTERCEPTOR_KEY); + if (securityProcessingInterceptor == null) { + asyncManager.registerCallableInterceptor(CALLABLE_INTERCEPTOR_KEY, + new SecurityContextCallableProcessingInterceptor()); + } + + filterChain.doFilter(request, response); + } +} \ No newline at end of file diff --git a/web/src/main/java/org/springframework/security/web/savedrequest/DefaultSavedRequest.java b/web/src/main/java/org/springframework/security/web/savedrequest/DefaultSavedRequest.java index 9dfd2f24a1..795642e13b 100644 --- a/web/src/main/java/org/springframework/security/web/savedrequest/DefaultSavedRequest.java +++ b/web/src/main/java/org/springframework/security/web/savedrequest/DefaultSavedRequest.java @@ -110,7 +110,7 @@ public class DefaultSavedRequest implements SavedRequest { } // Parameters - Map parameters = request.getParameterMap(); + Map parameters = request.getParameterMap(); for(String paramName : parameters.keySet()) { Object paramValues = parameters.get(paramName); diff --git a/web/src/test/java/org/springframework/security/web/authentication/rememberme/AbstractRememberMeServicesTests.java b/web/src/test/java/org/springframework/security/web/authentication/rememberme/AbstractRememberMeServicesTests.java index 5fca1d7c5e..2417efc583 100644 --- a/web/src/test/java/org/springframework/security/web/authentication/rememberme/AbstractRememberMeServicesTests.java +++ b/web/src/test/java/org/springframework/security/web/authentication/rememberme/AbstractRememberMeServicesTests.java @@ -1,13 +1,23 @@ package org.springframework.security.web.authentication.rememberme; -import static org.junit.Assert.*; -import static org.mockito.Mockito.*; +import static org.powermock.api.mockito.PowerMockito.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.core.classloader.annotations.PrepareOnlyThisForTest; +import org.powermock.modules.junit4.PowerMockRunner; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.security.authentication.AccountStatusUserDetailsChecker; @@ -19,17 +29,16 @@ import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices; -import org.springframework.security.web.authentication.rememberme.CookieTheftException; -import org.springframework.security.web.authentication.rememberme.InvalidCookieException; -import org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationException; import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; /** * @author Luke Taylor */ @SuppressWarnings("unchecked") +@RunWith(PowerMockRunner.class) +@PrepareOnlyThisForTest(ReflectionUtils.class) public class AbstractRememberMeServicesTests { static User joe = new User("joe", "password", true, true,true,true, AuthorityUtils.createAuthorityList("ROLE_A")); @@ -333,6 +342,9 @@ public class AbstractRememberMeServicesTests { @Test public void setHttpOnlyIgnoredForServlet25() throws Exception { + spy(ReflectionUtils.class); + when(ReflectionUtils.findMethod(Cookie.class,"setHttpOnly", boolean.class)).thenReturn(null); + MockRememberMeServices services = new MockRememberMeServices(); assertNull(ReflectionTestUtils.getField(services, "setHttpOnlyMethod")); diff --git a/web/src/test/java/org/springframework/security/web/context/request/async/SecurityContextCallableProcessingInterceptorTests.java b/web/src/test/java/org/springframework/security/web/context/request/async/SecurityContextCallableProcessingInterceptorTests.java new file mode 100644 index 0000000000..0f8c107380 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/context/request/async/SecurityContextCallableProcessingInterceptorTests.java @@ -0,0 +1,77 @@ +/* + * Copyright 2002-2012 the original author or authors. + * + * 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 org.springframework.security.web.context.request.async; + +import static org.fest.assertions.Assertions.assertThat; + +import java.util.concurrent.Callable; + +import org.junit.After; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.context.request.NativeWebRequest; + +/** + * + * @author Rob Winch + * + */ +@RunWith(MockitoJUnitRunner.class) +public class SecurityContextCallableProcessingInterceptorTests { + @Mock + private SecurityContext securityContext; + @Mock + private Callable callable; + @Mock + private NativeWebRequest webRequest; + + @After + public void clearSecurityContext() { + SecurityContextHolder.clearContext(); + } + + @Test(expected = IllegalArgumentException.class) + public void constructorNull() { + new SecurityContextCallableProcessingInterceptor(null); + } + + @Test + public void currentSecurityContext() throws Exception { + SecurityContextCallableProcessingInterceptor interceptor = new SecurityContextCallableProcessingInterceptor(); + SecurityContextHolder.setContext(securityContext); + interceptor.beforeConcurrentHandling(webRequest, callable); + SecurityContextHolder.clearContext(); + + interceptor.preProcess(webRequest, callable); + assertThat(SecurityContextHolder.getContext()).isSameAs(securityContext); + + interceptor.afterCompletion(webRequest, callable); + assertThat(SecurityContextHolder.getContext()).isNotSameAs(securityContext); + } + + @Test + public void specificSecurityContext() throws Exception { + SecurityContextCallableProcessingInterceptor interceptor = new SecurityContextCallableProcessingInterceptor( + securityContext); + + interceptor.preProcess(webRequest, callable); + assertThat(SecurityContextHolder.getContext()).isSameAs(securityContext); + + interceptor.afterCompletion(webRequest, callable); + assertThat(SecurityContextHolder.getContext()).isNotSameAs(securityContext); + } +} diff --git a/web/src/test/java/org/springframework/security/web/context/request/async/WebAsyncManagerIntegrationFilterTests.java b/web/src/test/java/org/springframework/security/web/context/request/async/WebAsyncManagerIntegrationFilterTests.java new file mode 100644 index 0000000000..33d6740b1b --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/context/request/async/WebAsyncManagerIntegrationFilterTests.java @@ -0,0 +1,127 @@ +/* + * Copyright 2002-2012 the original author or authors. + * + * 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 org.springframework.security.web.context.request.async; + +import static org.fest.assertions.Assertions.assertThat; +import static org.mockito.Mockito.when; + +import java.util.concurrent.Callable; +import java.util.concurrent.ThreadFactory; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.core.task.SimpleAsyncTaskExecutor; +import org.springframework.mock.web.MockFilterChain; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.context.request.async.AsyncWebRequest; +import org.springframework.web.context.request.async.WebAsyncManager; +import org.springframework.web.context.request.async.WebAsyncUtils; + + +/** + * + * @author Rob Winch + * + */ +@RunWith(MockitoJUnitRunner.class) +public class WebAsyncManagerIntegrationFilterTests { + @Mock + private SecurityContext securityContext; + @Mock + private HttpServletRequest request; + @Mock + private HttpServletResponse response; + @Mock + private AsyncWebRequest asyncWebRequest; + private WebAsyncManager asyncManager; + private JoinableThreadFactory threadFactory; + + private MockFilterChain filterChain; + + private WebAsyncManagerIntegrationFilter filter; + + @Before + public void setUp() { + when(asyncWebRequest.getNativeRequest(HttpServletRequest.class)).thenReturn(request); + when(request.getRequestURI()).thenReturn("/"); + filterChain = new MockFilterChain(); + + threadFactory = new JoinableThreadFactory(); + SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor(); + executor.setThreadFactory(threadFactory); + + asyncManager = WebAsyncUtils.getAsyncManager(request); + asyncManager.setAsyncWebRequest(asyncWebRequest); + asyncManager.setTaskExecutor(executor); + when(request.getAttribute(WebAsyncUtils.WEB_ASYNC_MANAGER_ATTRIBUTE)).thenReturn(asyncManager); + + filter = new WebAsyncManagerIntegrationFilter(); + } + + @After + public void clearSecurityContext() { + SecurityContextHolder.clearContext(); + } + + @Test + public void doFilterInternalRegistersSecurityContextCallableProcessor() throws Exception { + SecurityContextHolder.setContext(securityContext); + filter.doFilterInternal(request, response, filterChain); + + VerifyingCallable verifyingCallable = new VerifyingCallable(); + asyncManager.startCallableProcessing(verifyingCallable); + threadFactory.join(); + assertThat(asyncManager.getConcurrentResult()).isSameAs(securityContext); + } + + @Test + public void doFilterInternalRegistersSecurityContextCallableProcessorContextUpdated() throws Exception { + SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext()); + filter.doFilterInternal(request, response, filterChain); + SecurityContextHolder.setContext(securityContext); + + VerifyingCallable verifyingCallable = new VerifyingCallable(); + asyncManager.startCallableProcessing(verifyingCallable); + threadFactory.join(); + assertThat(asyncManager.getConcurrentResult()).isSameAs(securityContext); + } + + private static final class JoinableThreadFactory implements ThreadFactory { + private Thread t; + + public Thread newThread(Runnable r) { + t = new Thread(r); + return t; + } + + public void join() throws InterruptedException { + t.join(); + } + } + + private class VerifyingCallable implements Callable { + + public SecurityContext call() throws Exception { + return SecurityContextHolder.getContext(); + } + + } +} diff --git a/web/web.gradle b/web/web.gradle index 5648190fbf..19e9ec6f35 100644 --- a/web/web.gradle +++ b/web/web.gradle @@ -9,7 +9,7 @@ dependencies { "org.springframework:spring-tx:$springVersion", "org.springframework:spring-web:$springVersion" - provided 'javax.servlet:servlet-api:2.5' + provided "org.apache.tomcat:tomcat-servlet-api:$servletApiVersion" testCompile project(':spring-security-core').sourceSets.test.output, 'commons-codec:commons-codec:1.3',