/*
 * Copyright 2002-2013 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.springframework.security.config.annotation.web.builders

import javax.servlet.http.HttpServletRequest

import org.springframework.context.annotation.Configuration
import org.springframework.security.access.AccessDecisionManager
import org.springframework.security.access.ConfigAttribute
import org.springframework.security.access.vote.AuthenticatedVoter
import org.springframework.security.access.vote.RoleVoter
import org.springframework.security.authentication.AuthenticationManager
import org.springframework.security.authentication.BadCredentialsException
import org.springframework.security.config.annotation.BaseSpringSpec
import org.springframework.security.config.annotation.web.builders.NamespaceHttpTests.AuthenticationManagerRefConfig.CustomAuthenticationManager
import org.springframework.security.config.annotation.web.builders.NamespaceHttpTests.RequestMatcherRefConfig.MyRequestMatcher
import org.springframework.security.config.annotation.web.configuration.BaseWebConfig
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.annotation.web.configurers.UrlAuthorizationConfigurer
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.Authentication
import org.springframework.security.core.AuthenticationException
import org.springframework.security.web.FilterInvocation
import org.springframework.security.web.access.ExceptionTranslationFilter
import org.springframework.security.web.access.expression.ExpressionBasedFilterInvocationSecurityMetadataSource
import org.springframework.security.web.access.expression.WebExpressionVoter
import org.springframework.security.web.access.intercept.DefaultFilterInvocationSecurityMetadataSource
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter
import org.springframework.security.web.context.HttpSessionSecurityContextRepository
import org.springframework.security.web.context.NullSecurityContextRepository
import org.springframework.security.web.context.SecurityContextPersistenceFilter
import org.springframework.security.web.jaasapi.JaasApiIntegrationFilter
import org.springframework.security.web.savedrequest.HttpSessionRequestCache
import org.springframework.security.web.savedrequest.NullRequestCache
import org.springframework.security.web.savedrequest.RequestCacheAwareFilter
import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter
import org.springframework.security.web.session.SessionManagementFilter
import org.springframework.security.web.util.matcher.RegexRequestMatcher
import org.springframework.security.web.util.matcher.RequestMatcher

/**
 * Tests to verify that all the functionality of <http> attributes is present
 *
 * @author Rob Winch
 *
 */
public class NamespaceHttpTests extends BaseSpringSpec {
    def "http@access-decision-manager-ref"() {
        setup:
            AccessDecisionManagerRefConfig.ACCESS_DECISION_MGR = Mock(AccessDecisionManager)
            AccessDecisionManagerRefConfig.ACCESS_DECISION_MGR.supports(FilterInvocation) >> true
            AccessDecisionManagerRefConfig.ACCESS_DECISION_MGR.supports(_ as ConfigAttribute) >> true
        when:
            loadConfig(AccessDecisionManagerRefConfig)
        then:
            findFilter(FilterSecurityInterceptor).accessDecisionManager == AccessDecisionManagerRefConfig.ACCESS_DECISION_MGR
    }

    @Configuration
    static class AccessDecisionManagerRefConfig extends BaseWebConfig {
        static AccessDecisionManager ACCESS_DECISION_MGR

        protected void configure(HttpSecurity http) throws Exception {
            http
                .authorizeRequests()
                    .anyRequest().permitAll()
                    .accessDecisionManager(ACCESS_DECISION_MGR)
        }
    }

    def "http@access-denied-page"() {
        when:
            loadConfig(AccessDeniedPageConfig)
        then:
            findFilter(ExceptionTranslationFilter).accessDeniedHandler.errorPage == "/AccessDeniedPageConfig"
    }

    @Configuration
    static class AccessDeniedPageConfig extends BaseWebConfig {
        protected void configure(HttpSecurity http) throws Exception {
            http
                .exceptionHandling()
                    .accessDeniedPage("/AccessDeniedPageConfig")
        }
    }

    def "http@authentication-manager-ref"() {
        when: "Specify AuthenticationManager"
            loadConfig(AuthenticationManagerRefConfig)
        then: "Populates the AuthenticationManager"
            findFilter(FilterSecurityInterceptor).authenticationManager.parent.class == CustomAuthenticationManager
    }

