Skip to content

Commit

Permalink
Add SecurityContextHolderStrategy Java Configuration for Method Security
Browse files Browse the repository at this point in the history
Issue gh-11061
  • Loading branch information
jzheaux committed Jun 27, 2022
1 parent 7a9c873 commit 74d646f
Show file tree
Hide file tree
Showing 7 changed files with 142 additions and 13 deletions.
Original file line number Diff line number Diff line change
@@ -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");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -73,6 +73,8 @@
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
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;

/**
Expand Down Expand Up @@ -101,6 +103,9 @@ public <T> T postProcess(T object) {

};

private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder
.getContextHolderStrategy();

private DefaultMethodSecurityExpressionHandler defaultMethodExpressionHandler = new DefaultMethodSecurityExpressionHandler();

private AuthenticationManager authenticationManager;
Expand Down Expand Up @@ -143,6 +148,7 @@ public MethodInterceptor methodSecurityInterceptor(MethodSecurityMetadataSource
this.methodSecurityInterceptor.setAccessDecisionManager(accessDecisionManager());
this.methodSecurityInterceptor.setAfterInvocationManager(afterInvocationManager());
this.methodSecurityInterceptor.setSecurityMetadataSource(methodSecurityMetadataSource);
this.methodSecurityInterceptor.setSecurityContextHolderStrategy(this.securityContextHolderStrategy);
RunAsManager runAsManager = runAsManager();
if (runAsManager != null) {
this.methodSecurityInterceptor.setRunAsManager(runAsManager);
Expand Down Expand Up @@ -411,6 +417,12 @@ public void setMethodSecurityExpressionHandler(List<MethodSecurityExpressionHand
this.expressionHandler = handlers.get(0);
}

@Autowired(required = false)
void setSecurityContextHolderStrategy(SecurityContextHolderStrategy securityContextHolderStrategy) {
Assert.notNull(securityContextHolderStrategy, "securityContextHolderStrategy cannot be null");
this.securityContextHolderStrategy = securityContextHolderStrategy;
}

@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.context = beanFactory;
Expand Down
Original file line number Diff line number Diff line change
@@ -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");
* you may not use this file except in compliance with the License.
Expand All @@ -25,6 +25,8 @@
import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor;
import org.springframework.security.authorization.method.Jsr250AuthorizationManager;
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.
Expand All @@ -40,15 +42,26 @@ final class Jsr250MethodSecurityConfiguration {

private final Jsr250AuthorizationManager jsr250AuthorizationManager = new Jsr250AuthorizationManager();

private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder
.getContextHolderStrategy();

@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
Advisor jsr250AuthorizationMethodInterceptor() {
return AuthorizationManagerBeforeMethodInterceptor.jsr250(this.jsr250AuthorizationManager);
AuthorizationManagerBeforeMethodInterceptor interceptor = AuthorizationManagerBeforeMethodInterceptor
.jsr250(this.jsr250AuthorizationManager);
interceptor.setSecurityContextHolderStrategy(this.securityContextHolderStrategy);
return interceptor;
}

@Autowired(required = false)
void setGrantedAuthorityDefaults(GrantedAuthorityDefaults grantedAuthorityDefaults) {
this.jsr250AuthorizationManager.setRolePrefix(grantedAuthorityDefaults.getRolePrefix());
}

@Autowired(required = false)
void setSecurityContextHolderStrategy(SecurityContextHolderStrategy securityContextHolderStrategy) {
this.securityContextHolderStrategy = securityContextHolderStrategy;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import org.springframework.security.authorization.method.PreAuthorizeAuthorizationManager;
import org.springframework.security.authorization.method.PreFilterAuthorizationMethodInterceptor;
import org.springframework.security.config.core.GrantedAuthorityDefaults;
import org.springframework.security.core.context.SecurityContextHolderStrategy;

/**
* Base {@link Configuration} for enabling Spring Security Method Security.
Expand Down Expand Up @@ -109,6 +110,14 @@ void setMethodSecurityExpressionHandler(MethodSecurityExpressionHandler methodSe
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)
void setGrantedAuthorityDefaults(GrantedAuthorityDefaults grantedAuthorityDefaults) {
this.expressionHandler.setDefaultRolePrefix(grantedAuthorityDefaults.getRolePrefix());
Expand Down
Original file line number Diff line number Diff line change
@@ -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");
* you may not use this file except in compliance with the License.
Expand All @@ -17,12 +17,15 @@
package org.springframework.security.config.annotation.method.configuration;

import org.springframework.aop.Advisor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Role;
import org.springframework.security.access.annotation.Secured;
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.
Expand All @@ -36,10 +39,20 @@
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
final class SecuredMethodSecurityConfiguration {

private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder
.getContextHolderStrategy();

@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
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;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -50,19 +50,24 @@
import org.springframework.security.authorization.method.AuthorizationInterceptorsOrder;
import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor;
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.test.SpringTestContext;
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.context.SecurityContextHolder;
import org.springframework.security.test.context.annotation.SecurityTestExecutionListeners;
import org.springframework.security.core.context.SecurityContextHolderStrategy;
import org.springframework.security.test.context.support.WithAnonymousUser;
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 static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;

Expand All @@ -73,7 +78,9 @@
* @author Josh Cummings
*/
@ExtendWith({ SpringExtension.class, SpringTestContextExtension.class })
@SecurityTestExecutionListeners
@ContextConfiguration(classes = SecurityContextChangedListenerConfig.class)
@TestExecutionListeners(listeners = { WithSecurityContextTestExecutionListener.class,
SpringTestParentApplicationContextExecutionListener.class })
public class PrePostMethodSecurityConfigurationTests {

public final SpringTestContext spring = new SpringTestContext(this);
Expand Down Expand Up @@ -137,6 +144,8 @@ public void securedUserWhenRoleAdminThenAccessDeniedException() {
this.spring.register(MethodSecurityServiceEnabledConfig.class).autowire();
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.methodSecurityService::securedUser)
.withMessage("Access Denied");
SecurityContextHolderStrategy strategy = this.spring.getContext().getBean(SecurityContextHolderStrategy.class);
verify(strategy, atLeastOnce()).getContext();
}

