/*
 * Copyright 2012-2017 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.boot.autoconfigure.security;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import javax.servlet.Filter;

import org.hamcrest.Matchers;
import org.junit.After;
import org.junit.Test;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.context.WebApplicationContext;

import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;

/**
 * Tests for {@link SpringBootWebSecurityConfiguration}.
 *
 * @author Dave Syer
 * @author Rob Winch
 * @author Andy Wilkinson
 */
public class SpringBootWebSecurityConfigurationTests {

	private ConfigurableApplicationContext context;

	@After
	public void close() {
		if (this.context != null) {
			this.context.close();
		}
	}

	@Test
	public void testWebConfigurationOverrideGlobalAuthentication() throws Exception {
		this.context = SpringApplication.run(TestWebConfiguration.class,
				"--server.port=0");
		assertThat(this.context.getBean(AuthenticationManagerBuilder.class)).isNotNull();
		assertThat(this.context.getBean(AuthenticationManager.class)
				.authenticate(new UsernamePasswordAuthenticationToken("dave", "secret")))
						.isNotNull();
	}

	@Test
	public void testWebConfigurationFilterChainUnauthenticated() throws Exception {
		this.context = SpringApplication.run(VanillaWebConfiguration.class,
				"--server.port=0");
		MockMvc mockMvc = MockMvcBuilders
				.webAppContextSetup((WebApplicationContext) this.context)
				.addFilters(
						this.context.getBean("springSecurityFilterChain", Filter.class))
				.build();
		mockMvc.perform(MockMvcRequestBuilders.get("/"))
				.andExpect(MockMvcResultMatchers.status().isUnauthorized())
				.andExpect(MockMvcResultMatchers.header().string("www-authenticate",
						Matchers.containsString("realm=\"Spring\"")));
	}

	@Test
	public void testWebConfigurationFilterChainUnauthenticatedWithAuthorizeModeNone()
			throws Exception {
		this.context = SpringApplication.run(VanillaWebConfiguration.class,
				"--server.port=0", "--security.basic.authorize-mode=none");
		MockMvc mockMvc = MockMvcBuilders
				.webAppContextSetup((WebApplicationContext) this.context)
				.addFilters(
						this.context.getBean("springSecurityFilterChain", Filter.class))
				.build();
		mockMvc.perform(MockMvcRequestBuilders.get("/"))
				.andExpect(MockMvcResultMatchers.status().isNotFound());
	}

	@Test
	public void testWebConfigurationFilterChainUnauthenticatedWithAuthorizeModeAuthenticated()
			throws Exception {
		this.context = SpringApplication.run(VanillaWebConfiguration.class,
				"--server.port=0", "--security.basic.authorize-mode=authenticated");
		MockMvc mockMvc = MockMvcBuilders
				.webAppContextSetup((WebApplicationContext) this.context)
				.addFilters(
						this.context.getBean("springSecurityFilterChain", Filter.class))
				.build();
		mockMvc.perform(MockMvcRequestBuilders.get("/"))
				.andExpect(MockMvcResultMatchers.status().isUnauthorized())
				.andExpect(MockMvcResultMatchers.header().string("www-authenticate",
						Matchers.containsString("realm=\"Spring\"")));
	}

	@Test
	public void testWebConfigurationFilterChainBadCredentials() throws Exception {
		this.context = SpringApplication.run(VanillaWebConfiguration.class,
				"--server.port=0");
		MockMvc mockMvc = MockMvcBuilders
				.webAppContextSetup((WebApplicationContext) this.context)
				.addFilters(
						this.context.getBean("springSecurityFilterChain", Filter.class))
				.build();
		mockMvc.perform(
				MockMvcRequestBuilders.get("/").header("authorization", "Basic xxx"))
				.andExpect(MockMvcResultMatchers.status().isUnauthorized())
				.andExpect(MockMvcResultMatchers.header().string("www-authenticate",
						Matchers.containsString("realm=\"Spring\"")));
	}

	@Test
	public void testWebConfigurationInjectGlobalAuthentication() throws Exception {
		this.context = SpringApplication.run(TestInjectWebConfiguration.class,
				"--server.port=0");
		assertThat(this.context.getBean(AuthenticationManagerBuilder.class)).isNotNull();
		assertThat(this.context.getBean(AuthenticationManager.class)
				.authenticate(new UsernamePasswordAuthenticationToken("dave", "secret")))
						.isNotNull();
	}

