/*
 * Copyright 2002-2018 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.method.configuration;

import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.annotation.AdviceMode;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.PermissionEvaluator;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
import org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationTrustResolver;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.event.AbstractAuthenticationEvent;
import org.springframework.security.authentication.event.AuthenticationFailureBadCredentialsEvent;
import org.springframework.security.config.MockEventListener;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.authentication.configuration.GlobalAuthenticationConfigurerAdapter;
import org.springframework.security.config.core.GrantedAuthorityDefaults;
import org.springframework.security.config.test.SpringTestRule;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.test.context.annotation.SecurityTestExecutionListeners;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;

import javax.sql.DataSource;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

/**
 *
 * @author Rob Winch
 */
@RunWith(SpringJUnit4ClassRunner.class)
@SecurityTestExecutionListeners
public class GlobalMethodSecurityConfigurationTests {
	@Rule
	public final SpringTestRule spring = new SpringTestRule();

	@Autowired(required = false)
	private MethodSecurityService service;

	private AuthenticationManager authenticationManager;

	@Autowired
	public void setMethodInterceptor(MethodSecurityInterceptor interceptor) {
		this.authenticationManager = interceptor.getAuthenticationManager();
	}

	@Autowired(required = false)
	MockEventListener<AbstractAuthenticationEvent> events;

	@Test
	public void methodSecurityAuthenticationManagerPublishesEvent() {
		this.spring.register(InMemoryAuthWithGlobalMethodSecurityConfig.class).autowire();

		try {
			this.authenticationManager.authenticate(new UsernamePasswordAuthenticationToken("foo", "bar"));
		} catch(AuthenticationException e) {}

		assertThat(this.events.getEvents()).extracting(Object::getClass).containsOnly((Class) AuthenticationFailureBadCredentialsEvent.class);
	}

	@EnableGlobalMethodSecurity(prePostEnabled = true)
	public static class InMemoryAuthWithGlobalMethodSecurityConfig extends GlobalMethodSecurityConfiguration {
		@Override
		protected void configure(AuthenticationManagerBuilder auth) throws Exception {
			auth
				.inMemoryAuthentication();
		}

		@Bean
		public MockEventListener<AbstractAuthenticationEvent> listener() {
			return new MockEventListener<AbstractAuthenticationEvent>() {};
		}
	}

	@Test
	@WithMockUser
	public void methodSecurityWhenAuthenticationTrustResolverIsBeanThenAutowires() {
		this.spring.register(CustomTrustResolverConfig.class).autowire();

		AuthenticationTrustResolver trustResolver = this.spring.getContext().getBean(AuthenticationTrustResolver.class);
		when(trustResolver.isAnonymous(any())).thenReturn(true, false);

		assertThatThrownBy(() -> this.service.preAuthorizeNotAnonymous())
			.isInstanceOf(AccessDeniedException.class);

		this.service.preAuthorizeNotAnonymous();

		verify(trustResolver, atLeastOnce()).isAnonymous(any());
	}

	@EnableGlobalMethodSecurity(prePostEnabled = true)
	static class CustomTrustResolverConfig {

		@Bean
		public AuthenticationTrustResolver trustResolver() {
			return mock(AuthenticationTrustResolver.class);
		}

		@Bean
		public MethodSecurityServiceImpl service() {
			return new MethodSecurityServiceImpl();
		}
	}

	// SEC-2301
	@Test
	@WithMockUser
	public void defaultWebSecurityExpressionHandlerHasBeanResolverSet() {
		this.spring.register(ExpressionHandlerHasBeanResolverSetConfig.class).autowire();
		Authz authz = this.spring.getContext().getBean(Authz.class);

		assertThatThrownBy(() -> this.service.preAuthorizeBean(false))
			.isInstanceOf(AccessDeniedException.class);

		this.service.preAuthorizeBean(true);
	}

	@EnableGlobalMethodSecurity(prePostEnabled = true, proxyTargetClass = true)
	static class ExpressionHandlerHasBeanResolverSetConfig {

		@Bean
		public MethodSecurityServiceImpl service() {
			return new MethodSecurityServiceImpl();
		}

		@Bean
		public Authz authz() {
			return new Authz();
		}
	}

	@Test
	@WithMockUser
	public void methodSecuritySupportsAnnotaitonsOnInterfaceParamerNames() {
		this.spring.register(MethodSecurityServiceConfig.class).autowire();

		assertThatThrownBy(() -> this.service.postAnnotation("deny"))
			.isInstanceOf(AccessDeniedException.class);

		this.service.postAnnotation("grant");
		// no exception
	}

