/* * 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) { } } }