/*
* 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.config.method;
import static org.assertj.core.api.Assertions.*;
import static org.springframework.security.config.ConfigTestUtils.AUTH_PROVIDER_XML;
import java.util.ArrayList;
import java.util.List;
import org.junit.After;
import org.junit.Test;
import org.springframework.aop.Advisor;
import org.springframework.aop.framework.Advised;
import org.springframework.beans.BeansException;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.factory.parsing.BeanDefinitionParsingException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.support.AbstractXmlApplicationContext;
import org.springframework.context.support.StaticApplicationContext;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.access.annotation.BusinessService;
import org.springframework.security.access.annotation.ExpressionProtectedBusinessServiceImpl;
import org.springframework.security.access.intercept.AfterInvocationProviderManager;
import org.springframework.security.access.intercept.RunAsManagerImpl;
import org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor;
import org.springframework.security.access.intercept.aopalliance.MethodSecurityMetadataSourceAdvisor;
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.vote.AffirmativeBased;
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.config.ConfigTestUtils;
import org.springframework.security.config.PostProcessedMockUserDetailsService;
import org.springframework.security.config.util.InMemoryXmlApplicationContext;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.util.FieldUtils;
/**
* @author Ben Alex
* @author Luke Taylor
*/
public class GlobalMethodSecurityBeanDefinitionParserTests {
private final UsernamePasswordAuthenticationToken bob = new UsernamePasswordAuthenticationToken(
"bob", "bobspassword");
private AbstractXmlApplicationContext appContext;
private BusinessService target;
public void loadContext() {
setContext("<b:bean id='target' class='org.springframework.security.access.annotation.BusinessServiceImpl'/>"
+ "<global-method-security order='1001' proxy-target-class='false' >"
+ " <protect-pointcut expression='execution(* *.someUser*(..))' access='ROLE_USER'/>"
+ " <protect-pointcut expression='execution(* *.someAdmin*(..))' access='ROLE_ADMIN'/>"
+ "</global-method-security>" + ConfigTestUtils.AUTH_PROVIDER_XML);
target = (BusinessService) appContext.getBean("target");
}
@After
public void closeAppContext() {
if (appContext != null) {
appContext.close();
appContext = null;
}
SecurityContextHolder.clearContext();
target = null;
}
@Test(expected = AuthenticationCredentialsNotFoundException.class)
public void targetShouldPreventProtectedMethodInvocationWithNoContext() {
loadContext();
target.someUserMethod1();
}
@Test
public void targetShouldAllowProtectedMethodInvocationWithCorrectRole() {
loadContext();
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
"user", "password");
SecurityContextHolder.getContext().setAuthentication(token);
target.someUserMethod1();
// SEC-1213. Check the order
Advisor[] advisors = ((Advised) target).getAdvisors();
assertThat(advisors.length).isEqualTo(1);
assertThat(((MethodSecurityMetadataSourceAdvisor) advisors[0]).getOrder()).isEqualTo(1001);
}
@Test(expected = AccessDeniedException.class)
public void targetShouldPreventProtectedMethodInvocationWithIncorrectRole() {
loadContext();
TestingAuthenticationToken token = new TestingAuthenticationToken("Test",
"Password", "ROLE_SOMEOTHERROLE");
token.setAuthenticated(true);
SecurityContextHolder.getContext().setAuthentication(token);
target.someAdminMethod();
}
@Test
public void doesntInterfereWithBeanPostProcessing() {
setContext("<b:bean id='myUserService' class='org.springframework.security.config.PostProcessedMockUserDetailsService'/>"
+ "<global-method-security />"
+ "<authentication-manager>"
+ " <authentication-provider user-service-ref='myUserService'/>"
+ "</authentication-manager>"
+ "<b:bean id='beanPostProcessor' class='org.springframework.security.config.MockUserServiceBeanPostProcessor'/>");
PostProcessedMockUserDetailsService service = (PostProcessedMockUserDetailsService) appContext
.getBean("myUserService");
assertThat(service.getPostProcessorWasHere()).isEqualTo("Hello from the post processor!");
}
@Test(expected = AccessDeniedException.class)
public void worksWithAspectJAutoproxy() {
setContext("<global-method-security>"
+ " <protect-pointcut expression='execution(* org.springframework.security.config.*Service.*(..))'"
+ " access='ROLE_SOMETHING' />"
+ "</global-method-security>"
+ "<b:bean id='myUserService' class='org.springframework.security.config.PostProcessedMockUserDetailsService'/>"
+ "<aop:aspectj-autoproxy />" + "<authentication-manager>"
+ " <authentication-provider user-service-ref='myUserService'/>"
+ "</authentication-manager>");
UserDetailsService service = (UserDetailsService) appContext
.getBean("myUserService");
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
"Test", "Password",
AuthorityUtils.createAuthorityList("ROLE_SOMEOTHERROLE"));
SecurityContextHolder.getContext().setAuthentication(token);
service.loadUserByUsername("notused");
}
@Test
public void supportsMethodArgumentsInPointcut() {
setContext("<b:bean id='target' class='org.springframework.security.access.annotation.BusinessServiceImpl'/>"
+ "<global-method-security>"
+ " <protect-pointcut expression='execution(* org.springframework.security.access.annotation.BusinessService.someOther(String))' access='ROLE_ADMIN'/>"
+ " <protect-pointcut expression='execution(* org.springframework.security.access.annotation.BusinessService.*(..))' access='ROLE_USER'/>"
+ "</global-method-security>" + ConfigTestUtils.AUTH_PROVIDER_XML);
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken("user", "password"));
target = (BusinessService) appContext.getBean("target");
// someOther(int) should not be matched by someOther(String), but should require
// ROLE_USER
target.someOther(0);
try {
// String version should required admin role
target.someOther("somestring");
fail("Expected AccessDeniedException");
}
catch (AccessDeniedException expected) {
}
}
@Test
public void supportsBooleanPointcutExpressions() {
setContext("<b:bean id='target' class='org.springframework.security.access.annotation.BusinessServiceImpl'/>"
+ "<global-method-security>"
+ " <protect-pointcut expression="
+ " 'execution(* org.springframework.security.access.annotation.BusinessService.*(..)) "
+ " and not execution(* org.springframework.security.access.annotation.BusinessService.someOther(String)))' "
+ " access='ROLE_USER'/>"
+ "</global-method-security>"
+ AUTH_PROVIDER_XML);
target = (BusinessService) appContext.getBean("target");
// String method should not be protected
target.someOther("somestring");
// All others should require ROLE_USER
try {
target.someOther(0);
fail("Expected AuthenticationCredentialsNotFoundException");
}
catch (AuthenticationCredentialsNotFoundException expected) {
}
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken("user", "password"));
target.someOther(0);
}
@Test(expected = BeanDefinitionParsingException.class)
public void duplicateElementCausesError() {
setContext("<global-method-security />" + "<global-method-security />");
}
// SEC-936
@Test(expected = AccessDeniedException.class)
public void worksWithoutTargetOrClass() {
setContext("<global-method-security secured-annotations='enabled'/>"
+ "<b:bean id='businessService' class='org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean'>"
+ " <b:property name='serviceUrl' value='http://localhost:8080/SomeService'/>"
+ " <b:property name='serviceInterface' value='org.springframework.security.access.annotation.BusinessService'/>"
+ "</b:bean>" + AUTH_PROVIDER_XML);
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
"Test", "Password",
AuthorityUtils.createAuthorityList("ROLE_SOMEOTHERROLE"));
SecurityContextHolder.getContext().setAuthentication(token);
target = (BusinessService) appContext.getBean("businessService");
target.someUserMethod1();
}
// Expression configuration tests
@SuppressWarnings("unchecked")
@Test
public void expressionVoterAndAfterInvocationProviderUseSameExpressionHandlerInstance()
throws Exception {
setContext("<global-method-security pre-post-annotations='enabled'/>"
+ AUTH_PROVIDER_XML);
AffirmativeBased adm = (AffirmativeBased) appContext
.getBeansOfType(AffirmativeBased.class).values().toArray()[0];
List voters = (List) FieldUtils.getFieldValue(adm, "decisionVoters");
PreInvocationAuthorizationAdviceVoter mev = (PreInvocationAuthorizationAdviceVoter) voters
.get(0);
MethodSecurityMetadataSourceAdvisor msi = (MethodSecurityMetadataSourceAdvisor) appContext
.getBeansOfType(MethodSecurityMetadataSourceAdvisor.class).values()
.toArray()[0];
AfterInvocationProviderManager pm = (AfterInvocationProviderManager) ((MethodSecurityInterceptor) msi
.getAdvice()).getAfterInvocationManager();
PostInvocationAdviceProvider aip = (PostInvocationAdviceProvider) pm
.getProviders().get(0);
assertThat(FieldUtils.getFieldValue(mev, "preAdvice.expressionHandler")).isSameAs(FieldUtils
.getFieldValue(aip, "postAdvice.expressionHandler"));
}
@Test(expected = AccessDeniedException.class)
public void accessIsDeniedForHasRoleExpression() {
setContext("<global-method-security pre-post-annotations='enabled'/>"
+ "<b:bean id='target' class='org.springframework.security.access.annotation.ExpressionProtectedBusinessServiceImpl'/>"
+ AUTH_PROVIDER_XML);
SecurityContextHolder.getContext().setAuthentication(bob);
target = (BusinessService) appContext.getBean("target");
target.someAdminMethod();
}
@Test
public void beanNameExpressionPropertyIsSupported() {
setContext("<global-method-security pre-post-annotations='enabled' proxy-target-class='true'/>"
+ "<b:bean id='number' class='java.lang.Integer'>"
+ " <b:constructor-arg value='1294'/>"
+ "</b:bean>"
+ "<b:bean id='target' class='org.springframework.security.access.annotation.ExpressionProtectedBusinessServiceImpl'/>"
+ AUTH_PROVIDER_XML);
SecurityContextHolder.getContext().setAuthentication(bob);
ExpressionProtectedBusinessServiceImpl target = (ExpressionProtectedBusinessServiceImpl) appContext
.getBean("target");
target.methodWithBeanNamePropertyAccessExpression("x");
}
@Test
public void preAndPostFilterAnnotationsWorkWithLists() {
setContext("<global-method-security pre-post-annotations='enabled'/>"
+ "<b:bean id='target' class='org.springframework.security.access.annotation.ExpressionProtectedBusinessServiceImpl'/>"
+ AUTH_PROVIDER_XML);
SecurityContextHolder.getContext().setAuthentication(bob);
target = (BusinessService) appContext.getBean("target");
List<String> arg = new ArrayList<String>();
arg.add("joe");
arg.add("bob");
arg.add("sam");
List<?> result = target.methodReturningAList(arg);
// Expression is (filterObject == name or filterObject == 'sam'), so "joe" should
// be gone after pre-filter
// PostFilter should remove sam from the return object
assertThat(result).hasSize(1);
assertThat(result.get(0)).isEqualTo("bob");
}
@Test
public void prePostFilterAnnotationWorksWithArrays() {
setContext("<global-method-security pre-post-annotations='enabled'/>"
+ "<b:bean id='target' class='org.springframework.security.access.annotation.ExpressionProtectedBusinessServiceImpl'/>"
+ AUTH_PROVIDER_XML);
SecurityContextHolder.getContext().setAuthentication(bob);
target = (BusinessService) appContext.getBean("target");
Object[] arg = new String[] { "joe", "bob", "sam" };
Object[] result = target.methodReturningAnArray(arg);
assertThat(result.length).isEqualTo(1);
assertThat(result[0]).isEqualTo("bob");
}
// SEC-1392
@Test
public void customPermissionEvaluatorIsSupported() throws Exception {
setContext("<global-method-security pre-post-annotations='enabled'>"
+ " <expression-handler ref='expressionHandler'/>"
+ "</global-method-security>"
+ "<b:bean id='expressionHandler' class='org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler'>"
+ " <b:property name='permissionEvaluator' ref='myPermissionEvaluator'/>"
+ "</b:bean>"
+ "<b:bean id='myPermissionEvaluator' class='org.springframework.security.config.method.TestPermissionEvaluator'/>"
+ AUTH_PROVIDER_XML);
}
// SEC-1450
@Test(expected = AuthenticationException.class)
@SuppressWarnings("unchecked")
public void genericsAreMatchedByProtectPointcut() throws Exception {
setContext("<b:bean id='target' class='org.springframework.security.config.method.GlobalMethodSecurityBeanDefinitionParserTests$ConcreteFoo'/>"
+ "<global-method-security>"
+ " <protect-pointcut expression='execution(* org..*Foo.foo(..))' access='ROLE_USER'/>"
+ "</global-method-security>" + AUTH_PROVIDER_XML);
Foo foo = (Foo) appContext.getBean("target");
foo.foo(new SecurityConfig("A"));
}
// SEC-1448
@Test
@SuppressWarnings("unchecked")
public void genericsMethodArgumentNamesAreResolved() throws Exception {
setContext("<b:bean id='target' class='" + ConcreteFoo.class.getName() + "'/>"
+ "<global-method-security pre-post-annotations='enabled'/>"
+ AUTH_PROVIDER_XML);
SecurityContextHolder.getContext().setAuthentication(bob);
Foo foo = (Foo) appContext.getBean("target");
foo.foo(new SecurityConfig("A"));
}
@Test
public void runAsManagerIsSetCorrectly() throws Exception {
StaticApplicationContext parent = new StaticApplicationContext();
MutablePropertyValues props = new MutablePropertyValues();
props.addPropertyValue("key", "blah");
parent.registerSingleton("runAsMgr", RunAsManagerImpl.class, props);
parent.refresh();
setContext("<global-method-security run-as-manager-ref='runAsMgr'/>"
+ AUTH_PROVIDER_XML, parent);
RunAsManagerImpl ram = (RunAsManagerImpl) appContext.getBean("runAsMgr");
MethodSecurityMetadataSourceAdvisor msi = (MethodSecurityMetadataSourceAdvisor) appContext
.getBeansOfType(MethodSecurityMetadataSourceAdvisor.class).values()
.toArray()[0];
assertThat(ram).isSameAs(FieldUtils.getFieldValue(msi.getAdvice(), "runAsManager"));
}
@Test
@SuppressWarnings("unchecked")
public void supportsExternalMetadataSource() throws Exception {
setContext("<b:bean id='target' class='"
+ ConcreteFoo.class.getName()
+ "'/>"
+ "<method-security-metadata-source id='mds'>"
+ " <protect method='"
+ Foo.class.getName()
+ ".foo' access='ROLE_ADMIN'/>"
+ "</method-security-metadata-source>"
+ "<global-method-security pre-post-annotations='enabled' metadata-source-ref='mds'/>"
+ AUTH_PROVIDER_XML);
// External MDS should take precedence over PreAuthorize
SecurityContextHolder.getContext().setAuthentication(bob);
Foo foo = (Foo) appContext.getBean("target");
try {
foo.foo(new SecurityConfig("A"));
fail("Bob can't invoke admin methods");
}
catch (AccessDeniedException expected) {
}
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken("admin", "password"));
foo.foo(new SecurityConfig("A"));
}
@Test
public void supportsCustomAuthenticationManager() throws Exception {
setContext("<b:bean id='target' class='"
+ ConcreteFoo.class.getName()
+ "'/>"
+ "<method-security-metadata-source id='mds'>"
+ " <protect method='"
+ Foo.class.getName()
+ ".foo' access='ROLE_ADMIN'/>"
+ "</method-security-metadata-source>"
+ "<global-method-security pre-post-annotations='enabled' metadata-source-ref='mds' authentication-manager-ref='customAuthMgr'/>"
+ "<b:bean id='customAuthMgr' class='org.springframework.security.config.method.GlobalMethodSecurityBeanDefinitionParserTests$CustomAuthManager'>"
+ " <b:constructor-arg value='authManager'/>" + "</b:bean>"
+ AUTH_PROVIDER_XML);
SecurityContextHolder.getContext().setAuthentication(bob);
Foo foo = (Foo) appContext.getBean("target");
try {
foo.foo(new SecurityConfig("A"));
fail("Bob can't invoke admin methods");
}
catch (AccessDeniedException expected) {
}
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken("admin", "password"));
foo.foo(new SecurityConfig("A"));
}
static class CustomAuthManager implements AuthenticationManager,
ApplicationContextAware {
private String beanName;
private AuthenticationManager authenticationManager;
CustomAuthManager(String beanName) {
this.beanName = beanName;
}
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
return authenticationManager.authenticate(authentication);
}
/*
* (non-Javadoc)
*
* @see
* org.springframework.context.ApplicationContextAware#setApplicationContext(org
* .springframework.context.ApplicationContext)
*/
public void setApplicationContext(ApplicationContext applicationContext)
throws BeansException {
this.authenticationManager = applicationContext.getBean(beanName,
AuthenticationManager.class);
}
}
private void setContext(String context) {
appContext = new InMemoryXmlApplicationContext(context);
}
private void setContext(String context, ApplicationContext parent) {
appContext = new InMemoryXmlApplicationContext(context, parent);
}
interface Foo<T extends ConfigAttribute> {
void foo(T action);
}
public static class ConcreteFoo implements Foo<SecurityConfig> {
@PreAuthorize("#action.attribute == 'A'")
public void foo(SecurityConfig action) {
}
}
}