/*
 * Copyright 2002-2016 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.access.intercept.aspectj.aspect;

import static org.assertj.core.api.Assertions.*;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import org.springframework.security.access.AccessDecisionVoter;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.annotation.Secured;
import org.springframework.security.access.annotation.SecuredAnnotationSecurityMetadataSource;
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
import org.springframework.security.access.expression.method.ExpressionBasedAnnotationAttributeFactory;
import org.springframework.security.access.expression.method.ExpressionBasedPostInvocationAdvice;
import org.springframework.security.access.expression.method.ExpressionBasedPreInvocationAdvice;
import org.springframework.security.access.intercept.AfterInvocationProviderManager;
import org.springframework.security.access.intercept.aspectj.AspectJMethodSecurityInterceptor;
import org.springframework.security.access.prepost.PostFilter;
import org.springframework.security.access.prepost.PostInvocationAdviceProvider;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.access.prepost.PreInvocationAuthorizationAdviceVoter;
import org.springframework.security.access.prepost.PrePostAnnotationSecurityMetadataSource;
import org.springframework.security.access.vote.AffirmativeBased;
import org.springframework.security.access.vote.RoleVoter;
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;

/**
 *
 * @author Luke Taylor
 * @since 3.0.3
 */
public class AnnotationSecurityAspectTests {
	private AffirmativeBased adm;
	private @Mock AuthenticationManager authman;
	private TestingAuthenticationToken anne = new TestingAuthenticationToken("anne", "",
			"ROLE_A");
	// private TestingAuthenticationToken bob = new TestingAuthenticationToken("bob", "",
	// "ROLE_B");
	private AspectJMethodSecurityInterceptor interceptor;
	private SecuredImpl secured = new SecuredImpl();
	private SecuredImplSubclass securedSub = new SecuredImplSubclass();
	private PrePostSecured prePostSecured = new PrePostSecured();

	@Before
	public final void setUp() throws Exception {
		MockitoAnnotations.initMocks(this);
		interceptor = new AspectJMethodSecurityInterceptor();
		AccessDecisionVoter[] voters = new AccessDecisionVoter[] {
				new RoleVoter(),
				new PreInvocationAuthorizationAdviceVoter(
						new ExpressionBasedPreInvocationAdvice()) };
		adm = new AffirmativeBased(
				Arrays.<AccessDecisionVoter<? extends Object>> asList(voters));
		interceptor.setAccessDecisionManager(adm);
		interceptor.setAuthenticationManager(authman);
		interceptor
				.setSecurityMetadataSource(new SecuredAnnotationSecurityMetadataSource());
		AnnotationSecurityAspect secAspect = AnnotationSecurityAspect.aspectOf();
		secAspect.setSecurityInterceptor(interceptor);
	}

	@After
	public void clearContext() {
		SecurityContextHolder.clearContext();
	}

	@Test
	public void securedInterfaceMethodAllowsAllAccess() throws Exception {
		secured.securedMethod();
	}

	@Test(expected = AuthenticationCredentialsNotFoundException.class)
	public void securedClassMethodDeniesUnauthenticatedAccess() throws Exception {
		secured.securedClassMethod();
	}

	@Test
	public void securedClassMethodAllowsAccessToRoleA() throws Exception {
		SecurityContextHolder.getContext().setAuthentication(anne);
		secured.securedClassMethod();
	}

	@Test(expected = AccessDeniedException.class)
	public void internalPrivateCallIsIntercepted() {
		SecurityContextHolder.getContext().setAuthentication(anne);

		try {
			secured.publicCallsPrivate();
			fail("Expected AccessDeniedException");
		}
		catch (AccessDeniedException expected) {
		}
		securedSub.publicCallsPrivate();
	}

	@Test(expected = AccessDeniedException.class)
	public void protectedMethodIsIntercepted() throws Exception {
		SecurityContextHolder.getContext().setAuthentication(anne);

		secured.protectedMethod();
	}

	@Test
	public void overriddenProtectedMethodIsNotIntercepted() throws Exception {
		// AspectJ doesn't inherit annotations
		securedSub.protectedMethod();
	}

	// SEC-1262
	@Test(expected = AccessDeniedException.class)
	public void denyAllPreAuthorizeDeniesAccess() throws Exception {
		configureForElAnnotations();
		SecurityContextHolder.getContext().setAuthentication(anne);
		prePostSecured.denyAllMethod();
	}

	@Test
	public void postFilterIsApplied() throws Exception {
		configureForElAnnotations();
		SecurityContextHolder.getContext().setAuthentication(anne);
		List<String> objects = prePostSecured.postFilterMethod();
		assertThat(objects).hasSize(2);
		assertThat(objects.contains("apple")).isTrue();
		assertThat(objects.contains("aubergine")).isTrue();
	}

	private void configureForElAnnotations() {
		DefaultMethodSecurityExpressionHandler eh = new DefaultMethodSecurityExpressionHandler();
		interceptor
				.setSecurityMetadataSource(new PrePostAnnotationSecurityMetadataSource(
						new ExpressionBasedAnnotationAttributeFactory(eh)));
		interceptor.setAccessDecisionManager(adm);
		AfterInvocationProviderManager aim = new AfterInvocationProviderManager();
		aim.setProviders(Arrays.asList(new PostInvocationAdviceProvider(
				new ExpressionBasedPostInvocationAdvice(eh))));
		interceptor.setAfterInvocationManager(aim);
	}
}

interface SecuredInterface {
	@Secured("ROLE_X")
	void securedMethod();
}

class SecuredImpl implements SecuredInterface {
	// Not really secured because AspectJ doesn't inherit annotations from interfaces
	public void securedMethod() {
	}

	@Secured("ROLE_A")
	public void securedClassMethod() {
	}

	@Secured("ROLE_X")
	private void privateMethod() {
	}

	@Secured("ROLE_X")
	protected void protectedMethod() {
	}

	@Secured("ROLE_X")
	public void publicCallsPrivate() {
		privateMethod();
	}
}

class SecuredImplSubclass extends SecuredImpl {
	protected void protectedMethod() {
	}

	public void publicCallsPrivate() {
		super.publicCallsPrivate();
	}
}

class PrePostSecured {
	@PreAuthorize("denyAll")
	public void denyAllMethod() {
	}

	@PostFilter("filterObject.startsWith('a')")
	public List<String> postFilterMethod() {
		ArrayList<String> objects = new ArrayList<>();
		objects.addAll(Arrays.asList(new String[] { "apple", "banana", "aubergine",
				"orange" }));
		return objects;
	}
}