	@EnableGlobalMethodSecurity(prePostEnabled = true)
	static class MethodSecurityServiceConfig {

		@Bean
		public MethodSecurityService service() {
			return new MethodSecurityServiceImpl();
		}
	}

	@Test
	@WithMockUser
	public void globalMethodSecurityConfigurationAutowiresPermissionEvaluator() {
		this.spring.register(AutowirePermissionEvaluatorConfig.class).autowire();
		PermissionEvaluator permission = this.spring.getContext().getBean(PermissionEvaluator.class);
		when(permission.hasPermission(any(), eq("something"), eq("read"))).thenReturn(true, false);

		this.service.hasPermission("something");
		// no exception

		assertThatThrownBy(() -> this.service.hasPermission("something"))
			.isInstanceOf(AccessDeniedException.class);
	}

	@EnableGlobalMethodSecurity(prePostEnabled = true)
	public static class AutowirePermissionEvaluatorConfig {

		@Bean
		public PermissionEvaluator permissionEvaluator() {
			return mock(PermissionEvaluator.class);
		}

		@Bean
		public MethodSecurityService service() {
			return new MethodSecurityServiceImpl();
		}
	}

	@Test
	public void multiPermissionEvaluatorConfig() throws Exception {
		this.spring.register(MultiPermissionEvaluatorConfig.class).autowire();

		// no exception
	}

	@EnableGlobalMethodSecurity(prePostEnabled = true)
	public static class MultiPermissionEvaluatorConfig {

		@Bean
		public PermissionEvaluator permissionEvaluator() {
			return mock(PermissionEvaluator.class);
		}

		@Bean
		public PermissionEvaluator permissionEvaluator2() {
			return mock(PermissionEvaluator.class);
		}
	}

	// SEC-2425
	@Test
	@WithMockUser
	public void enableGlobalMethodSecurityWorksOnSuperclass() {
		this.spring.register(ChildConfig.class).autowire();

		assertThatThrownBy(() -> this.service.preAuthorize())
			.isInstanceOf(AccessDeniedException.class);
	}

	@Configuration
	static class ChildConfig extends ParentConfig {}

	@EnableGlobalMethodSecurity(prePostEnabled = true)
	static class ParentConfig {

		@Bean
		public MethodSecurityService service() {
			return new MethodSecurityServiceImpl();
		}
	}

	// SEC-2479
	@Test
	@WithMockUser
	public void supportAuthenticationManagerInParent() {
		try (AnnotationConfigWebApplicationContext parent = new AnnotationConfigWebApplicationContext()) {
			parent.register(Sec2479ParentConfig.class);
			parent.refresh();
			try (AnnotationConfigWebApplicationContext child = new AnnotationConfigWebApplicationContext()) {
				child.setParent(parent);
				child.register(Sec2479ChildConfig.class);
				child.refresh();
				this.spring.context(child).autowire();

				assertThatThrownBy(() -> this.service.preAuthorize())
					.isInstanceOf(AccessDeniedException.class);
			}
		}
	}

	@Configuration
	static class Sec2479ParentConfig {
		@Bean
		public AuthenticationManager am() {
			return mock(AuthenticationManager.class);
		}
	}

	@EnableGlobalMethodSecurity(prePostEnabled = true)
	static class Sec2479ChildConfig {
		@Bean
		public MethodSecurityService service() {
			return new MethodSecurityServiceImpl();
		}
	}

	@Test
	public void enableGlobalMethodSecurityDoesNotTriggerEagerInitializationOfBeansInGlobalAuthenticationConfigurer() {
		this.spring.register(Sec2815Config.class).autowire();

		MockBeanPostProcessor pp = this.spring.getContext().getBean(MockBeanPostProcessor.class);

		assertThat(pp.beforeInit).containsKeys("dataSource");
		assertThat(pp.afterInit).containsKeys("dataSource");
	}

	@EnableGlobalMethodSecurity(prePostEnabled = true)
	static class Sec2815Config {
		@Bean
		public MethodSecurityService service() {
			return new MethodSecurityServiceImpl();
		}

		@Bean
		public MockBeanPostProcessor mockBeanPostProcessor() {
			return new MockBeanPostProcessor();
		}

		@Bean
		public DataSource dataSource() {
			return mock(DataSource.class);
		}

		@Configuration
		static class AuthConfig extends GlobalAuthenticationConfigurerAdapter {
			@Autowired
			DataSource dataSource;

