From b0758dd8de2263ac9f8eab0b54cfc472f01deae2 Mon Sep 17 00:00:00 2001 From: Luke Taylor Date: Sat, 27 Mar 2010 00:07:59 +0000 Subject: [PATCH] Refactoring HTTP config tests to use spock and groovy MarkupBuilder --- build.gradle | 1 + config/config.gradle | 8 +- .../config/AbstractXmlConfigTests.groovy | 71 +++ .../http/AbstractHttpConfigTests.groovy | 68 +++ .../http/AccessDeniedConfigTests.groovy | 71 +++ .../config/http/FormLoginConfigTests.groovy | 71 +++ .../config/http/HttpOpenIDConfigTests.groovy | 149 ++++++ .../config/http/MiscHttpConfigTests.groovy | 497 ++++++++++++++++++ .../http/PlaceHolderAndELConfigTests.groovy | 154 ++++++ .../config/http/RememberMeConfigTests.groovy | 145 +++++ .../http/SessionManagementConfigTests.groovy | 175 ++++++ ...HttpSecurityBeanDefinitionParserTests.java | 5 - 12 files changed, 1409 insertions(+), 6 deletions(-) create mode 100644 config/src/test/groovy/org/springframework/security/config/AbstractXmlConfigTests.groovy create mode 100644 config/src/test/groovy/org/springframework/security/config/http/AbstractHttpConfigTests.groovy create mode 100644 config/src/test/groovy/org/springframework/security/config/http/AccessDeniedConfigTests.groovy create mode 100644 config/src/test/groovy/org/springframework/security/config/http/FormLoginConfigTests.groovy create mode 100644 config/src/test/groovy/org/springframework/security/config/http/HttpOpenIDConfigTests.groovy create mode 100644 config/src/test/groovy/org/springframework/security/config/http/MiscHttpConfigTests.groovy create mode 100644 config/src/test/groovy/org/springframework/security/config/http/PlaceHolderAndELConfigTests.groovy create mode 100644 config/src/test/groovy/org/springframework/security/config/http/RememberMeConfigTests.groovy create mode 100644 config/src/test/groovy/org/springframework/security/config/http/SessionManagementConfigTests.groovy diff --git a/build.gradle b/build.gradle index 9b50b73da3..8a819bceba 100644 --- a/build.gradle +++ b/build.gradle @@ -14,6 +14,7 @@ allprojects { mavenRepo name: 'SpringSource Maven Snapshot Repo', urls: 'http://maven.springframework.org/snapshot/' mavenRepo name: 'SpringSource Enterprise Release', urls: 'http://repository.springsource.com/maven/bundles/release' mavenRepo name: 'SpringSource Enterprise External', urls: 'http://repository.springsource.com/maven/bundles/external' + mavenRepo(name: 'Spock Snapshots', urls: 'http://m2repo.spockframework.org/snapshots') } } diff --git a/config/config.gradle b/config/config.gradle index 7539f0b46c..6cfbd06c43 100644 --- a/config/config.gradle +++ b/config/config.gradle @@ -1,5 +1,7 @@ // Config Module build file +apply plugin: 'groovy' + compileTestJava.dependsOn(':spring-security-core:compileTestJava') dependencies { @@ -14,13 +16,17 @@ dependencies { provided "javax.servlet:servlet-api:2.5" + groovy group: 'org.codehaus.groovy', name: 'groovy', version: '1.7.1' + testCompile project(':spring-security-ldap'), project(':spring-security-openid'), + 'org.openid4java:openid4java-nodeps:0.9.5', files(this.project(':spring-security-core').sourceSets.test.classesDir), 'javax.annotation:jsr250-api:1.0', "org.springframework.ldap:spring-ldap-core:$springLdapVersion", "org.springframework:spring-jdbc:$springVersion", - "org.springframework:spring-tx:$springVersion" + "org.springframework:spring-tx:$springVersion", + 'org.spockframework:spock-core:0.4-groovy-1.7' testRuntime "hsqldb:hsqldb:$hsqlVersion", "cglib:cglib-nodep:2.2" diff --git a/config/src/test/groovy/org/springframework/security/config/AbstractXmlConfigTests.groovy b/config/src/test/groovy/org/springframework/security/config/AbstractXmlConfigTests.groovy new file mode 100644 index 0000000000..4895564450 --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/AbstractXmlConfigTests.groovy @@ -0,0 +1,71 @@ +package org.springframework.security.config + +import static org.springframework.security.config.ConfigTestUtils.AUTH_PROVIDER_XML; + +import groovy.xml.MarkupBuilder +import java.util.List; +import java.util.Map; + +import org.springframework.context.support.AbstractXmlApplicationContext; +import org.springframework.security.config.util.InMemoryXmlApplicationContext +import org.springframework.security.core.context.SecurityContextHolder + +import spock.lang.Specification + +/** + * + * @author Luke Taylor + */ +abstract class AbstractXmlConfigTests extends Specification { + AbstractXmlApplicationContext appContext; + Writer writer; + MarkupBuilder xml; + + def setup() { + writer = new StringWriter() + xml = new MarkupBuilder(writer) + } + + def cleanup() { + if (appContext != null) { + appContext.close(); + appContext = null; + } + SecurityContextHolder.clearContext(); + } + + def bean(String name, Class clazz) { + xml.'b:bean'(id: name, 'class': clazz.name) + } + + def bean(String name, String clazz) { + xml.'b:bean'(id: name, 'class': clazz) + } + + def bean(String name, String clazz, List constructorArgs) { + xml.'b:bean'(id: name, 'class': clazz) { + constructorArgs.each { val -> + 'b:constructor-arg'(value: val) + } + } + } + + def bean(String name, String clazz, Map properties, Map refs) { + xml.'b:bean'(id: name, 'class': clazz) { + properties.each {key, val -> + 'b:property'(name: key, value: val) + } + refs.each {key, val -> + 'b:property'(name: key, ref: val) + } + } + } + + def createAppContext() { + createAppContext(AUTH_PROVIDER_XML) + } + + def createAppContext(String extraXml) { + appContext = new InMemoryXmlApplicationContext(writer.toString() + extraXml); + } +} 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 new file mode 100644 index 0000000000..843bf275a0 --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/http/AbstractHttpConfigTests.groovy @@ -0,0 +1,68 @@ +package org.springframework.security.config.http + +import groovy.lang.Closure; +import groovy.xml.MarkupBuilder +import java.util.List; + +import javax.servlet.Filter; + +import org.springframework.mock.web.MockFilterChain +import org.springframework.mock.web.MockHttpServletRequest +import org.springframework.mock.web.MockHttpServletResponse +import org.springframework.security.config.AbstractXmlConfigTests +import org.springframework.security.config.BeanIds +import org.springframework.security.config.util.InMemoryXmlApplicationContext +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.security.web.FilterChainProxy +import org.springframework.security.web.FilterInvocation + +abstract class AbstractHttpConfigTests extends AbstractXmlConfigTests { + final int AUTO_CONFIG_FILTERS = 11; + + def httpAutoConfig(Closure c) { + xml.http('auto-config': 'true', c) + } + + def httpAutoConfig(String matcher, Closure c) { + xml.http(['auto-config': 'true', 'request-matcher': matcher], c) + } + + def interceptUrl(String path, String authz) { + xml.'intercept-url'(pattern: path, access: authz) + } + + def interceptUrl(String path, String httpMethod, String authz) { + xml.'intercept-url'(pattern: path, method: httpMethod, access: authz) + } + + + def interceptUrlNoFilters(String path) { + xml.'intercept-url'(pattern: path, filters: 'none') + } + + Filter getFilter(Class type) { + List filters = getFilters("/any"); + + for (f in filters) { + if (f.class.isAssignableFrom(type)) { + return f; + } + } + + return null; + } + + List getFilters(String url) { + FilterChainProxy fcp = appContext.getBean(BeanIds.FILTER_CHAIN_PROXY); + return fcp.getFilters(url) + } + + FilterInvocation createFilterinvocation(String path, String method) { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod(method); + request.setRequestURI(null); + request.setServletPath(path); + + return new FilterInvocation(request, new MockHttpServletResponse(), new MockFilterChain()); + } +} diff --git a/config/src/test/groovy/org/springframework/security/config/http/AccessDeniedConfigTests.groovy b/config/src/test/groovy/org/springframework/security/config/http/AccessDeniedConfigTests.groovy new file mode 100644 index 0000000000..2ffaa31039 --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/http/AccessDeniedConfigTests.groovy @@ -0,0 +1,71 @@ +package org.springframework.security.config.http + +import org.springframework.beans.factory.BeanCreationException +import org.springframework.beans.factory.parsing.BeanDefinitionParsingException +import org.springframework.security.util.FieldUtils +import org.springframework.security.web.access.AccessDeniedHandlerImpl +import org.springframework.security.web.access.ExceptionTranslationFilter + +/** + * + * @author Luke Taylor + */ +class AccessDeniedConfigTests extends AbstractHttpConfigTests { + private static final String ACCESS_DENIED_PAGE = 'access-denied-page'; + + def accessDeniedPageAttributeIsSupported() { + httpAccessDeniedPage ('/accessDenied') { } + createAppContext(); + + expect: + getFilter(ExceptionTranslationFilter.class).accessDeniedHandler.errorPage == '/accessDenied' + + } + + def invalidAccessDeniedUrlIsDetected() { + when: + httpAccessDeniedPage ('noLeadingSlash') { } + createAppContext(); + then: + BeanCreationException e = thrown() + } + + def accessDeniedHandlerIsSetCorectly() { + httpAutoConfig() { + 'access-denied-handler'(ref: 'adh') + } + bean('adh', AccessDeniedHandlerImpl) + createAppContext(); + + def filter = getFilter(ExceptionTranslationFilter.class); + def adh = appContext.getBean("adh"); + + expect: + filter.accessDeniedHandler == adh + } + + def void accessDeniedPageAndAccessDeniedHandlerAreMutuallyExclusive() { + when: + httpAccessDeniedPage ('/accessDenied') { + 'access-denied-handler'('error-page': '/go-away') + } + createAppContext(); + then: + BeanDefinitionParsingException e = thrown() + } + + def void accessDeniedHandlerPageAndRefAreMutuallyExclusive() { + when: + httpAutoConfig { + 'access-denied-handler'('error-page': '/go-away', ref: 'adh') + } + createAppContext(); + bean('adh', AccessDeniedHandlerImpl) + then: + BeanDefinitionParsingException e = thrown() + } + + def httpAccessDeniedPage(String page, Closure c) { + xml.http(['auto-config': 'true', 'access-denied-page': page], c) + } +} diff --git a/config/src/test/groovy/org/springframework/security/config/http/FormLoginConfigTests.groovy b/config/src/test/groovy/org/springframework/security/config/http/FormLoginConfigTests.groovy new file mode 100644 index 0000000000..2168173035 --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/http/FormLoginConfigTests.groovy @@ -0,0 +1,71 @@ +package org.springframework.security.config.http + +import org.springframework.beans.factory.BeanCreationException +import org.springframework.security.util.FieldUtils +import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler; +import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter + +/** + * + * @author Luke Taylor + */ +class FormLoginConfigTests extends AbstractHttpConfigTests { + + def formLoginWithNoLoginPageAddsDefaultLoginPageFilter() { + httpAutoConfig('ant') { + form-login() + } + createAppContext() + filtersMatchExpectedAutoConfigList(); + } + + def 'Form login alwaysUseDefaultTarget sets correct property'() { + xml.http { + 'form-login'('default-target-url':'/default', 'always-use-default-target': 'true') + } + createAppContext() + def filter = getFilter(UsernamePasswordAuthenticationFilter.class); + + expect: + FieldUtils.getFieldValue(filter, 'successHandler.defaultTargetUrl') == '/default'; + FieldUtils.getFieldValue(filter, 'successHandler.alwaysUseDefaultTargetUrl') == true; + } + + def invalidLoginPageIsDetected() { + when: + xml.http { + 'form-login'('login-page': 'noLeadingSlash') + } + createAppContext() + + then: + BeanCreationException e = thrown(); + } + + def invalidDefaultTargetUrlIsDetected() { + when: + xml.http { + 'form-login'('default-target-url': 'noLeadingSlash') + } + createAppContext() + + then: + BeanCreationException e = thrown(); + } + + def customSuccessAndFailureHandlersCanBeSetThroughTheNamespace() { + xml.http { + 'form-login'('authentication-success-handler-ref': 'sh', 'authentication-failure-handler-ref':'fh') + } + bean('sh', SavedRequestAwareAuthenticationSuccessHandler.class.name) + bean('fh', SimpleUrlAuthenticationFailureHandler.class.name) + createAppContext() + + def apf = getFilter(UsernamePasswordAuthenticationFilter.class); + + expect: + FieldUtils.getFieldValue(apf, "successHandler") == appContext.getBean("sh"); + FieldUtils.getFieldValue(apf, "failureHandler") == appContext.getBean("fh") + } +} diff --git a/config/src/test/groovy/org/springframework/security/config/http/HttpOpenIDConfigTests.groovy b/config/src/test/groovy/org/springframework/security/config/http/HttpOpenIDConfigTests.groovy new file mode 100644 index 0000000000..00a4137fd0 --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/http/HttpOpenIDConfigTests.groovy @@ -0,0 +1,149 @@ +package org.springframework.security.config.http + +import javax.servlet.http.HttpServletRequest +import org.springframework.beans.factory.parsing.BeanDefinitionParsingException +import org.springframework.mock.web.MockFilterChain +import org.springframework.mock.web.MockHttpServletRequest +import org.springframework.mock.web.MockHttpServletResponse +import org.springframework.security.config.BeanIds +import org.springframework.security.openid.OpenIDAuthenticationFilter +import org.springframework.security.openid.OpenIDAuthenticationToken +import org.springframework.security.openid.OpenIDConsumer +import org.springframework.security.openid.OpenIDConsumerException +import org.springframework.security.web.FilterChainProxy +import org.springframework.security.web.access.ExceptionTranslationFilter +import org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices +import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter + +/** + * + * @author Luke Taylor + */ +class OpenIDConfigTests extends AbstractHttpConfigTests { + def openIDAndFormLoginWorkTogether() { + xml.http() { + 'openid-login'() + 'form-login'() + } + createAppContext() + + def etf = getFilter(ExceptionTranslationFilter) + def ap = etf.getAuthenticationEntryPoint(); + + expect: + ap.loginFormUrl == "/spring_security_login" + // Default login filter should be present since we haven't specified any login URLs + getFilter(DefaultLoginPageGeneratingFilter) != null + } + + def formLoginEntryPointTakesPrecedenceIfLoginUrlIsSet() { + xml.http() { + 'openid-login'() + 'form-login'('login-page': '/form-page') + } + createAppContext() + + expect: + getFilter(ExceptionTranslationFilter).authenticationEntryPoint.loginFormUrl == '/form-page' + } + + def openIDEntryPointTakesPrecedenceIfLoginUrlIsSet() { + xml.http() { + 'openid-login'('login-page': '/openid-page') + 'form-login'() + } + createAppContext() + + expect: + getFilter(ExceptionTranslationFilter).authenticationEntryPoint.loginFormUrl == '/openid-page' + } + + def multipleLoginPagesCausesError() { + when: + xml.http() { + 'openid-login'('login-page': '/openid-page') + 'form-login'('login-page': '/form-page') + } + createAppContext() + then: + thrown(BeanDefinitionParsingException) + } + + def openIDAndRememberMeWorkTogether() { + xml.http() { + interceptUrl('/**', 'ROLE_NOBODY') + 'openid-login'() + 'remember-me'() + } + createAppContext() + + // Default login filter should be present since we haven't specified any login URLs + def loginFilter = getFilter(DefaultLoginPageGeneratingFilter) + def openIDFilter = getFilter(OpenIDAuthenticationFilter) + openIDFilter.setConsumer(new OpenIDConsumer() { + public String beginConsumption(HttpServletRequest req, String claimedIdentity, String returnToUrl, String realm) + throws OpenIDConsumerException { + return "http://testopenid.com?openid.return_to=" + returnToUrl; + } + + public OpenIDAuthenticationToken endConsumption(HttpServletRequest req) throws OpenIDConsumerException { + throw new UnsupportedOperationException(); + } + }) + Set returnToUrlParameters = new HashSet() + returnToUrlParameters.add(AbstractRememberMeServices.DEFAULT_PARAMETER) + openIDFilter.setReturnToUrlParameters(returnToUrlParameters) + assert loginFilter.openIDrememberMeParameter != null + + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + + when: "Initial request is made" + FilterChainProxy fcp = appContext.getBean(BeanIds.FILTER_CHAIN_PROXY) + request.setServletPath("/something.html") + fcp.doFilter(request, response, new MockFilterChain()) + then: "Redirected to login" + response.getRedirectedUrl().endsWith("/spring_security_login") + when: "Login page is requested" + request.setServletPath("/spring_security_login") + request.setRequestURI("/spring_security_login") + response = new MockHttpServletResponse() + fcp.doFilter(request, response, new MockFilterChain()) + then: "Remember-me choice is added to page" + response.getContentAsString().contains(AbstractRememberMeServices.DEFAULT_PARAMETER) + when: "Login is submitted with remember-me selected" + request.setRequestURI("/j_spring_openid_security_check") + request.setParameter(OpenIDAuthenticationFilter.DEFAULT_CLAIMED_IDENTITY_FIELD, "http://hey.openid.com/") + request.setParameter(AbstractRememberMeServices.DEFAULT_PARAMETER, "on") + response = new MockHttpServletResponse(); + fcp.doFilter(request, response, new MockFilterChain()); + String expectedReturnTo = request.getRequestURL().append("?") + .append(AbstractRememberMeServices.DEFAULT_PARAMETER) + .append("=").append("on").toString(); + then: "return_to URL contains remember-me choice" + response.getRedirectedUrl() == "http://testopenid.com?openid.return_to=" + expectedReturnTo + } + + def openIDWithAttributeExchangeConfigurationIsParsedCorrectly() { + xml.http() { + 'openid-login'() { + 'attribute-exchange'() { + 'openid-attribute'(name: 'nickname', type: 'http://schema.openid.net/namePerson/friendly') + 'openid-attribute'(name: 'email', type: 'http://schema.openid.net/contact/email', required: 'true', + 'count': '2') + } + } + } + createAppContext() + + List attributes = getFilter(OpenIDAuthenticationFilter).consumer.attributesToFetchFactory.createAttributeList('http://someid') + + expect: + attributes.size() == 2 + attributes[0].name == 'nickname' + attributes[0].type == 'http://schema.openid.net/namePerson/friendly' + attributes[0].required == false + attributes[1].required == true + attributes[1].getCount() == 2 + } +} 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 new file mode 100644 index 0000000000..042c95cb10 --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/http/MiscHttpConfigTests.groovy @@ -0,0 +1,497 @@ +package org.springframework.security.config.http; + +import java.util.Collection; +import java.util.Map; +import java.util.Iterator; + +import javax.servlet.Filter +import javax.servlet.http.HttpServletRequest; + +import org.springframework.beans.BeansException +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.mock.web.MockFilterChain; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer; +import org.springframework.beans.factory.parsing.BeanDefinitionParsingException; +import org.springframework.context.support.AbstractXmlApplicationContext +import org.springframework.security.config.BeanIds; +import org.springframework.security.config.util.InMemoryXmlApplicationContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.util.FieldUtils; +import org.springframework.security.access.AccessDeniedException +import org.springframework.security.access.SecurityConfig; +import org.springframework.security.authentication.TestingAuthenticationToken +import org.springframework.security.config.MockUserServiceBeanPostProcessor; +import org.springframework.security.config.PostProcessedMockUserDetailsService; +import org.springframework.security.web.*; +import org.springframework.security.web.access.channel.ChannelProcessingFilter; +import org.springframework.security.web.access.ExceptionTranslationFilter; +import org.springframework.security.web.access.intercept.FilterSecurityInterceptor; +import org.springframework.security.web.authentication.* +import org.springframework.security.web.authentication.logout.LogoutFilter +import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler +import org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter +import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter +import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint +import org.springframework.security.web.authentication.www.BasicAuthenticationFilter +import org.springframework.security.web.context.*; +import org.springframework.security.web.savedrequest.HttpSessionRequestCache +import org.springframework.security.web.savedrequest.RequestCacheAwareFilter; +import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter; +import org.springframework.security.web.session.SessionManagementFilter; + +import groovy.lang.Closure; + +class MiscHttpConfigTests extends AbstractHttpConfigTests { + def 'Minimal configuration parses'() { + setup: + xml.http { + 'http-basic'() + } + createAppContext() + } + + def httpAutoConfigSetsUpCorrectFilterList() { + when: + xml.http('auto-config': 'true') + createAppContext() + + then: + filtersMatchExpectedAutoConfigList('/anyurl'); + } + + void filtersMatchExpectedAutoConfigList(String url) { + def filterList = getFilters(url); + Iterator filters = filterList.iterator(); + + assert filters.next() instanceof SecurityContextPersistenceFilter + assert filters.next() instanceof LogoutFilter + Object authProcFilter = filters.next(); + assert authProcFilter instanceof UsernamePasswordAuthenticationFilter + assert filters.next() instanceof DefaultLoginPageGeneratingFilter + assert filters.next() instanceof BasicAuthenticationFilter + assert filters.next() instanceof RequestCacheAwareFilter + assert filters.next() instanceof SecurityContextHolderAwareRequestFilter + assert filters.next() instanceof AnonymousAuthenticationFilter + assert filters.next() instanceof SessionManagementFilter + assert filters.next() instanceof ExceptionTranslationFilter + Object fsiObj = filters.next(); + assert fsiObj instanceof FilterSecurityInterceptor + def fsi = (FilterSecurityInterceptor) fsiObj; + assert fsi.isObserveOncePerRequest() + } + + def duplicateElementCausesError() { + when: "Two http blocks are defined" + xml.http('auto-config': 'true') + xml.http('auto-config': 'true') + createAppContext() + + then: + BeanDefinitionParsingException e = thrown(); + } + + def filterListShouldBeEmptyForPatternWithNoFilters() { + httpAutoConfig() { + interceptUrlNoFilters('/unprotected') + } + createAppContext() + + expect: + getFilters("/unprotected").size() == 0 + } + + def regexPathsWorkCorrectly() { + httpAutoConfig('regex') { + interceptUrlNoFilters('\\A\\/[a-z]+') + } + createAppContext() + + expect: + getFilters('/imlowercase').size() == 0 + filtersMatchExpectedAutoConfigList('/MixedCase'); + } + + def ciRegexPathsWorkCorrectly() { + when: + httpAutoConfig('ciRegex') { + interceptUrlNoFilters('\\A\\/[a-z]+') + } + createAppContext() + + then: + getFilters('/imMixedCase').size() == 0 + filtersMatchExpectedAutoConfigList('/Im_caught_by_the_Universal_Match'); + } + + // SEC-1152 + def anonymousFilterIsAddedByDefault() { + xml.http { + 'form-login'() + } + createAppContext() + + expect: + getFilters("/anything")[5] instanceof AnonymousAuthenticationFilter + } + + def anonymousFilterIsRemovedIfDisabledFlagSet() { + xml.http { + 'form-login'() + 'anonymous'(enabled: 'false') + } + createAppContext() + + expect: + !(getFilters("/anything").get(5) instanceof AnonymousAuthenticationFilter) + } + + def anonymousCustomAttributesAreSetCorrectly() { + xml.http { + 'form-login'() + 'anonymous'(username: 'joe', 'granted-authority':'anonymity', key: 'customKey') + } + createAppContext() + + AnonymousAuthenticationFilter filter = getFilter(AnonymousAuthenticationFilter); + + expect: + 'customKey' == filter.getKey() + 'joe' == filter.userAttribute.password + 'anonymity' == filter.userAttribute.authorities[0].authority + } + + def httpMethodMatchIsSupported() { + httpAutoConfig { + interceptUrl '/secure*', 'DELETE', 'ROLE_SUPERVISOR' + interceptUrl '/secure*', 'POST', 'ROLE_A,ROLE_B' + interceptUrl '/**', 'ROLE_C' + } + createAppContext() + + def fids = getFilter(FilterSecurityInterceptor).getSecurityMetadataSource(); + def attrs = fids.getAttributes(createFilterinvocation("/secure", "POST")); + + expect: + attrs.size() == 2 + attrs.contains(new SecurityConfig("ROLE_A")) + attrs.contains(new SecurityConfig("ROLE_B")) + } + + def oncePerRequestAttributeIsSupported() { + xml.http('once-per-request': 'false') { + 'http-basic'() + } + createAppContext() + + expect: + !getFilter(FilterSecurityInterceptor).isObserveOncePerRequest() + } + + def httpBasicSupportsSeparateEntryPoint() { + xml.http() { + 'http-basic'('entry-point-ref': 'ep') + } + bean('ep', BasicAuthenticationEntryPoint.class.name, ['realmName':'whocares'],[:]) + createAppContext(); + + def baf = getFilter(BasicAuthenticationFilter) + def etf = getFilter(ExceptionTranslationFilter) + def ep = appContext.getBean("ep") + + expect: + baf.authenticationEntryPoint == ep + // Since no other authentication system is in use, this should also end up on the ETF + etf.authenticationEntryPoint == ep + } + + def interceptUrlWithRequiresChannelAddsChannelFilterToStack() { + httpAutoConfig { + 'intercept-url'(pattern: '/**', 'requires-channel': 'https') + } + createAppContext(); + List filters = getFilters("/someurl"); + + expect: + filters.size() == AUTO_CONFIG_FILTERS + 1 + filters[0] instanceof ChannelProcessingFilter + } + + def portMappingsAreParsedCorrectly() { + httpAutoConfig { + 'port-mappings'() { + 'port-mapping'(http: '9080', https: '9443') + } + } + createAppContext(); + + def pm = (appContext.getBeansOfType(PortMapperImpl).values() as List)[0]; + + expect: + pm.getTranslatedPortMappings().size() == 1 + pm.lookupHttpPort(9443) == 9080 + pm.lookupHttpsPort(9080) == 9443 + } + + def externalFiltersAreTreatedCorrectly() { + httpAutoConfig { + 'custom-filter'(position: 'FIRST', ref: '${customFilterRef}') + 'custom-filter'(after: 'LOGOUT_FILTER', ref: 'userFilter') + 'custom-filter'(before: 'SECURITY_CONTEXT_FILTER', ref: 'userFilter1') + } + bean('phc', PropertyPlaceholderConfigurer) + bean('userFilter', SecurityContextHolderAwareRequestFilter) + bean('userFilter1', SecurityContextPersistenceFilter) + + System.setProperty('customFilterRef', 'userFilter') + createAppContext(); + + def filters = getFilters("/someurl"); + + expect: + AUTO_CONFIG_FILTERS + 3 == filters.size(); + filters[0] instanceof SecurityContextHolderAwareRequestFilter + filters[1] instanceof SecurityContextPersistenceFilter + filters[4] instanceof SecurityContextHolderAwareRequestFilter + filters[1] instanceof SecurityContextPersistenceFilter + } + + def twoFiltersWithSameOrderAreRejected() { + when: + httpAutoConfig { + 'custom-filter'(position: 'LOGOUT_FILTER', ref: 'userFilter') + } + bean('userFilter', SecurityContextHolderAwareRequestFilter) + createAppContext(); + + then: + thrown(BeanDefinitionParsingException) + } + + def x509SupportAddsFilterAtExpectedPosition() { + httpAutoConfig { + x509() + } + createAppContext() + + def filters = getFilters("/someurl") + + expect: + getFilters("/someurl")[2] instanceof X509AuthenticationFilter + } + + def x509SubjectPrincipalRegexCanBeSetUsingPropertyPlaceholder() { + httpAutoConfig { + x509('subject-principal-regex':'${subject-principal-regex}') + } + bean('phc', PropertyPlaceholderConfigurer.class.name) + System.setProperty("subject-principal-regex", "uid=(.*),"); + createAppContext() + def filter = getFilter(X509AuthenticationFilter) + + expect: + filter.principalExtractor.subjectDnPattern.pattern() == "uid=(.*)," + } + + def invalidLogoutSuccessUrlIsDetected() { + when: + xml.http { + 'form-login'() + 'logout'('logout-success-url': 'noLeadingSlash') + } + createAppContext() + + then: + BeanCreationException e = thrown() + } + + def invalidLogoutUrlIsDetected() { + when: + xml.http { + 'logout'('logout-url': 'noLeadingSlash') + 'form-login'() + } + createAppContext() + + then: + BeanCreationException e = thrown(); + } + + def logoutSuccessHandlerIsSetCorrectly() { + xml.http { + 'form-login'() + 'logout'('success-handler-ref': 'logoutHandler') + } + bean('logoutHandler', SimpleUrlLogoutSuccessHandler) + createAppContext() + + LogoutFilter filter = getFilter(LogoutFilter); + + expect: + FieldUtils.getFieldValue(filter, "logoutSuccessHandler") == appContext.getBean("logoutHandler") + } + + def externalRequestCacheIsConfiguredCorrectly() { + httpAutoConfig { + 'request-cache'(ref: 'cache') + } + bean('cache', HttpSessionRequestCache.class.name) + createAppContext() + + expect: + appContext.getBean("cache") == getFilter(ExceptionTranslationFilter.class).requestCache + } + + def customEntryPointIsSupported() { + xml.http('auto-config': 'true', 'entry-point-ref': 'entryPoint') {} + bean('entryPoint', MockEntryPoint.class.name) + createAppContext() + + expect: + getFilter(ExceptionTranslationFilter).getAuthenticationEntryPoint() instanceof MockEntryPoint + } + + def disablingSessionProtectionRemovesSessionManagementFilterIfNoInvalidSessionUrlSet() { + httpAutoConfig { + 'session-management'('session-fixation-protection': 'none') + } + createAppContext() + + expect: + !(getFilters("/someurl")[8] instanceof SessionManagementFilter) + } + + def disablingSessionProtectionRetainsSessionManagementFilterInvalidSessionUrlSet() { + httpAutoConfig { + 'session-management'('session-fixation-protection': 'none', 'invalid-session-url': '/timeoutUrl') + } + createAppContext() + def filter = getFilters("/someurl")[8] + + expect: + filter instanceof SessionManagementFilter + filter.invalidSessionUrl == '/timeoutUrl' + } + + /** + * See SEC-750. If the http security post processor causes beans to be instantiated too eagerly, they way miss + * additional processing. In this method we have a UserDetailsService which is referenced from the namespace + * and also has a post processor registered which will modify it. + */ + def httpElementDoesntInterfereWithBeanPostProcessing() { + httpAutoConfig {} + xml.'authentication-manager'() { + 'authentication-provider'('user-service-ref': 'myUserService') + } + bean('myUserService', PostProcessedMockUserDetailsService) + bean('beanPostProcessor', MockUserServiceBeanPostProcessor) + createAppContext("") + + expect: + appContext.getBean("myUserService").getPostProcessorWasHere() == "Hello from the post processor!" + } + + /* SEC-934 */ + def supportsTwoIdenticalInterceptUrls() { + httpAutoConfig { + interceptUrl ('/someUrl', 'ROLE_A') + interceptUrl ('/someUrl', 'ROLE_B') + } + createAppContext() + def fis = getFilter(FilterSecurityInterceptor) + def fids = fis.securityMetadataSource + Collection attrs = fids.getAttributes(createFilterinvocation("/someurl", null)); + + expect: + attrs.size() == 1 + attrs.contains(new SecurityConfig("ROLE_B")) + } + + def supportsExternallyDefinedSecurityContextRepository() { + xml.http('create-session': 'always', 'security-context-repository-ref': 'repo') { + 'http-basic'() + } + bean('repo', HttpSessionSecurityContextRepository) + createAppContext() + + def filter = getFilter(SecurityContextPersistenceFilter) + + expect: + filter.repo == appContext.getBean('repo') + filter.forceEagerSessionCreation == true + } + + def expressionBasedAccessAllowsAndDeniesAccessAsExpected() { + setup: + xml.http('auto-config': 'true', 'use-expressions': 'true') { + interceptUrl('/secure*', "hasAnyRole('ROLE_A','ROLE_C')") + interceptUrl('/**', 'permitAll') + } + createAppContext() + + def fis = getFilter(FilterSecurityInterceptor) + def fids = fis.getSecurityMetadataSource() + Collection attrs = fids.getAttributes(createFilterinvocation("/secure", null)); + assert 1 == attrs.size() + + when: "Unprotected URL" + SecurityContextHolder.getContext().setAuthentication(new TestingAuthenticationToken("joe", "", "ROLE_A")); + fis.invoke(createFilterinvocation("/permitallurl", null)); + then: + notThrown(AccessDeniedException) + + when: "Invoking secure Url as a valid user" + fis.invoke(createFilterinvocation("/secure", null)); + then: + notThrown(AccessDeniedException) + + when: "User does not have the required role" + SecurityContextHolder.getContext().setAuthentication(new TestingAuthenticationToken("joe", "", "ROLE_B")); + fis.invoke(createFilterinvocation("/secure", null)); + then: + thrown(AccessDeniedException) + } + + def disablingUrlRewritingThroughTheNamespaceSetsCorrectPropertyOnContextRepo() { + xml.http('auto-config': 'true', 'disable-url-rewriting': 'true') + createAppContext() + + expect: + getFilter(SecurityContextPersistenceFilter).repo.disableUrlRewriting == true + } + + def userDetailsServiceInParentContextIsLocatedSuccessfully() { + when: + createAppContext() + httpAutoConfig { + 'remember-me' + } + appContext = new InMemoryXmlApplicationContext(writer.toString(), appContext) + + then: + notThrown(BeansException) + } + + def httpConfigWithNoAuthProvidersWorksOk() { + when: "Http config has no internal authentication providers" + xml.http() { + 'form-login'() + anonymous(enabled: 'false') + } + createAppContext() + FilterChainProxy fcp = appContext.getBean(BeanIds.FILTER_CHAIN_PROXY); + MockHttpServletRequest request = new MockHttpServletRequest("POST", "/j_spring_security_check"); + request.setServletPath("/j_spring_security_check"); + request.addParameter("j_username", "bob"); + request.addParameter("j_password", "bob"); + then: "App context creation and login request succeed" + fcp.doFilter(request, new MockHttpServletResponse(), new MockFilterChain()); + } +} + +class MockEntryPoint extends LoginUrlAuthenticationEntryPoint { + public MockEntryPoint() { + super.setLoginFormUrl("/notused"); + } +} diff --git a/config/src/test/groovy/org/springframework/security/config/http/PlaceHolderAndELConfigTests.groovy b/config/src/test/groovy/org/springframework/security/config/http/PlaceHolderAndELConfigTests.groovy new file mode 100644 index 0000000000..2d281d368f --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/http/PlaceHolderAndELConfigTests.groovy @@ -0,0 +1,154 @@ +package org.springframework.security.config.http + +import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer +import org.springframework.mock.web.MockFilterChain +import org.springframework.mock.web.MockHttpServletRequest +import org.springframework.mock.web.MockHttpServletResponse +import org.springframework.security.access.ConfigAttribute +import org.springframework.security.access.SecurityConfig +import org.springframework.security.util.FieldUtils +import org.springframework.security.web.PortMapperImpl +import org.springframework.security.web.access.ExceptionTranslationFilter +import org.springframework.security.web.access.channel.ChannelProcessingFilter +import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource +import org.springframework.security.web.access.intercept.FilterSecurityInterceptor +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter + +class PlaceHolderAndELConfigTests extends AbstractHttpConfigTests { + + void setup() { + // Add a PropertyPlaceholderConfigurer to the context for all the tests + xml.'b:bean'('class': PropertyPlaceholderConfigurer.class.name) + } + + def filtersEqualsNoneSupportsPlaceholderForPattern() { + System.setProperty("pattern.nofilters", "/unprotected"); + + httpAutoConfig() { + interceptUrlNoFilters('${pattern.nofilters}') + interceptUrl('/**', 'ROLE_A') + } + createAppContext() + + List filters = getFilters("/unprotected"); + + expect: + filters.size() == 0 + } + + // SEC-1201 + def interceptUrlsAndFormLoginSupportPropertyPlaceholders() { + System.setProperty("secure.Url", "/Secure"); + System.setProperty("secure.role", "ROLE_A"); + System.setProperty("login.page", "/loginPage"); + System.setProperty("default.target", "/defaultTarget"); + System.setProperty("auth.failure", "/authFailure"); + + xml.http { + interceptUrl('${secure.Url}', '${secure.role}') + interceptUrlNoFilters('${login.page}'); + 'form-login'('login-page':'${login.page}', 'default-target-url': '${default.target}', + 'authentication-failure-url':'${auth.failure}'); + } + createAppContext(); + + expect: + propertyValuesMatchPlaceholders() + getFilters("/loginPage").size() == 0 + } + + // SEC-1309 + def interceptUrlsAndFormLoginSupportEL() { + System.setProperty("secure.url", "/Secure"); + System.setProperty("secure.role", "ROLE_A"); + System.setProperty("login.page", "/loginPage"); + System.setProperty("default.target", "/defaultTarget"); + System.setProperty("auth.failure", "/authFailure"); + + xml.http { + interceptUrl("#{systemProperties['secure.url']}", "#{systemProperties['secure.role']}") + 'form-login'('login-page':"#{systemProperties['login.page']}", 'default-target-url': "#{systemProperties['default.target']}", + 'authentication-failure-url':"#{systemProperties['auth.failure']}"); + } + createAppContext() + + expect: + propertyValuesMatchPlaceholders() + } + + private void propertyValuesMatchPlaceholders() { + // Check the security attribute + def fis = getFilter(FilterSecurityInterceptor); + def fids = fis.getSecurityMetadataSource(); + Collection attrs = fids.getAttributes(createFilterinvocation("/secure", null)); + assert attrs.size() == 1 + assert attrs.contains(new SecurityConfig("ROLE_A")) + + // Check the form login properties are set + def apf = getFilter(UsernamePasswordAuthenticationFilter) + assert FieldUtils.getFieldValue(apf, "successHandler.defaultTargetUrl") == '/defaultTarget' + assert "/authFailure" == FieldUtils.getFieldValue(apf, "failureHandler.defaultFailureUrl") + + def etf = getFilter(ExceptionTranslationFilter) + assert "/loginPage"== etf.authenticationEntryPoint.loginFormUrl + } + + def portMappingsWorkWithPlaceholdersAndEL() { + System.setProperty("http", "9080"); + System.setProperty("https", "9443"); + + httpAutoConfig { + 'port-mappings'() { + 'port-mapping'(http: '#{systemProperties.http}', https: '${https}') + } + } + createAppContext(); + + def pm = (appContext.getBeansOfType(PortMapperImpl).values() as List)[0]; + + expect: + pm.getTranslatedPortMappings().size() == 1 + pm.lookupHttpPort(9443) == 9080 + pm.lookupHttpsPort(9080) == 9443 + } + + def requiresChannelSupportsPlaceholder() { + System.setProperty("secure.url", "/secure"); + System.setProperty("required.channel", "https"); + + httpAutoConfig { + 'intercept-url'(pattern: '${secure.url}', 'requires-channel': '${required.channel}') + } + createAppContext(); + List filters = getFilters("/secure"); + + expect: + filters.size() == AUTO_CONFIG_FILTERS + 1 + filters[0] instanceof ChannelProcessingFilter + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setServletPath("/secure"); + MockHttpServletResponse response = new MockHttpServletResponse(); + filters[0].doFilter(request, response, new MockFilterChain()); + response.getRedirectedUrl().startsWith("https") + } + + def accessDeniedPageWorksWithPlaceholders() { + System.setProperty("accessDenied", "/go-away"); + xml.http('auto-config': 'true', 'access-denied-page': '${accessDenied}') + createAppContext(); + + expect: + FieldUtils.getFieldValue(getFilter(ExceptionTranslationFilter.class), "accessDeniedHandler.errorPage") == '/go-away' + } + + def accessDeniedHandlerPageWorksWithEL() { + httpAutoConfig { + 'access-denied-handler'('error-page': "#{'/go' + '-away'}") + } + createAppContext() + + expect: + getFilter(ExceptionTranslationFilter).accessDeniedHandler.errorPage == '/go-away' + } + +} diff --git a/config/src/test/groovy/org/springframework/security/config/http/RememberMeConfigTests.groovy b/config/src/test/groovy/org/springframework/security/config/http/RememberMeConfigTests.groovy new file mode 100644 index 0000000000..35902df44f --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/http/RememberMeConfigTests.groovy @@ -0,0 +1,145 @@ +package org.springframework.security.config.http + +import static org.springframework.security.config.ConfigTestUtils.AUTH_PROVIDER_XML; + +import org.springframework.beans.factory.parsing.BeanDefinitionParsingException +import org.springframework.security.TestDataSource; +import org.springframework.security.authentication.ProviderManager +import org.springframework.security.authentication.RememberMeAuthenticationProvider +import org.springframework.security.config.BeanIds +import org.springframework.security.core.userdetails.MockUserDetailsService; +import org.springframework.security.util.FieldUtils +import org.springframework.security.web.authentication.logout.LogoutFilter +import org.springframework.security.web.authentication.logout.LogoutHandler +import org.springframework.security.web.authentication.rememberme.InMemoryTokenRepositoryImpl +import org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices +import org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter +import org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices + +/** + * + * @author Luke Taylor + */ +class RememberMeConfigTests extends AbstractHttpConfigTests { + + def rememberMeServiceWorksWithTokenRepoRef() { + httpAutoConfig () { + 'remember-me'('token-repository-ref': 'tokenRepo') + } + bean('tokenRepo', InMemoryTokenRepositoryImpl.class.name) + + createAppContext(AUTH_PROVIDER_XML) + + expect: + rememberMeServices() instanceof PersistentTokenBasedRememberMeServices + FieldUtils.getFieldValue(rememberMeServices(), "useSecureCookie") == false + } + + def rememberMeServiceWorksWithDataSourceRef() { + httpAutoConfig () { + 'remember-me'('data-source-ref': 'ds') + } + bean('ds', TestDataSource.class.name, ['tokendb']) + + createAppContext(AUTH_PROVIDER_XML) + + expect: + rememberMeServices() instanceof PersistentTokenBasedRememberMeServices + } + + def rememberMeServiceWorksWithExternalServicesImpl() { + httpAutoConfig () { + 'remember-me'('key': "#{'our' + 'key'}", 'services-ref': 'rms') + } + bean('rms', TokenBasedRememberMeServices.class.name, + ['key':'ourKey', 'tokenValiditySeconds':'5000'], ['userDetailsService':'us']) + + createAppContext(AUTH_PROVIDER_XML) + + List logoutHandlers = FieldUtils.getFieldValue(getFilter(LogoutFilter.class), "handlers"); + Map ams = appContext.getBeansOfType(ProviderManager.class); + ams.remove(BeanIds.AUTHENTICATION_MANAGER); + RememberMeAuthenticationProvider rmp = (ams.values() as List)[0].providers[1]; + + expect: + 5000 == FieldUtils.getFieldValue(rememberMeServices(), "tokenValiditySeconds") + // SEC-909 + logoutHandlers.size() == 2 + logoutHandlers.get(1) == rememberMeServices() + // SEC-1281 + rmp.key == "ourkey" + } + + def rememberMeTokenValidityIsParsedCorrectly() { + httpAutoConfig () { + 'remember-me'('key': 'ourkey', 'token-validity-seconds':'10000') + } + + createAppContext(AUTH_PROVIDER_XML) + expect: + rememberMeServices().tokenValiditySeconds == 10000 + } + + def 'Remember-me token validity allows negative value for non-persistent implementation'() { + httpAutoConfig () { + 'remember-me'('key': 'ourkey', 'token-validity-seconds':'-1') + } + + createAppContext(AUTH_PROVIDER_XML) + expect: + rememberMeServices().tokenValiditySeconds == -1 + } + + def rememberMeSecureCookieAttributeIsSetCorrectly() { + httpAutoConfig () { + 'remember-me'('key': 'ourkey', 'use-secure-cookie':'true') + } + + createAppContext(AUTH_PROVIDER_XML) + expect: + FieldUtils.getFieldValue(rememberMeServices(), "useSecureCookie") == true + } + + def 'Negative token-validity is rejected with persistent implementation'() { + when: + httpAutoConfig () { + 'remember-me'('key': 'ourkey', 'token-validity-seconds':'-1', 'token-repository-ref': 'tokenRepo') + } + bean('tokenRepo', InMemoryTokenRepositoryImpl.class.name) + createAppContext(AUTH_PROVIDER_XML) + + then: + BeanDefinitionParsingException e = thrown() + } + + def 'Custom user service is supported'() { + when: + httpAutoConfig () { + 'remember-me'('key': 'ourkey', 'token-validity-seconds':'-1', 'user-service-ref': 'userService') + } + bean('userService', MockUserDetailsService.class.name) + createAppContext(AUTH_PROVIDER_XML) + + then: "Parses OK" + notThrown BeanDefinitionParsingException + } + + // SEC-742 + def rememberMeWorksWithoutBasicProcessingFilter() { + when: + xml.http () { + 'form-login'('login-page': '/login.jsp', 'default-target-url': '/messageList.html' ) + logout('logout-success-url': '/login.jsp') + anonymous(username: 'guest', 'granted-authority': 'guest') + 'remember-me'() + } + createAppContext(AUTH_PROVIDER_XML) + + then: "Parses OK" + notThrown BeanDefinitionParsingException + } + + def rememberMeServices() { + getFilter(RememberMeAuthenticationFilter.class).getRememberMeServices() + } +} 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 new file mode 100644 index 0000000000..9d5fd9cf89 --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/http/SessionManagementConfigTests.groovy @@ -0,0 +1,175 @@ +package org.springframework.security.config.http + +import static org.junit.Assert.*; + +import groovy.lang.Closure; + +import javax.servlet.Filter; +import org.springframework.security.web.FilterChainProxy +import org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy; + +import org.springframework.mock.web.MockFilterChain +import org.springframework.mock.web.MockHttpServletRequest +import org.springframework.mock.web.MockHttpServletResponse +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken +import org.springframework.security.config.BeanIds +import org.springframework.security.core.context.SecurityContext +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.security.core.session.SessionRegistryImpl +import org.springframework.security.util.FieldUtils +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter +import org.springframework.security.web.context.NullSecurityContextRepository +import org.springframework.security.web.context.SaveContextOnUpdateOrErrorResponseWrapper +import org.springframework.security.web.context.SecurityContextPersistenceFilter +import org.springframework.security.web.savedrequest.RequestCacheAwareFilter +import org.springframework.security.web.session.ConcurrentSessionFilter +import org.springframework.security.web.session.SessionManagementFilter + + +/** + * Tests session-related functionality for the <http> namespace element and <session-management> + * + * @author Luke Taylor + */ +class SessionManagementConfigTests extends AbstractHttpConfigTests { + + def settingCreateSessionToAlwaysSetsFilterPropertiesCorrectly() { + httpCreateSession('always') { } + createAppContext(); + + def filter = getFilter(SecurityContextPersistenceFilter.class); + + expect: + filter.forceEagerSessionCreation == true + filter.repo.allowSessionCreation == true + filter.repo.disableUrlRewriting == false + } + + def settingCreateSessionToNeverSetsFilterPropertiesCorrectly() { + httpCreateSession('never') { } + createAppContext(); + + def filter = getFilter(SecurityContextPersistenceFilter.class); + + expect: + filter.forceEagerSessionCreation == false + filter.repo.allowSessionCreation == false + } + + def settingCreateSessionToStatelessSetsFilterPropertiesCorrectly() { + httpCreateSession('stateless') { } + createAppContext(); + + def filter = getFilter(SecurityContextPersistenceFilter.class); + + expect: + filter.forceEagerSessionCreation == false + filter.repo instanceof NullSecurityContextRepository + getFilter(SessionManagementFilter.class) == null + getFilter(RequestCacheAwareFilter.class) == null + } + + def settingCreateSessionToIfRequiredDoesntCreateASessionForPublicInvocation() { + httpCreateSession('ifRequired') { } + createAppContext(); + + def filter = getFilter(SecurityContextPersistenceFilter.class); + + expect: + filter.forceEagerSessionCreation == false + filter.repo.allowSessionCreation == true + } + + def httpCreateSession(String create, Closure c) { + xml.http(['auto-config': 'true', 'create-session': create], c) + } + + def concurrentSessionSupportAddsFilterAndExpectedBeans() { + httpAutoConfig { + 'session-management'() { + 'concurrency-control'('session-registry-alias':'sr', 'expired-url': '/expired') + } + } + createAppContext(); + List filters = getFilters("/someurl"); + + expect: + filters.get(0) instanceof ConcurrentSessionFilter + appContext.getBean("sr") != null + getFilter(SessionManagementFilter.class) != null + sessionRegistryIsValid(); + } + + def externalSessionStrategyIsSupported() { + when: + httpAutoConfig { + 'session-management'('session-authentication-strategy-ref':'ss') + } + bean('ss', SessionFixationProtectionStrategy.class.name) + createAppContext(); + + then: + notThrown(Exception.class) + } + + def externalSessionRegistryBeanIsConfiguredCorrectly() { + httpAutoConfig { + 'session-management'() { + 'concurrency-control'('session-registry-ref':'sr') + } + } + bean('sr', SessionRegistryImpl.class.name) + createAppContext(); + + expect: + sessionRegistryIsValid(); + } + + def sessionRegistryIsValid() { + Object sessionRegistry = appContext.getBean("sr"); + Object sessionRegistryFromConcurrencyFilter = FieldUtils.getFieldValue( + getFilter(ConcurrentSessionFilter.class), "sessionRegistry"); + Object sessionRegistryFromFormLoginFilter = FieldUtils.getFieldValue( + getFilter(UsernamePasswordAuthenticationFilter.class),"sessionStrategy.sessionRegistry"); + Object sessionRegistryFromMgmtFilter = FieldUtils.getFieldValue( + getFilter(SessionManagementFilter.class),"sessionStrategy.sessionRegistry"); + + assertSame(sessionRegistry, sessionRegistryFromConcurrencyFilter); + assertSame(sessionRegistry, sessionRegistryFromMgmtFilter); + // SEC-1143 + assertSame(sessionRegistry, sessionRegistryFromFormLoginFilter); + true; + } + + def concurrentSessionMaxSessionsIsCorrectlyConfigured() { + setup: + httpAutoConfig { + 'session-management'('session-authentication-error-url':'/max-exceeded') { + 'concurrency-control'('max-sessions': '2', 'error-if-maximum-exceeded':'true') + } + } + createAppContext(); + + def seshFilter = getFilter(SessionManagementFilter.class); + def auth = new UsernamePasswordAuthenticationToken("bob", "pass"); + SecurityContextHolder.getContext().setAuthentication(auth); + MockHttpServletResponse mockResponse = new MockHttpServletResponse(); + def response = new SaveContextOnUpdateOrErrorResponseWrapper(mockResponse, false) { + protected void saveContext(SecurityContext context) { + } + }; + when: "First session is established" + seshFilter.doFilter(new MockHttpServletRequest(), response, new MockFilterChain()); + then: "ok" + mockResponse.redirectedUrl == null + when: "Second session is established" + seshFilter.doFilter(new MockHttpServletRequest(), response, new MockFilterChain()); + then: "ok" + mockResponse.redirectedUrl == null + when: "Third session is established" + seshFilter.doFilter(new MockHttpServletRequest(), response, new MockFilterChain()); + then: "Rejected" + mockResponse.redirectedUrl == "/max-exceeded"; + } + +} diff --git a/config/src/test/java/org/springframework/security/config/http/HttpSecurityBeanDefinitionParserTests.java b/config/src/test/java/org/springframework/security/config/http/HttpSecurityBeanDefinitionParserTests.java index 450e229306..7961da9942 100644 --- a/config/src/test/java/org/springframework/security/config/http/HttpSecurityBeanDefinitionParserTests.java +++ b/config/src/test/java/org/springframework/security/config/http/HttpSecurityBeanDefinitionParserTests.java @@ -128,11 +128,6 @@ public class HttpSecurityBeanDefinitionParserTests { checkAutoConfigFilters(filterList); } - @Test(expected=BeanDefinitionParsingException.class) - public void duplicateElementCausesError() throws Exception { - setContext("" + AUTH_PROVIDER_XML); - } - private void checkAutoConfigFilters(List filterList) throws Exception { Iterator filters = filterList.iterator();