	// gh-3447
	@Test
	public void testHiddenHttpMethodFilterOrderedFirst() throws Exception {
		this.context = SpringApplication.run(DenyPostRequestConfig.class,
				"--server.port=0");
		int port = Integer
				.parseInt(this.context.getEnvironment().getProperty("local.server.port"));
		TestRestTemplate rest = new TestRestTemplate();

		// not overriding causes forbidden
		MultiValueMap<String, String> form = new LinkedMultiValueMap<>();

		ResponseEntity<Object> result = rest
				.postForEntity("http://localhost:" + port + "/", form, Object.class);
		assertThat(result.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN);

		// override method with GET
		form = new LinkedMultiValueMap<>();
		form.add("_method", "GET");

		result = rest.postForEntity("http://localhost:" + port + "/", form, Object.class);
		assertThat(result.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
	}

	@Test
	public void defaultHeaderConfiguration() throws Exception {
		this.context = SpringApplication.run(VanillaWebConfiguration.class,
				"--server.port=0");
		MockMvc mockMvc = MockMvcBuilders
				.webAppContextSetup((WebApplicationContext) this.context)
				.addFilters((FilterChainProxy) this.context
						.getBean("springSecurityFilterChain", Filter.class))
				.build();
		mockMvc.perform(MockMvcRequestBuilders.get("/"))
				.andExpect(MockMvcResultMatchers.header().string("X-Content-Type-Options",
						is(notNullValue())))
				.andExpect(MockMvcResultMatchers.header().string("X-XSS-Protection",
						is(notNullValue())))
				.andExpect(MockMvcResultMatchers.header().string("Cache-Control",
						is(notNullValue())))
				.andExpect(MockMvcResultMatchers.header().string("X-Frame-Options",
						is(notNullValue())))
				.andExpect(MockMvcResultMatchers.header()
						.doesNotExist("Content-Security-Policy"));
	}

	@Test
	public void securityHeadersCanBeDisabled() throws Exception {
		this.context = SpringApplication.run(VanillaWebConfiguration.class,
				"--server.port=0", "--security.headers.content-type=false",
				"--security.headers.xss=false", "--security.headers.cache=false",
				"--security.headers.frame=false");

		MockMvc mockMvc = MockMvcBuilders
				.webAppContextSetup((WebApplicationContext) this.context)
				.addFilters(
						this.context.getBean("springSecurityFilterChain", Filter.class))
				.build();
		mockMvc.perform(MockMvcRequestBuilders.get("/"))
				.andExpect(MockMvcResultMatchers.status().isUnauthorized())
				.andExpect(MockMvcResultMatchers.header()
						.doesNotExist("X-Content-Type-Options"))
				.andExpect(
						MockMvcResultMatchers.header().doesNotExist("X-XSS-Protection"))
				.andExpect(MockMvcResultMatchers.header().doesNotExist("Cache-Control"))
				.andExpect(
						MockMvcResultMatchers.header().doesNotExist("X-Frame-Options"));
	}

	@Test
	public void contentSecurityPolicyConfiguration() throws Exception {
		this.context = SpringApplication.run(VanillaWebConfiguration.class,
				"--security.headers.content-security-policy=default-src 'self';",
				"--server.port=0");
		MockMvc mockMvc = MockMvcBuilders
				.webAppContextSetup((WebApplicationContext) this.context)
				.addFilters((FilterChainProxy) this.context
						.getBean("springSecurityFilterChain", Filter.class))
				.build();
		mockMvc.perform(MockMvcRequestBuilders.get("/"))
				.andExpect(MockMvcResultMatchers.header()
						.string("Content-Security-Policy", is("default-src 'self';")))
				.andExpect(MockMvcResultMatchers.header()
						.doesNotExist("Content-Security-Policy-Report-Only"));
	}

	@Test
	public void contentSecurityPolicyReportOnlyConfiguration() throws Exception {
		this.context = SpringApplication.run(VanillaWebConfiguration.class,
				"--security.headers.content-security-policy=default-src 'self';",
				"--security.headers.content-security-policy-mode=report-only",
				"--server.port=0");
		MockMvc mockMvc = MockMvcBuilders
				.webAppContextSetup((WebApplicationContext) this.context)
				.addFilters((FilterChainProxy) this.context
						.getBean("springSecurityFilterChain", Filter.class))
				.build();
		mockMvc.perform(MockMvcRequestBuilders.get("/"))
				.andExpect(MockMvcResultMatchers.header().string(
						"Content-Security-Policy-Report-Only", is("default-src 'self';")))
				.andExpect(MockMvcResultMatchers.header()
						.doesNotExist("Content-Security-Policy"));
	}

	@Configuration
	@Import(TestWebConfiguration.class)
	@Order(Ordered.LOWEST_PRECEDENCE)
	protected static class TestInjectWebConfiguration
			extends WebSecurityConfigurerAdapter {

		private final AuthenticationManagerBuilder auth;

		// It's a bad idea to inject an AuthenticationManager into a
		// WebSecurityConfigurerAdapter because it can cascade early instantiation,
		// unless you explicitly want the Boot default AuthenticationManager. It's
		// better to inject the builder, if you want the global AuthenticationManager. It
		// might even be necessary to wrap the builder in a lazy AuthenticationManager
		// (that calls getOrBuild() only when the AuthenticationManager is actually
		// called).
		protected TestInjectWebConfiguration(AuthenticationManagerBuilder auth) {
			this.auth = auth;
		}

		@Override
		public void init(WebSecurity web) throws Exception {
			this.auth.getOrBuild();
		}

	}

	@MinimalWebConfiguration
	@Import(SecurityAutoConfiguration.class)
	protected static class VanillaWebConfiguration {

	}

	@MinimalWebConfiguration
	@Import(SecurityAutoConfiguration.class)
	@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
	protected static class TestWebConfiguration extends WebSecurityConfigurerAdapter {

		@Autowired
		public void init(AuthenticationManagerBuilder auth) throws Exception {
			auth.inMemoryAuthentication().withUser("dave").password("secret")
					.roles("USER");
		}

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

	}

	@Configuration
	@Target(ElementType.TYPE)
	@Retention(RetentionPolicy.RUNTIME)
	@Documented
	@Import({ ServletWebServerFactoryAutoConfiguration.class,
			DispatcherServletAutoConfiguration.class, WebMvcAutoConfiguration.class,
			HttpMessageConvertersAutoConfiguration.class, ErrorMvcAutoConfiguration.class,
			PropertyPlaceholderAutoConfiguration.class })
	protected @interface MinimalWebConfiguration {

	}

	@MinimalWebConfiguration
	@Import(SecurityAutoConfiguration.class)
	protected static class DenyPostRequestConfig extends WebSecurityConfigurerAdapter {

		@Override
		protected void configure(HttpSecurity http) throws Exception {
			http.authorizeRequests().antMatchers(HttpMethod.POST, "/**").denyAll();
		}

	}

}