			@Override
			public void init(AuthenticationManagerBuilder auth) throws Exception {
				auth.inMemoryAuthentication();
			}
		}
	}

	static class MockBeanPostProcessor implements BeanPostProcessor {
		Map<String, Object> beforeInit = new HashMap<String, Object>();
		Map<String, Object> afterInit = new HashMap<String, Object>();

		@Override
		public Object postProcessBeforeInitialization(Object bean, String beanName) throws
			BeansException {
			this.beforeInit.put(beanName, bean);
			return bean;
		}

		@Override
		public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
			this.afterInit.put(beanName, bean);
			return bean;
		}
	}

	// SEC-3045
	@Test
	public void globalSecurityProxiesSecurity() {
		this.spring.register(Sec3005Config.class).autowire();

		assertThat(this.service.getClass()).matches(c-> !Proxy.isProxyClass(c), "is not proxy class");
	}

	@EnableGlobalMethodSecurity(prePostEnabled = true, mode= AdviceMode.ASPECTJ)
	@EnableTransactionManagement
	static class Sec3005Config {
		@Bean
		public MethodSecurityService service() {
			return new MethodSecurityServiceImpl();
		}

		@Autowired
		public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
			auth.inMemoryAuthentication();
		}
	}

	//
	//	// gh-3797
	//	def preAuthorizeBeanSpel() {
	//		setup:
	//			SecurityContextHolder.getContext().setAuthentication(
	//				new TestingAuthenticationToken("user", "password","ROLE_USER"))
	//			context = new AnnotationConfigApplicationContext(PreAuthorizeBeanSpelConfig)
	//			BeanSpelService service = context.getBean(BeanSpelService)
	//		when:
	//			service.run(true)
	//		then:
	//			noExceptionThrown()
	//		when:
	//			service.run(false)
	//		then:
	//			thrown(AccessDeniedException)
	//	}
	//

	@Test
	@WithMockUser
	public void preAuthorizeBeanSpel() {
		this.spring.register(PreAuthorizeBeanSpelConfig.class).autowire();

		assertThatThrownBy(() -> this.service.preAuthorizeBean(false))
			.isInstanceOf(AccessDeniedException.class);

		this.service.preAuthorizeBean(true);
	}

	@Configuration
	@EnableGlobalMethodSecurity(prePostEnabled = true)
	public static class PreAuthorizeBeanSpelConfig {
		@Bean
		MethodSecurityService service() {
			return new MethodSecurityServiceImpl();
		}

		@Bean
		Authz authz() {
			return new Authz();
		}
	}

	// gh-3394
	@Test
	@WithMockUser
	public void roleHierarchy() {
		this.spring.register(RoleHierarchyConfig.class).autowire();

		assertThatThrownBy(() -> this.service.preAuthorize())
			.isInstanceOf(AccessDeniedException.class);
		this.service.preAuthorizeAdmin();
	}

	@EnableGlobalMethodSecurity(prePostEnabled = true)
	@Configuration
	public static class RoleHierarchyConfig {
		@Bean
		MethodSecurityService service() {
			return new MethodSecurityServiceImpl();
		}

		@Bean
		RoleHierarchy roleHierarchy() {
			RoleHierarchyImpl result = new RoleHierarchyImpl();
			result.setHierarchy("ROLE_USER > ROLE_ADMIN");
			return result;
		}
	}

	@Test
	@WithMockUser(authorities = "ROLE:USER")
	public void grantedAuthorityDefaultsAutowires() {
		this.spring.register(CustomGrantedAuthorityConfig.class).autowire();

		CustomGrantedAuthorityConfig.CustomAuthorityService customService = this.spring.getContext().getBean(
			CustomGrantedAuthorityConfig.CustomAuthorityService.class);

		assertThatThrownBy(() -> this.service.preAuthorize())
			.isInstanceOf(AccessDeniedException.class);

		customService.customPrefixRoleUser();
		// no exception
	}

	@EnableGlobalMethodSecurity(prePostEnabled = true)
	static class CustomGrantedAuthorityConfig {

		@Bean
		public GrantedAuthorityDefaults ga() {
			return new GrantedAuthorityDefaults("ROLE:");
		}

		@Bean
		public CustomAuthorityService service() {
			return new CustomAuthorityService();
		}

		@Bean
		public MethodSecurityServiceImpl methodSecurityService() {
			return new MethodSecurityServiceImpl();
		}

		static class CustomAuthorityService {
			@PreAuthorize("hasRole('ROLE:USER')")
			public void customPrefixRoleUser() {}
		}
	}
}