@WithMockUser
Expand All @@ -162,6 +171,15 @@ public void preAuthorizeAdminWhenRoleAdminThenPasses() {
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")
@Test
public void preAuthorizeAdminWhenRoleAdminAndCustomPrefixThenPasses() {
Expand Down Expand Up @@ -285,6 +303,8 @@ public void rolesAllowedUserWhenRoleAdminThenAccessDeniedException() {
this.spring.register(BusinessServiceConfig.class).autowire();
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.businessService::rolesAllowedUser)
.withMessage("Access Denied");
SecurityContextHolderStrategy strategy = this.spring.getContext().getBean(SecurityContextHolderStrategy.class);
verify(strategy, atLeastOnce()).getContext();
}

@WithMockUser
Expand Down Expand Up @@ -480,12 +500,15 @@ static class CustomAuthorizationManagerBeforeAdviceConfig {

@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
Advisor customBeforeAdvice() {
Advisor customBeforeAdvice(SecurityContextHolderStrategy strategy) {
JdkRegexpMethodPointcut pointcut = new JdkRegexpMethodPointcut();
pointcut.setPattern(".*MethodSecurityServiceImpl.*securedUser");
AuthorizationManager<MethodInvocation> authorizationManager = (a,
o) -> new AuthorizationDecision("bob".equals(a.get().getName()));
return new AuthorizationManagerBeforeMethodInterceptor(pointcut, authorizationManager);
AuthorizationManagerBeforeMethodInterceptor before = new AuthorizationManagerBeforeMethodInterceptor(
pointcut, authorizationManager);
before.setSecurityContextHolderStrategy(strategy);
return before;
}

}
Expand All @@ -495,11 +518,11 @@ static class CustomAuthorizationManagerAfterAdviceConfig {

@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
Advisor customAfterAdvice() {
Advisor customAfterAdvice(SecurityContextHolderStrategy strategy) {
JdkRegexpMethodPointcut pointcut = new JdkRegexpMethodPointcut();
pointcut.setPattern(".*MethodSecurityServiceImpl.*securedUser");
MethodInterceptor interceptor = (mi) -> {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
Authentication auth = strategy.getContext().getAuthentication();
if ("bob".equals(auth.getName())) {
return "granted";
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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");
* you may not use this file except in compliance with the License.
Expand All @@ -19,6 +19,7 @@
import java.io.Closeable;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;

import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
Expand Down Expand Up @@ -56,6 +57,8 @@ public class SpringTestContext implements Closeable {

private List<Filter> filters = new ArrayList<>();

private List<Consumer<ConfigurableWebApplicationContext>> postProcessors = new ArrayList<>();

public SpringTestContext(Object test) {
setTest(test);
}
Expand Down Expand Up @@ -104,6 +107,11 @@ public SpringTestContext context(String configuration) {
return this;
}

public SpringTestContext postProcessor(Consumer<ConfigurableWebApplicationContext> contextConsumer) {
this.postProcessors.add(contextConsumer);
return this;
}

public SpringTestContext mockMvcAfterSpringSecurityOk() {
return addFilter(new OncePerRequestFilter() {
@Override
Expand Down Expand Up @@ -131,6 +139,9 @@ public ConfigurableWebApplicationContext getContext() {
public void autowire() {
this.context.setServletContext(new MockServletContext());
this.context.setServletConfig(new MockServletConfig());
for (Consumer<ConfigurableWebApplicationContext> postProcessor : this.postProcessors) {
postProcessor.accept(this.context);
}
this.context.refresh();
if (this.context.containsBean(BeanIds.SPRING_SECURITY_FILTER_CHAIN)) {
// @formatter:off
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}

}

0 comments on commit 74d646f

Please sign in to comment.