Add order offset to @EnableMethodSecurity

Closes gh-13214
This commit is contained in:
Yan Kardziyaka 2023-10-26 20:00:48 +03:00 committed by Josh Cummings
parent c19f3d9d06
commit 99218db84a
7 changed files with 280 additions and 10 deletions

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2023 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.
@ -87,4 +87,14 @@ public @interface EnableMethodSecurity {
*/
AdviceMode mode() default AdviceMode.PROXY;
/**
* Indicate additional offset in the ordering of the execution of the security
* interceptors when multiple advices are applied at a specific joinpoint. I.e.,
* precedence of each security interceptor enabled by this annotation will be
* calculated as sum of its default precedence and offset. The default is 0.
* @return the offset in the order the security advisor should be applied
* @since 6.3
*/
int offset() default 0;
}

View File

@ -24,7 +24,9 @@ import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportAware;
import org.springframework.context.annotation.Role;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.security.access.hierarchicalroles.NullRoleHierarchy;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.authorization.AuthoritiesAuthorizationManager;
@ -45,14 +47,17 @@ import org.springframework.security.core.context.SecurityContextHolderStrategy;
*/
@Configuration(proxyBeanMethods = false)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
final class Jsr250MethodSecurityConfiguration {
final class Jsr250MethodSecurityConfiguration implements ImportAware {
private int interceptorOrderOffset;
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static MethodInterceptor jsr250AuthorizationMethodInterceptor(
ObjectProvider<GrantedAuthorityDefaults> defaultsProvider,
ObjectProvider<SecurityContextHolderStrategy> strategyProvider,
ObjectProvider<ObservationRegistry> registryProvider, ObjectProvider<RoleHierarchy> roleHierarchyProvider) {
ObjectProvider<ObservationRegistry> registryProvider, ObjectProvider<RoleHierarchy> roleHierarchyProvider,
Jsr250MethodSecurityConfiguration configuration) {
Jsr250AuthorizationManager jsr250 = new Jsr250AuthorizationManager();
AuthoritiesAuthorizationManager authoritiesAuthorizationManager = new AuthoritiesAuthorizationManager();
RoleHierarchy roleHierarchy = roleHierarchyProvider.getIfAvailable(NullRoleHierarchy::new);
@ -65,8 +70,15 @@ final class Jsr250MethodSecurityConfiguration {
registryProvider, jsr250);
AuthorizationManagerBeforeMethodInterceptor interceptor = AuthorizationManagerBeforeMethodInterceptor
.jsr250(manager);
interceptor.setOrder(interceptor.getOrder() + configuration.interceptorOrderOffset);
interceptor.setSecurityContextHolderStrategy(strategy);
return interceptor;
}
@Override
public void setImportMetadata(AnnotationMetadata importMetadata) {
EnableMethodSecurity annotation = importMetadata.getAnnotations().get(EnableMethodSecurity.class).synthesize();
this.interceptorOrderOffset = annotation.offset();
}
}

View File

@ -27,7 +27,9 @@ import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportAware;
import org.springframework.context.annotation.Role;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
@ -58,7 +60,9 @@ import org.springframework.util.function.SingletonSupplier;
*/
@Configuration(proxyBeanMethods = false)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
final class PrePostMethodSecurityConfiguration {
final class PrePostMethodSecurityConfiguration implements ImportAware {
private int interceptorOrderOffset;
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
@ -66,8 +70,10 @@ final class PrePostMethodSecurityConfiguration {
ObjectProvider<GrantedAuthorityDefaults> defaultsProvider,
ObjectProvider<MethodSecurityExpressionHandler> expressionHandlerProvider,
ObjectProvider<SecurityContextHolderStrategy> strategyProvider,
ObjectProvider<RoleHierarchy> roleHierarchyProvider, ApplicationContext context) {
ObjectProvider<RoleHierarchy> roleHierarchyProvider, PrePostMethodSecurityConfiguration configuration,
ApplicationContext context) {
PreFilterAuthorizationMethodInterceptor preFilter = new PreFilterAuthorizationMethodInterceptor();
preFilter.setOrder(preFilter.getOrder() + configuration.interceptorOrderOffset);
strategyProvider.ifAvailable(preFilter::setSecurityContextHolderStrategy);
preFilter.setExpressionHandler(new DeferringMethodSecurityExpressionHandler(expressionHandlerProvider,
defaultsProvider, roleHierarchyProvider, context));
@ -82,12 +88,13 @@ final class PrePostMethodSecurityConfiguration {
ObjectProvider<SecurityContextHolderStrategy> strategyProvider,
ObjectProvider<AuthorizationEventPublisher> eventPublisherProvider,
ObjectProvider<ObservationRegistry> registryProvider, ObjectProvider<RoleHierarchy> roleHierarchyProvider,
ApplicationContext context) {
PrePostMethodSecurityConfiguration configuration, ApplicationContext context) {
PreAuthorizeAuthorizationManager manager = new PreAuthorizeAuthorizationManager();
manager.setExpressionHandler(new DeferringMethodSecurityExpressionHandler(expressionHandlerProvider,
defaultsProvider, roleHierarchyProvider, context));
AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor
.preAuthorize(manager(manager, registryProvider));
preAuthorize.setOrder(preAuthorize.getOrder() + configuration.interceptorOrderOffset);
strategyProvider.ifAvailable(preAuthorize::setSecurityContextHolderStrategy);
eventPublisherProvider.ifAvailable(preAuthorize::setAuthorizationEventPublisher);
return preAuthorize;
@ -101,12 +108,13 @@ final class PrePostMethodSecurityConfiguration {
ObjectProvider<SecurityContextHolderStrategy> strategyProvider,
ObjectProvider<AuthorizationEventPublisher> eventPublisherProvider,
ObjectProvider<ObservationRegistry> registryProvider, ObjectProvider<RoleHierarchy> roleHierarchyProvider,
ApplicationContext context) {
PrePostMethodSecurityConfiguration configuration, ApplicationContext context) {
PostAuthorizeAuthorizationManager manager = new PostAuthorizeAuthorizationManager();
manager.setExpressionHandler(new DeferringMethodSecurityExpressionHandler(expressionHandlerProvider,
defaultsProvider, roleHierarchyProvider, context));
AuthorizationManagerAfterMethodInterceptor postAuthorize = AuthorizationManagerAfterMethodInterceptor
.postAuthorize(manager(manager, registryProvider));
postAuthorize.setOrder(postAuthorize.getOrder() + configuration.interceptorOrderOffset);
strategyProvider.ifAvailable(postAuthorize::setSecurityContextHolderStrategy);
eventPublisherProvider.ifAvailable(postAuthorize::setAuthorizationEventPublisher);
return postAuthorize;
@ -118,8 +126,10 @@ final class PrePostMethodSecurityConfiguration {
ObjectProvider<GrantedAuthorityDefaults> defaultsProvider,
ObjectProvider<MethodSecurityExpressionHandler> expressionHandlerProvider,
ObjectProvider<SecurityContextHolderStrategy> strategyProvider,
ObjectProvider<RoleHierarchy> roleHierarchyProvider, ApplicationContext context) {
ObjectProvider<RoleHierarchy> roleHierarchyProvider, PrePostMethodSecurityConfiguration configuration,
ApplicationContext context) {
PostFilterAuthorizationMethodInterceptor postFilter = new PostFilterAuthorizationMethodInterceptor();
postFilter.setOrder(postFilter.getOrder() + configuration.interceptorOrderOffset);
strategyProvider.ifAvailable(postFilter::setSecurityContextHolderStrategy);
postFilter.setExpressionHandler(new DeferringMethodSecurityExpressionHandler(expressionHandlerProvider,
defaultsProvider, roleHierarchyProvider, context));
@ -142,6 +152,12 @@ final class PrePostMethodSecurityConfiguration {
return new DeferringObservationAuthorizationManager<>(registryProvider, delegate);
}
@Override
public void setImportMetadata(AnnotationMetadata importMetadata) {
EnableMethodSecurity annotation = importMetadata.getAnnotations().get(EnableMethodSecurity.class).synthesize();
this.interceptorOrderOffset = annotation.offset();
}
private static final class DeferringMethodSecurityExpressionHandler implements MethodSecurityExpressionHandler {
private final Supplier<MethodSecurityExpressionHandler> expressionHandler;

View File

@ -24,7 +24,9 @@ import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportAware;
import org.springframework.context.annotation.Role;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.security.access.annotation.Secured;
import org.springframework.security.access.hierarchicalroles.NullRoleHierarchy;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
@ -45,13 +47,16 @@ import org.springframework.security.core.context.SecurityContextHolderStrategy;
*/
@Configuration(proxyBeanMethods = false)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
final class SecuredMethodSecurityConfiguration {
final class SecuredMethodSecurityConfiguration implements ImportAware {
private int interceptorOrderOffset;
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static MethodInterceptor securedAuthorizationMethodInterceptor(
ObjectProvider<SecurityContextHolderStrategy> strategyProvider,
ObjectProvider<ObservationRegistry> registryProvider, ObjectProvider<RoleHierarchy> roleHierarchyProvider) {
ObjectProvider<ObservationRegistry> registryProvider, ObjectProvider<RoleHierarchy> roleHierarchyProvider,
SecuredMethodSecurityConfiguration configuration) {
SecuredAuthorizationManager secured = new SecuredAuthorizationManager();
AuthoritiesAuthorizationManager authoritiesAuthorizationManager = new AuthoritiesAuthorizationManager();
RoleHierarchy roleHierarchy = roleHierarchyProvider.getIfAvailable(NullRoleHierarchy::new);
@ -63,8 +68,15 @@ final class SecuredMethodSecurityConfiguration {
registryProvider, secured);
AuthorizationManagerBeforeMethodInterceptor interceptor = AuthorizationManagerBeforeMethodInterceptor
.secured(manager);
interceptor.setOrder(interceptor.getOrder() + configuration.interceptorOrderOffset);
interceptor.setSecurityContextHolderStrategy(strategy);
return interceptor;
}
@Override
public void setImportMetadata(AnnotationMetadata importMetadata) {
EnableMethodSecurity annotation = importMetadata.getAnnotations().get(EnableMethodSecurity.class).synthesize();
this.interceptorOrderOffset = annotation.offset();
}
}

View File

@ -96,6 +96,14 @@ public interface MethodSecurityService {
@PostAuthorize("returnObject.size == 2")
List<String> manyAnnotations(List<String> array);
@PreFilter("filterObject != 'DropOnPreFilter'")
@PreAuthorize("#list.remove('DropOnPreAuthorize')")
@Secured("ROLE_SECURED")
@RolesAllowed("JSR250")
@PostAuthorize("#list.remove('DropOnPostAuthorize')")
@PostFilter("filterObject != 'DropOnPostFilter'")
List<String> allAnnotations(List<String> list);
@RequireUserRole
@RequireAdminRole
void repeatedAnnotations();

View File

@ -117,6 +117,11 @@ public class MethodSecurityServiceImpl implements MethodSecurityService {
return object;
}
@Override
public List<String> allAnnotations(List<String> list) {
return null;
}
@Override
public void repeatedAnnotations() {
}

View File

@ -36,6 +36,7 @@ import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.AdviceMode;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Role;
import org.springframework.core.annotation.AnnotationConfigurationException;
import org.springframework.security.access.AccessDeniedException;
@ -467,10 +468,115 @@ public class PrePostMethodSecurityConfigurationTests {
this.methodSecurityService.jsr250RolesAllowedUser();
}
@Test
public void allAnnotationsWhenAdviceBeforeOffsetPreFilterThenReturnsFilteredList() {
this.spring.register(ReturnBeforeOffsetPreFilterConfig.class).autowire();
List<String> list = Arrays.asList("DropOnPreFilter", "DropOnPreAuthorize", "DropOnPostAuthorize",
"DropOnPostFilter", "DoNotDrop");
List<String> filtered = this.methodSecurityService.allAnnotations(new ArrayList<>(list));
assertThat(filtered).hasSize(5);
assertThat(filtered).containsExactly("DropOnPreFilter", "DropOnPreAuthorize", "DropOnPostAuthorize",
"DropOnPostFilter", "DoNotDrop");
}
@Test
public void allAnnotationsWhenAdviceBeforeOffsetPreAuthorizeThenReturnsFilteredList() {
this.spring.register(ReturnBeforeOffsetPreAuthorizeConfig.class).autowire();
List<String> list = Arrays.asList("DropOnPreFilter", "DropOnPreAuthorize", "DropOnPostAuthorize",
"DropOnPostFilter", "DoNotDrop");
List<String> filtered = this.methodSecurityService.allAnnotations(new ArrayList<>(list));
assertThat(filtered).hasSize(4);
assertThat(filtered).containsExactly("DropOnPreAuthorize", "DropOnPostAuthorize", "DropOnPostFilter",
"DoNotDrop");
}
@Test
public void allAnnotationsWhenAdviceBeforeOffsetSecuredThenReturnsFilteredList() {
this.spring.register(ReturnBeforeOffsetSecuredConfig.class).autowire();
List<String> list = Arrays.asList("DropOnPreFilter", "DropOnPreAuthorize", "DropOnPostAuthorize",
"DropOnPostFilter", "DoNotDrop");
List<String> filtered = this.methodSecurityService.allAnnotations(new ArrayList<>(list));
assertThat(filtered).hasSize(3);
assertThat(filtered).containsExactly("DropOnPostAuthorize", "DropOnPostFilter", "DoNotDrop");
}
@Test
@WithMockUser
public void allAnnotationsWhenAdviceBeforeOffsetJsr250WithInsufficientRolesThenFails() {
this.spring.register(ReturnBeforeOffsetJsr250Config.class).autowire();
List<String> list = Arrays.asList("DropOnPreFilter", "DropOnPreAuthorize", "DropOnPostAuthorize",
"DropOnPostFilter", "DoNotDrop");
assertThatExceptionOfType(AccessDeniedException.class)
.isThrownBy(() -> this.methodSecurityService.allAnnotations(new ArrayList<>(list)));
}
@Test
@WithMockUser(roles = "SECURED")
public void allAnnotationsWhenAdviceBeforeOffsetJsr250ThenReturnsFilteredList() {
this.spring.register(ReturnBeforeOffsetJsr250Config.class).autowire();
List<String> list = Arrays.asList("DropOnPreFilter", "DropOnPreAuthorize", "DropOnPostAuthorize",
"DropOnPostFilter", "DoNotDrop");
List<String> filtered = this.methodSecurityService.allAnnotations(new ArrayList<>(list));
assertThat(filtered).hasSize(3);
assertThat(filtered).containsExactly("DropOnPostAuthorize", "DropOnPostFilter", "DoNotDrop");
}
@Test
@WithMockUser(roles = { "SECURED" })
public void allAnnotationsWhenAdviceBeforeOffsetPostAuthorizeWithInsufficientRolesThenFails() {
this.spring.register(ReturnBeforeOffsetPostAuthorizeConfig.class).autowire();
List<String> list = Arrays.asList("DropOnPreFilter", "DropOnPreAuthorize", "DropOnPostAuthorize",
"DropOnPostFilter", "DoNotDrop");
assertThatExceptionOfType(AccessDeniedException.class)
.isThrownBy(() -> this.methodSecurityService.allAnnotations(new ArrayList<>(list)));
}
@Test
@WithMockUser(roles = { "SECURED", "JSR250" })
public void allAnnotationsWhenAdviceBeforeOffsetPostAuthorizeThenReturnsFilteredList() {
this.spring.register(ReturnBeforeOffsetPostAuthorizeConfig.class).autowire();
List<String> list = Arrays.asList("DropOnPreFilter", "DropOnPreAuthorize", "DropOnPostAuthorize",
"DropOnPostFilter", "DoNotDrop");
List<String> filtered = this.methodSecurityService.allAnnotations(new ArrayList<>(list));
assertThat(filtered).hasSize(3);
assertThat(filtered).containsExactly("DropOnPostAuthorize", "DropOnPostFilter", "DoNotDrop");
}
@Test
@WithMockUser(roles = { "SECURED", "JSR250" })
public void allAnnotationsWhenAdviceBeforeOffsetPostFilterThenReturnsFilteredList() {
this.spring.register(ReturnBeforeOffsetPostFilterConfig.class).autowire();
List<String> list = Arrays.asList("DropOnPreFilter", "DropOnPreAuthorize", "DropOnPostAuthorize",
"DropOnPostFilter", "DoNotDrop");
List<String> filtered = this.methodSecurityService.allAnnotations(new ArrayList<>(list));
assertThat(filtered).hasSize(2);
assertThat(filtered).containsExactly("DropOnPostFilter", "DoNotDrop");
}
@Test
@WithMockUser(roles = { "SECURED", "JSR250" })
public void allAnnotationsWhenAdviceAfterAllOffsetThenReturnsFilteredList() {
this.spring.register(ReturnAfterAllOffsetConfig.class).autowire();
List<String> list = Arrays.asList("DropOnPreFilter", "DropOnPreAuthorize", "DropOnPostAuthorize",
"DropOnPostFilter", "DoNotDrop");
List<String> filtered = this.methodSecurityService.allAnnotations(new ArrayList<>(list));
assertThat(filtered).hasSize(1);
assertThat(filtered).containsExactly("DoNotDrop");
}
private static Consumer<ConfigurableWebApplicationContext> disallowBeanOverriding() {
return (context) -> ((AnnotationConfigWebApplicationContext) context).setAllowBeanDefinitionOverriding(false);
}
private static Advisor returnAdvisor(int order) {
JdkRegexpMethodPointcut pointcut = new JdkRegexpMethodPointcut();
pointcut.setPattern(".*MethodSecurityServiceImpl.*");
MethodInterceptor interceptor = (mi) -> mi.getArguments()[0];
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(pointcut, interceptor);
advisor.setOrder(order);
return advisor;
}
@Configuration
@EnableCustomMethodSecurity
static class CustomMethodSecurityServiceConfig {
@ -660,4 +766,105 @@ public class PrePostMethodSecurityConfigurationTests {
}
@Import(OffsetConfig.class)
static class ReturnBeforeOffsetPreFilterConfig {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
Advisor returnBeforePreFilter() {
return returnAdvisor(AuthorizationInterceptorsOrder.PRE_FILTER.getOrder() + OffsetConfig.OFFSET - 1);
}
}
@Configuration
@Import(OffsetConfig.class)
static class ReturnBeforeOffsetPreAuthorizeConfig {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
Advisor returnBeforePreAuthorize() {
return returnAdvisor(AuthorizationInterceptorsOrder.PRE_AUTHORIZE.getOrder() + OffsetConfig.OFFSET - 1);
}
}
@Configuration
@Import(OffsetConfig.class)
static class ReturnBeforeOffsetSecuredConfig {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
Advisor returnBeforeSecured() {
return returnAdvisor(AuthorizationInterceptorsOrder.SECURED.getOrder() + OffsetConfig.OFFSET - 1);
}
}
@Configuration
@Import(OffsetConfig.class)
static class ReturnBeforeOffsetJsr250Config {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
Advisor returnBeforeJsr250() {
return returnAdvisor(AuthorizationInterceptorsOrder.JSR250.getOrder() + OffsetConfig.OFFSET - 1);
}
}
@Configuration
@Import(OffsetConfig.class)
static class ReturnBeforeOffsetPostAuthorizeConfig {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
Advisor returnBeforePreAuthorize() {
return returnAdvisor(AuthorizationInterceptorsOrder.POST_AUTHORIZE.getOrder() + OffsetConfig.OFFSET - 1);
}
}
@Configuration
@Import(OffsetConfig.class)
static class ReturnBeforeOffsetPostFilterConfig {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
Advisor returnBeforePostFilter() {
return returnAdvisor(AuthorizationInterceptorsOrder.POST_FILTER.getOrder() + OffsetConfig.OFFSET - 1);
}
}
@Configuration
@Import(OffsetConfig.class)
static class ReturnAfterAllOffsetConfig {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
Advisor returnAfterAll() {
return returnAdvisor(AuthorizationInterceptorsOrder.POST_FILTER.getOrder() + OffsetConfig.OFFSET + 1);
}
}
@Configuration
@EnableMethodSecurity(offset = OffsetConfig.OFFSET, jsr250Enabled = true, securedEnabled = true)
static class OffsetConfig {
static final int OFFSET = 2;
@Bean
MethodSecurityService methodSecurityService() {
return new MethodSecurityServiceImpl();
}
@Bean
Authz authz() {
return new Authz();
}
}
}