    @Configuration
    static class AuthenticationManagerRefConfig extends BaseWebConfig {
        // demo authentication-manager-ref (could be any value)

        @Override
        protected AuthenticationManager authenticationManager() throws Exception {
            return new CustomAuthenticationManager();
        }

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                .authorizeRequests()
                    .anyRequest().hasRole("USER");
        }

        static class CustomAuthenticationManager implements AuthenticationManager {
            public Authentication authenticate(Authentication authentication)
                    throws AuthenticationException {
                throw new BadCredentialsException("This always fails");
            }
        }
    }

    // Note: There is no http@auto-config equivalent in Java Config

    def "http@create-session=always"() {
        when:
            loadConfig(IfRequiredConfig)
        then:
            findFilter(SecurityContextPersistenceFilter).forceEagerSessionCreation == false
            findFilter(SecurityContextPersistenceFilter).repo.allowSessionCreation == true
            findFilter(SessionManagementFilter).securityContextRepository.allowSessionCreation == true
            findFilter(ExceptionTranslationFilter).requestCache.class == HttpSessionRequestCache
    }

    @Configuration
    static class CreateSessionAlwaysConfig extends BaseWebConfig {
        protected void configure(HttpSecurity http) throws Exception {
            http
                .sessionManagement()
                    .sessionCreationPolicy(SessionCreationPolicy.ALWAYS);
        }
    }

    def "http@create-session=stateless"() {
        when:
            loadConfig(CreateSessionStatelessConfig)
        then:
            findFilter(SecurityContextPersistenceFilter).forceEagerSessionCreation == false
            findFilter(SecurityContextPersistenceFilter).repo.class == NullSecurityContextRepository
            findFilter(SessionManagementFilter).securityContextRepository.class == NullSecurityContextRepository
            findFilter(ExceptionTranslationFilter).requestCache.class == NullRequestCache
            findFilter(RequestCacheAwareFilter).requestCache.class == NullRequestCache
    }

    @Configuration
    static class CreateSessionStatelessConfig extends BaseWebConfig {
        protected void configure(HttpSecurity http) throws Exception {
            http
                .sessionManagement()
                    .sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        }
    }

    def "http@create-session=ifRequired"() {
        when:
            loadConfig(IfRequiredConfig)
        then:
            findFilter(SecurityContextPersistenceFilter).forceEagerSessionCreation == false
            findFilter(SecurityContextPersistenceFilter).repo.allowSessionCreation == true
            findFilter(SessionManagementFilter).securityContextRepository.allowSessionCreation == true
    }

    @Configuration
    static class IfRequiredConfig extends BaseWebConfig {
        protected void configure(HttpSecurity http) throws Exception {
            http
                .sessionManagement()
                    .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED);
        }
    }

    def "http@create-session defaults to ifRequired"() {
        when:
            loadConfig(IfRequiredConfig)
        then:
            findFilter(SecurityContextPersistenceFilter).forceEagerSessionCreation == false
            findFilter(SecurityContextPersistenceFilter).repo.allowSessionCreation == true
            findFilter(SessionManagementFilter).securityContextRepository.allowSessionCreation == true
    }

    def "http@create-session=never"() {
        when:
            loadConfig(CreateSessionNeverConfig)
        then:
            findFilter(SecurityContextPersistenceFilter).forceEagerSessionCreation == false
            findFilter(SecurityContextPersistenceFilter).repo.allowSessionCreation == false
            findFilter(SessionManagementFilter).securityContextRepository.allowSessionCreation == false
    }

    @Configuration
    static class CreateSessionNeverConfig extends BaseWebConfig {
        protected void configure(HttpSecurity http) throws Exception {
            http
                .sessionManagement()
                    .sessionCreationPolicy(SessionCreationPolicy.NEVER);
        }
    }

    @Configuration
    static class DefaultCreateSessionConfig extends BaseWebConfig {
        protected void configure(HttpSecurity http) throws Exception {
        }
    }

    def "http@disable-url-rewriting = true (default for Java Config)"() {
        when:
            loadConfig(DefaultUrlRewritingConfig)
        then:
            findFilter(SecurityContextPersistenceFilter).repo.disableUrlRewriting
    }

    @Configuration
    static class DefaultUrlRewritingConfig extends BaseWebConfig {
        protected void configure(HttpSecurity http) throws Exception {
        }
    }

    // http@disable-url-rewriting is on by default to disable it create a custom HttpSecurityContextRepository and use security-context-repository-ref

    def "http@disable-url-rewriting = false"() {
        when:
            loadConfig(EnableUrlRewritingConfig)
        then:
            findFilter(SecurityContextPersistenceFilter).repo.disableUrlRewriting == false
    }

    @Configuration
    static class EnableUrlRewritingConfig extends BaseWebConfig {
        protected void configure(HttpSecurity http) throws Exception {
            HttpSessionSecurityContextRepository repository = new HttpSessionSecurityContextRepository()
            repository.disableUrlRewriting = false // explicitly configured (not necessary due to default values)

            http.
                securityContext()
                    .securityContextRepository(repository)
        }
    }

    def "http@entry-point-ref"() {
        when:
            loadConfig(EntryPointRefConfig)
        then:
            findFilter(ExceptionTranslationFilter).authenticationEntryPoint.loginFormUrl == "/EntryPointRefConfig"
    }

    @Configuration
    static class EntryPointRefConfig extends BaseWebConfig {
        protected void configure(HttpSecurity http) throws Exception {
            http
                .exceptionHandling()
                    .authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/EntryPointRefConfig"))
        }
    }

    def "http@jaas-api-provision"() {
        when:
            loadConfig(JaasApiProvisionConfig)
        then:
            findFilter(JaasApiIntegrationFilter)
    }

    @Configuration
    static class JaasApiProvisionConfig extends BaseWebConfig {
        protected void configure(HttpSecurity http) throws Exception {
            http
                .addFilter(new JaasApiIntegrationFilter())
        }
    }

    // http@name is not available since it can be done w/ standard bean configuration easily

    def "http@once-per-request=true"() {
        when:
            loadConfig(OncePerRequestConfig)
        then:
            findFilter(FilterSecurityInterceptor).observeOncePerRequest
    }

    @Configuration
    static class OncePerRequestConfig extends BaseWebConfig {
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                .authorizeRequests()
                    .anyRequest().hasRole("USER");
        }
    }

    def "http@once-per-request=false"() {
        when:
            loadConfig(OncePerRequestFalseConfig)
        then:
            !findFilter(FilterSecurityInterceptor).observeOncePerRequest
    }

    @Configuration
    static class OncePerRequestFalseConfig extends BaseWebConfig {
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.
                authorizeRequests()
                    .filterSecurityInterceptorOncePerRequest(false)
                    .antMatchers("/users**","/sessions/**").hasRole("ADMIN")
                    .antMatchers("/signup").permitAll()
                    .anyRequest().hasRole("USER");
        }
    }

    // http@path-type is not available (instead request matcher instances are used)

    // http@pattern is not available (instead see the tests http@request-matcher-ref ant or http@request-matcher-ref regex)

    def "http@realm"() {
        setup:
            loadConfig(RealmConfig)
        when:
            springSecurityFilterChain.doFilter(request,response,chain)
        then:
            response.getHeader("WWW-Authenticate") == 'Basic realm="RealmConfig"'
    }

    @Configuration
    static class RealmConfig extends BaseWebConfig {
        protected void configure(HttpSecurity http) throws Exception {
            http
                .authorizeRequests()
                    .anyRequest().authenticated()
                    .and()
                .httpBasic().realmName("RealmConfig")
        }
    }

    // http@request-matcher is not available (instead request matcher instances are used)

    def "http@request-matcher-ref ant"() {
        when:
            loadConfig(RequestMatcherAntConfig)
        then:
            filterChain(0).requestMatcher.pattern == "/api/**"
    }

    @Configuration
    static class RequestMatcherAntConfig extends BaseWebConfig {
        protected void configure(HttpSecurity http) throws Exception {
            http
                .antMatcher("/api/**")
        }
    }

    def "http@request-matcher-ref regex"() {
        when:
            loadConfig(RequestMatcherRegexConfig)
        then:
            filterChain(0).requestMatcher.class == RegexRequestMatcher
            filterChain(0).requestMatcher.pattern.matcher("/regex/a")
            filterChain(0).requestMatcher.pattern.matcher("/regex/b")
            !filterChain(0).requestMatcher.pattern.matcher("/regex1/b")
    }

    @Configuration
    static class RequestMatcherRegexConfig extends BaseWebConfig {
        protected void configure(HttpSecurity http) throws Exception {
            http
                .regexMatcher("/regex/.*")
        }
    }

    def "http@request-matcher-ref"() {
        when:
            loadConfig(RequestMatcherRefConfig)
        then:
            filterChain(0).requestMatcher.class == MyRequestMatcher
    }

    @Configuration
    static class RequestMatcherRefConfig extends BaseWebConfig {
        protected void configure(HttpSecurity http) throws Exception {
            http
                .requestMatcher(new MyRequestMatcher());
        }
        static class MyRequestMatcher implements RequestMatcher {
            public boolean matches(HttpServletRequest request) {
                return true;
            }
        }
    }

    def "http@security=none"() {
        when:
            loadConfig(SecurityNoneConfig)
        then:
            filterChain(0).requestMatcher.pattern == "/resources/**"
            filterChain(0).filters.empty
            filterChain(1).requestMatcher.pattern == "/public/**"
            filterChain(1).filters.empty
    }

    @Configuration
    static class SecurityNoneConfig extends BaseWebConfig {

        @Override
        public void configure(WebSecurity web)
                throws Exception {
            web
                .ignoring()
                    .antMatchers("/resources/**","/public/**")
        }

        @Override
        protected void configure(HttpSecurity http) throws Exception {}

    }

    def "http@security-context-repository-ref"() {
        when:
            loadConfig(SecurityContextRepoConfig)
        then:
            findFilter(SecurityContextPersistenceFilter).repo.class == NullSecurityContextRepository
    }

    @Configuration
    static class SecurityContextRepoConfig extends BaseWebConfig {
        protected void configure(HttpSecurity http) throws Exception {
            http
                .securityContext()
                    .securityContextRepository(new NullSecurityContextRepository()) // security-context-repository-ref
        }
    }

    def "http@servlet-api-provision=false"() {
        when:
            loadConfig(ServletApiProvisionConfig)
        then:
            findFilter(SecurityContextHolderAwareRequestFilter) == null
    }

    @Configuration
    static class ServletApiProvisionConfig extends BaseWebConfig {
        protected void configure(HttpSecurity http) throws Exception {
            http.servletApi().disable()
        }
    }

    def "http@servlet-api-provision defaults to true"() {
        when:
            loadConfig(ServletApiProvisionDefaultsConfig)
        then:
            findFilter(SecurityContextHolderAwareRequestFilter) != null
    }

    @Configuration
    static class ServletApiProvisionDefaultsConfig extends BaseWebConfig {
        protected void configure(HttpSecurity http) throws Exception {
        }
    }

    def "http@use-expressions=true"() {
        when:
            loadConfig(UseExpressionsConfig)
        then:
            findFilter(FilterSecurityInterceptor).securityMetadataSource.class == ExpressionBasedFilterInvocationSecurityMetadataSource
            findFilter(FilterSecurityInterceptor).accessDecisionManager.decisionVoters.collect { it.class } == [WebExpressionVoter]
    }

    @Configuration
    @EnableWebSecurity
    static class UseExpressionsConfig extends BaseWebConfig {
        protected void configure(HttpSecurity http) throws Exception {
            http
                .authorizeRequests()
                    .antMatchers("/users**","/sessions/**").hasRole("USER")
                    .antMatchers("/signup").permitAll()
                    .anyRequest().hasRole("USER")
        }
    }

    def "http@use-expressions=false"() {
        when:
            loadConfig(DisableUseExpressionsConfig)
        then:
            findFilter(FilterSecurityInterceptor).securityMetadataSource.class == DefaultFilterInvocationSecurityMetadataSource
            findFilter(FilterSecurityInterceptor).accessDecisionManager.decisionVoters.collect { it.class } == [RoleVoter, AuthenticatedVoter]
    }
}
