Add SecurityContextHolderStrategy Java Configuration for Method Security

Issue gh-11061
This commit is contained in:
Josh Cummings 2022-06-21 17:11:43 -06:00
parent 7a9c873d7d
commit 74d646f569
No known key found for this signature in database
GPG Key ID: A306A51F43B8E5A5
7 changed files with 142 additions and 13 deletions

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2021 the original author or authors. * Copyright 2002-2022 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -73,6 +73,8 @@ import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.core.GrantedAuthorityDefaults; import org.springframework.security.config.core.GrantedAuthorityDefaults;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextHolderStrategy;
import org.springframework.util.Assert; import org.springframework.util.Assert;
/** /**
@ -101,6 +103,9 @@ public class GlobalMethodSecurityConfiguration implements ImportAware, SmartInit
}; };
private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder
.getContextHolderStrategy();
private DefaultMethodSecurityExpressionHandler defaultMethodExpressionHandler = new DefaultMethodSecurityExpressionHandler(); private DefaultMethodSecurityExpressionHandler defaultMethodExpressionHandler = new DefaultMethodSecurityExpressionHandler();
private AuthenticationManager authenticationManager; private AuthenticationManager authenticationManager;
@ -143,6 +148,7 @@ public class GlobalMethodSecurityConfiguration implements ImportAware, SmartInit
this.methodSecurityInterceptor.setAccessDecisionManager(accessDecisionManager()); this.methodSecurityInterceptor.setAccessDecisionManager(accessDecisionManager());
this.methodSecurityInterceptor.setAfterInvocationManager(afterInvocationManager()); this.methodSecurityInterceptor.setAfterInvocationManager(afterInvocationManager());
this.methodSecurityInterceptor.setSecurityMetadataSource(methodSecurityMetadataSource); this.methodSecurityInterceptor.setSecurityMetadataSource(methodSecurityMetadataSource);
this.methodSecurityInterceptor.setSecurityContextHolderStrategy(this.securityContextHolderStrategy);
RunAsManager runAsManager = runAsManager(); RunAsManager runAsManager = runAsManager();
if (runAsManager != null) { if (runAsManager != null) {
this.methodSecurityInterceptor.setRunAsManager(runAsManager); this.methodSecurityInterceptor.setRunAsManager(runAsManager);
@ -411,6 +417,12 @@ public class GlobalMethodSecurityConfiguration implements ImportAware, SmartInit
this.expressionHandler = handlers.get(0); this.expressionHandler = handlers.get(0);
} }
@Autowired(required = false)
void setSecurityContextHolderStrategy(SecurityContextHolderStrategy securityContextHolderStrategy) {
Assert.notNull(securityContextHolderStrategy, "securityContextHolderStrategy cannot be null");
this.securityContextHolderStrategy = securityContextHolderStrategy;
}
@Override @Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException { public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.context = beanFactory; this.context = beanFactory;

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2021 the original author or authors. * Copyright 2002-2022 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -25,6 +25,8 @@ import org.springframework.context.annotation.Role;
import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor; import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor;
import org.springframework.security.authorization.method.Jsr250AuthorizationManager; import org.springframework.security.authorization.method.Jsr250AuthorizationManager;
import org.springframework.security.config.core.GrantedAuthorityDefaults; import org.springframework.security.config.core.GrantedAuthorityDefaults;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextHolderStrategy;
/** /**
* {@link Configuration} for enabling JSR-250 Spring Security Method Security. * {@link Configuration} for enabling JSR-250 Spring Security Method Security.
@ -40,10 +42,16 @@ final class Jsr250MethodSecurityConfiguration {
private final Jsr250AuthorizationManager jsr250AuthorizationManager = new Jsr250AuthorizationManager(); private final Jsr250AuthorizationManager jsr250AuthorizationManager = new Jsr250AuthorizationManager();
private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder
.getContextHolderStrategy();
@Bean @Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE) @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
Advisor jsr250AuthorizationMethodInterceptor() { Advisor jsr250AuthorizationMethodInterceptor() {
return AuthorizationManagerBeforeMethodInterceptor.jsr250(this.jsr250AuthorizationManager); AuthorizationManagerBeforeMethodInterceptor interceptor = AuthorizationManagerBeforeMethodInterceptor
.jsr250(this.jsr250AuthorizationManager);
interceptor.setSecurityContextHolderStrategy(this.securityContextHolderStrategy);
return interceptor;
} }
@Autowired(required = false) @Autowired(required = false)
@ -51,4 +59,9 @@ final class Jsr250MethodSecurityConfiguration {
this.jsr250AuthorizationManager.setRolePrefix(grantedAuthorityDefaults.getRolePrefix()); this.jsr250AuthorizationManager.setRolePrefix(grantedAuthorityDefaults.getRolePrefix());
} }
@Autowired(required = false)
void setSecurityContextHolderStrategy(SecurityContextHolderStrategy securityContextHolderStrategy) {
this.securityContextHolderStrategy = securityContextHolderStrategy;
}
} }

View File

@ -34,6 +34,7 @@ import org.springframework.security.authorization.method.PostFilterAuthorization
import org.springframework.security.authorization.method.PreAuthorizeAuthorizationManager; import org.springframework.security.authorization.method.PreAuthorizeAuthorizationManager;
import org.springframework.security.authorization.method.PreFilterAuthorizationMethodInterceptor; import org.springframework.security.authorization.method.PreFilterAuthorizationMethodInterceptor;
import org.springframework.security.config.core.GrantedAuthorityDefaults; import org.springframework.security.config.core.GrantedAuthorityDefaults;
import org.springframework.security.core.context.SecurityContextHolderStrategy;
/** /**
* Base {@link Configuration} for enabling Spring Security Method Security. * Base {@link Configuration} for enabling Spring Security Method Security.
@ -109,6 +110,14 @@ final class PrePostMethodSecurityConfiguration {
this.postFilterAuthorizationMethodInterceptor.setExpressionHandler(methodSecurityExpressionHandler); this.postFilterAuthorizationMethodInterceptor.setExpressionHandler(methodSecurityExpressionHandler);
} }
@Autowired(required = false)
void setSecurityContextHolderStrategy(SecurityContextHolderStrategy strategy) {
this.preFilterAuthorizationMethodInterceptor.setSecurityContextHolderStrategy(strategy);
this.preAuthorizeAuthorizationMethodInterceptor.setSecurityContextHolderStrategy(strategy);
this.postAuthorizeAuthorizaitonMethodInterceptor.setSecurityContextHolderStrategy(strategy);
this.postFilterAuthorizationMethodInterceptor.setSecurityContextHolderStrategy(strategy);
}
@Autowired(required = false) @Autowired(required = false)
void setGrantedAuthorityDefaults(GrantedAuthorityDefaults grantedAuthorityDefaults) { void setGrantedAuthorityDefaults(GrantedAuthorityDefaults grantedAuthorityDefaults) {
this.expressionHandler.setDefaultRolePrefix(grantedAuthorityDefaults.getRolePrefix()); this.expressionHandler.setDefaultRolePrefix(grantedAuthorityDefaults.getRolePrefix());

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2021 the original author or authors. * Copyright 2002-2022 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -17,12 +17,15 @@
package org.springframework.security.config.annotation.method.configuration; package org.springframework.security.config.annotation.method.configuration;
import org.springframework.aop.Advisor; import org.springframework.aop.Advisor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Role; import org.springframework.context.annotation.Role;
import org.springframework.security.access.annotation.Secured; import org.springframework.security.access.annotation.Secured;
import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor; import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextHolderStrategy;
/** /**
* {@link Configuration} for enabling {@link Secured} Spring Security Method Security. * {@link Configuration} for enabling {@link Secured} Spring Security Method Security.
@ -36,10 +39,20 @@ import org.springframework.security.authorization.method.AuthorizationManagerBef
@Role(BeanDefinition.ROLE_INFRASTRUCTURE) @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
final class SecuredMethodSecurityConfiguration { final class SecuredMethodSecurityConfiguration {
private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder
.getContextHolderStrategy();
@Bean @Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE) @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
Advisor securedAuthorizationMethodInterceptor() { Advisor securedAuthorizationMethodInterceptor() {
return AuthorizationManagerBeforeMethodInterceptor.secured(); AuthorizationManagerBeforeMethodInterceptor interceptor = AuthorizationManagerBeforeMethodInterceptor.secured();
interceptor.setSecurityContextHolderStrategy(this.securityContextHolderStrategy);
return interceptor;
}
@Autowired(required = false)
void setSecurityContextHolderStrategy(SecurityContextHolderStrategy securityContextHolderStrategy) {
this.securityContextHolderStrategy = securityContextHolderStrategy;
} }
} }

View File

@ -50,19 +50,24 @@ import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.authorization.method.AuthorizationInterceptorsOrder; import org.springframework.security.authorization.method.AuthorizationInterceptorsOrder;
import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor; import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor;
import org.springframework.security.authorization.method.MethodInvocationResult; import org.springframework.security.authorization.method.MethodInvocationResult;
import org.springframework.security.config.annotation.SecurityContextChangedListenerConfig;
import org.springframework.security.config.core.GrantedAuthorityDefaults; import org.springframework.security.config.core.GrantedAuthorityDefaults;
import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.config.test.SpringTestContext;
import org.springframework.security.config.test.SpringTestContextExtension; import org.springframework.security.config.test.SpringTestContextExtension;
import org.springframework.security.config.test.SpringTestParentApplicationContextExecutionListener;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolderStrategy;
import org.springframework.security.test.context.annotation.SecurityTestExecutionListeners;
import org.springframework.security.test.context.support.WithAnonymousUser; import org.springframework.security.test.context.support.WithAnonymousUser;
import org.springframework.security.test.context.support.WithMockUser; import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.context.junit.jupiter.SpringExtension;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
@ -73,7 +78,9 @@ import static org.mockito.Mockito.verify;
* @author Josh Cummings * @author Josh Cummings
*/ */
@ExtendWith({ SpringExtension.class, SpringTestContextExtension.class }) @ExtendWith({ SpringExtension.class, SpringTestContextExtension.class })
@SecurityTestExecutionListeners @ContextConfiguration(classes = SecurityContextChangedListenerConfig.class)
@TestExecutionListeners(listeners = { WithSecurityContextTestExecutionListener.class,
SpringTestParentApplicationContextExecutionListener.class })
public class PrePostMethodSecurityConfigurationTests { public class PrePostMethodSecurityConfigurationTests {
public final SpringTestContext spring = new SpringTestContext(this); public final SpringTestContext spring = new SpringTestContext(this);
@ -137,6 +144,8 @@ public class PrePostMethodSecurityConfigurationTests {
this.spring.register(MethodSecurityServiceEnabledConfig.class).autowire(); this.spring.register(MethodSecurityServiceEnabledConfig.class).autowire();
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.methodSecurityService::securedUser) assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.methodSecurityService::securedUser)
.withMessage("Access Denied"); .withMessage("Access Denied");
SecurityContextHolderStrategy strategy = this.spring.getContext().getBean(SecurityContextHolderStrategy.class);
verify(strategy, atLeastOnce()).getContext();
} }
@WithMockUser @WithMockUser
@ -162,6 +171,15 @@ public class PrePostMethodSecurityConfigurationTests {
this.methodSecurityService.preAuthorizeAdmin(); this.methodSecurityService.preAuthorizeAdmin();
} }
@WithMockUser(roles = "ADMIN")
@Test
public void preAuthorizeAdminWhenSecurityContextHolderStrategyThenUses() {
this.spring.register(MethodSecurityServiceConfig.class).autowire();
this.methodSecurityService.preAuthorizeAdmin();
SecurityContextHolderStrategy strategy = this.spring.getContext().getBean(SecurityContextHolderStrategy.class);
verify(strategy, atLeastOnce()).getContext();
}
@WithMockUser(authorities = "PREFIX_ADMIN") @WithMockUser(authorities = "PREFIX_ADMIN")
@Test @Test
public void preAuthorizeAdminWhenRoleAdminAndCustomPrefixThenPasses() { public void preAuthorizeAdminWhenRoleAdminAndCustomPrefixThenPasses() {
@ -285,6 +303,8 @@ public class PrePostMethodSecurityConfigurationTests {
this.spring.register(BusinessServiceConfig.class).autowire(); this.spring.register(BusinessServiceConfig.class).autowire();
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.businessService::rolesAllowedUser) assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.businessService::rolesAllowedUser)
.withMessage("Access Denied"); .withMessage("Access Denied");
SecurityContextHolderStrategy strategy = this.spring.getContext().getBean(SecurityContextHolderStrategy.class);
verify(strategy, atLeastOnce()).getContext();
} }
@WithMockUser @WithMockUser
@ -480,12 +500,15 @@ public class PrePostMethodSecurityConfigurationTests {
@Bean @Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE) @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
Advisor customBeforeAdvice() { Advisor customBeforeAdvice(SecurityContextHolderStrategy strategy) {
JdkRegexpMethodPointcut pointcut = new JdkRegexpMethodPointcut(); JdkRegexpMethodPointcut pointcut = new JdkRegexpMethodPointcut();
pointcut.setPattern(".*MethodSecurityServiceImpl.*securedUser"); pointcut.setPattern(".*MethodSecurityServiceImpl.*securedUser");
AuthorizationManager<MethodInvocation> authorizationManager = (a, AuthorizationManager<MethodInvocation> authorizationManager = (a,
o) -> new AuthorizationDecision("bob".equals(a.get().getName())); o) -> new AuthorizationDecision("bob".equals(a.get().getName()));
return new AuthorizationManagerBeforeMethodInterceptor(pointcut, authorizationManager); AuthorizationManagerBeforeMethodInterceptor before = new AuthorizationManagerBeforeMethodInterceptor(
pointcut, authorizationManager);
before.setSecurityContextHolderStrategy(strategy);
return before;
} }
} }
@ -495,11 +518,11 @@ public class PrePostMethodSecurityConfigurationTests {
@Bean @Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE) @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
Advisor customAfterAdvice() { Advisor customAfterAdvice(SecurityContextHolderStrategy strategy) {
JdkRegexpMethodPointcut pointcut = new JdkRegexpMethodPointcut(); JdkRegexpMethodPointcut pointcut = new JdkRegexpMethodPointcut();
pointcut.setPattern(".*MethodSecurityServiceImpl.*securedUser"); pointcut.setPattern(".*MethodSecurityServiceImpl.*securedUser");
MethodInterceptor interceptor = (mi) -> { MethodInterceptor interceptor = (mi) -> {
Authentication auth = SecurityContextHolder.getContext().getAuthentication(); Authentication auth = strategy.getContext().getAuthentication();
if ("bob".equals(auth.getName())) { if ("bob".equals(auth.getName())) {
return "granted"; return "granted";
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2017 the original author or authors. * Copyright 2002-2022 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -19,6 +19,7 @@ package org.springframework.security.config.test;
import java.io.Closeable; import java.io.Closeable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.function.Consumer;
import jakarta.servlet.Filter; import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain; import jakarta.servlet.FilterChain;
@ -56,6 +57,8 @@ public class SpringTestContext implements Closeable {
private List<Filter> filters = new ArrayList<>(); private List<Filter> filters = new ArrayList<>();
private List<Consumer<ConfigurableWebApplicationContext>> postProcessors = new ArrayList<>();
public SpringTestContext(Object test) { public SpringTestContext(Object test) {
setTest(test); setTest(test);
} }
@ -104,6 +107,11 @@ public class SpringTestContext implements Closeable {
return this; return this;
} }
public SpringTestContext postProcessor(Consumer<ConfigurableWebApplicationContext> contextConsumer) {
this.postProcessors.add(contextConsumer);
return this;
}
public SpringTestContext mockMvcAfterSpringSecurityOk() { public SpringTestContext mockMvcAfterSpringSecurityOk() {
return addFilter(new OncePerRequestFilter() { return addFilter(new OncePerRequestFilter() {
@Override @Override
@ -131,6 +139,9 @@ public class SpringTestContext implements Closeable {
public void autowire() { public void autowire() {
this.context.setServletContext(new MockServletContext()); this.context.setServletContext(new MockServletContext());
this.context.setServletConfig(new MockServletConfig()); this.context.setServletConfig(new MockServletConfig());
for (Consumer<ConfigurableWebApplicationContext> postProcessor : this.postProcessors) {
postProcessor.accept(this.context);
}
this.context.refresh(); this.context.refresh();
if (this.context.containsBean(BeanIds.SPRING_SECURITY_FILTER_CHAIN)) { if (this.context.containsBean(BeanIds.SPRING_SECURITY_FILTER_CHAIN)) {
// @formatter:off // @formatter:off

View File

@ -0,0 +1,48 @@
/*
* Copyright 2002-2022 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
*
* https://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.test;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import org.springframework.context.ApplicationContext;
import org.springframework.test.context.TestContext;
import org.springframework.test.context.TestExecutionListener;
public class SpringTestParentApplicationContextExecutionListener implements TestExecutionListener {
@Override
public void beforeTestMethod(TestContext testContext) throws Exception {
ApplicationContext parent = testContext.getApplicationContext();
Object testInstance = testContext.getTestInstance();
getContexts(testInstance).forEach((springTestContext) -> springTestContext
.postProcessor((applicationContext) -> applicationContext.setParent(parent)));
}
private static List<SpringTestContext> getContexts(Object test) throws IllegalAccessException {
Field[] declaredFields = test.getClass().getDeclaredFields();
List<SpringTestContext> result = new ArrayList<>();
for (Field field : declaredFields) {
if (SpringTestContext.class.isAssignableFrom(field.getType())) {
result.add((SpringTestContext) field.get(test));
}
}
return result;
}
}