Ver código fonte

Refactoring HTTP config tests to use spock and groovy MarkupBuilder

Luke Taylor 15 anos atrás
pai
commit
b0758dd8de

+ 1 - 0
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')
     }
 }
 

+ 7 - 1
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"

+ 71 - 0
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);
+    }
+}

+ 68 - 0
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());
+    }
+}

+ 71 - 0
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)
+    }
+}

+ 71 - 0
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")
+    }
+}

+ 149 - 0
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<String> returnToUrlParameters = new HashSet<String>()
+        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
+    }
+}

+ 497 - 0
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<Filter> 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");
+    }
+}

+ 154 - 0
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'
+    }
+
+}

+ 145 - 0
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()
+    }
+}

+ 175 - 0
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 &lt;http&gt; namespace element and &lt;session-management&gt;
+ *
+ * @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";
+    }
+
+}

+ 0 - 5
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("<http auto-config='true' /><http auto-config='true' />" + AUTH_PROVIDER_XML);
-    }
-
     private void checkAutoConfigFilters(List<Filter> filterList) throws Exception {
         Iterator<Filter> filters = filterList.iterator();