Skip to content

Commit 3a85172

Browse files
committed
Add meta-annotation parameter support
Closes gh-14480
1 parent e771267 commit 3a85172

21 files changed

+582
-127
lines changed

config/src/main/java/org/springframework/security/config/annotation/method/configuration/EnableMethodSecurity.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,4 +97,11 @@
9797
*/
9898
int offset() default 0;
9999

100+
/**
101+
* Indicate whether to resolve authorization templates with annotation parameters
102+
* @return the resolution policy to use
103+
* @since 6.3
104+
*/
105+
boolean useAnnotationParameters() default false;
106+
100107
}

config/src/main/java/org/springframework/security/config/annotation/method/configuration/EnableReactiveMethodSecurity.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,4 +74,11 @@
7474
*/
7575
boolean useAuthorizationManager() default true;
7676

77+
/**
78+
* Indicate whether to resolve authorization templates with annotation parameters
79+
* @return the resolution policy to use
80+
* @since 6.3
81+
*/
82+
boolean useAnnotationParameters() default false;
83+
7784
}

config/src/main/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfiguration.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ final class PrePostMethodSecurityConfiguration implements ImportAware {
6464

6565
private int interceptorOrderOffset;
6666

67+
private boolean useAnnotationParameters;
68+
6769
@Bean
6870
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
6971
static MethodInterceptor preFilterAuthorizationMethodInterceptor(
@@ -77,6 +79,7 @@ static MethodInterceptor preFilterAuthorizationMethodInterceptor(
7779
strategyProvider.ifAvailable(preFilter::setSecurityContextHolderStrategy);
7880
preFilter.setExpressionHandler(new DeferringMethodSecurityExpressionHandler(expressionHandlerProvider,
7981
defaultsProvider, roleHierarchyProvider, context));
82+
preFilter.setUseAnnotationParameters(configuration.useAnnotationParameters);
8083
return preFilter;
8184
}
8285

@@ -92,6 +95,7 @@ static MethodInterceptor preAuthorizeAuthorizationMethodInterceptor(
9295
PreAuthorizeAuthorizationManager manager = new PreAuthorizeAuthorizationManager();
9396
manager.setExpressionHandler(new DeferringMethodSecurityExpressionHandler(expressionHandlerProvider,
9497
defaultsProvider, roleHierarchyProvider, context));
98+
manager.setUseAnnotationParameters(configuration.useAnnotationParameters);
9599
AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor
96100
.preAuthorize(manager(manager, registryProvider));
97101
preAuthorize.setOrder(preAuthorize.getOrder() + configuration.interceptorOrderOffset);
@@ -112,6 +116,7 @@ static MethodInterceptor postAuthorizeAuthorizationMethodInterceptor(
112116
PostAuthorizeAuthorizationManager manager = new PostAuthorizeAuthorizationManager();
113117
manager.setExpressionHandler(new DeferringMethodSecurityExpressionHandler(expressionHandlerProvider,
114118
defaultsProvider, roleHierarchyProvider, context));
119+
manager.setUseAnnotationParameters(configuration.useAnnotationParameters);
115120
AuthorizationManagerAfterMethodInterceptor postAuthorize = AuthorizationManagerAfterMethodInterceptor
116121
.postAuthorize(manager(manager, registryProvider));
117122
postAuthorize.setOrder(postAuthorize.getOrder() + configuration.interceptorOrderOffset);
@@ -133,6 +138,7 @@ static MethodInterceptor postFilterAuthorizationMethodInterceptor(
133138
strategyProvider.ifAvailable(postFilter::setSecurityContextHolderStrategy);
134139
postFilter.setExpressionHandler(new DeferringMethodSecurityExpressionHandler(expressionHandlerProvider,
135140
defaultsProvider, roleHierarchyProvider, context));
141+
postFilter.setUseAnnotationParameters(configuration.useAnnotationParameters);
136142
return postFilter;
137143
}
138144

@@ -156,6 +162,7 @@ static <T> AuthorizationManager<T> manager(AuthorizationManager<T> delegate,
156162
public void setImportMetadata(AnnotationMetadata importMetadata) {
157163
EnableMethodSecurity annotation = importMetadata.getAnnotations().get(EnableMethodSecurity.class).synthesize();
158164
this.interceptorOrderOffset = annotation.offset();
165+
this.useAnnotationParameters = annotation.useAnnotationParameters();
159166
}
160167

161168
private static final class DeferringMethodSecurityExpressionHandler implements MethodSecurityExpressionHandler {

config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveAuthorizationManagerMethodSecurityConfiguration.java

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@
2424
import org.springframework.beans.factory.config.BeanDefinition;
2525
import org.springframework.context.annotation.Bean;
2626
import org.springframework.context.annotation.Configuration;
27+
import org.springframework.context.annotation.ImportAware;
2728
import org.springframework.context.annotation.Role;
29+
import org.springframework.core.type.AnnotationMetadata;
2830
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
2931
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
3032
import org.springframework.security.authentication.ReactiveAuthenticationManager;
@@ -45,37 +47,55 @@
4547
* @since 5.8
4648
*/
4749
@Configuration(proxyBeanMethods = false)
48-
final class ReactiveAuthorizationManagerMethodSecurityConfiguration {
50+
final class ReactiveAuthorizationManagerMethodSecurityConfiguration implements ImportAware {
51+
52+
private boolean useAnnotationParameters;
4953

5054
@Bean
5155
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
5256
static PreFilterAuthorizationReactiveMethodInterceptor preFilterInterceptor(
53-
MethodSecurityExpressionHandler expressionHandler) {
54-
return new PreFilterAuthorizationReactiveMethodInterceptor(expressionHandler);
57+
MethodSecurityExpressionHandler expressionHandler,
58+
ReactiveAuthorizationManagerMethodSecurityConfiguration configuration) {
59+
PreFilterAuthorizationReactiveMethodInterceptor interceptor = new PreFilterAuthorizationReactiveMethodInterceptor(
60+
expressionHandler);
61+
interceptor.setUseAnnotationParameters(configuration.useAnnotationParameters);
62+
return interceptor;
5563
}
5664

5765
@Bean
5866
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
5967
static AuthorizationManagerBeforeReactiveMethodInterceptor preAuthorizeInterceptor(
60-
MethodSecurityExpressionHandler expressionHandler, ObjectProvider<ObservationRegistry> registryProvider) {
61-
ReactiveAuthorizationManager<MethodInvocation> authorizationManager = manager(
62-
new PreAuthorizeReactiveAuthorizationManager(expressionHandler), registryProvider);
68+
MethodSecurityExpressionHandler expressionHandler,
69+
ReactiveAuthorizationManagerMethodSecurityConfiguration configuration,
70+
ObjectProvider<ObservationRegistry> registryProvider) {
71+
PreAuthorizeReactiveAuthorizationManager manager = new PreAuthorizeReactiveAuthorizationManager(
72+
expressionHandler);
73+
manager.setUseAnnotationParameters(configuration.useAnnotationParameters);
74+
ReactiveAuthorizationManager<MethodInvocation> authorizationManager = manager(manager, registryProvider);
6375
return AuthorizationManagerBeforeReactiveMethodInterceptor.preAuthorize(authorizationManager);
6476
}
6577

6678
@Bean
6779
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
6880
static PostFilterAuthorizationReactiveMethodInterceptor postFilterInterceptor(
69-
MethodSecurityExpressionHandler expressionHandler) {
70-
return new PostFilterAuthorizationReactiveMethodInterceptor(expressionHandler);
81+
MethodSecurityExpressionHandler expressionHandler,
82+
ReactiveAuthorizationManagerMethodSecurityConfiguration configuration) {
83+
PostFilterAuthorizationReactiveMethodInterceptor interceptor = new PostFilterAuthorizationReactiveMethodInterceptor(
84+
expressionHandler);
85+
interceptor.setUseAnnotationParameters(configuration.useAnnotationParameters);
86+
return interceptor;
7187
}
7288

7389
@Bean
7490
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
7591
static AuthorizationManagerAfterReactiveMethodInterceptor postAuthorizeInterceptor(
76-
MethodSecurityExpressionHandler expressionHandler, ObjectProvider<ObservationRegistry> registryProvider) {
77-
ReactiveAuthorizationManager<MethodInvocationResult> authorizationManager = manager(
78-
new PostAuthorizeReactiveAuthorizationManager(expressionHandler), registryProvider);
92+
MethodSecurityExpressionHandler expressionHandler,
93+
ReactiveAuthorizationManagerMethodSecurityConfiguration configuration,
94+
ObjectProvider<ObservationRegistry> registryProvider) {
95+
PostAuthorizeReactiveAuthorizationManager manager = new PostAuthorizeReactiveAuthorizationManager(
96+
expressionHandler);
97+
manager.setUseAnnotationParameters(configuration.useAnnotationParameters);
98+
ReactiveAuthorizationManager<MethodInvocationResult> authorizationManager = manager(manager, registryProvider);
7999
return AuthorizationManagerAfterReactiveMethodInterceptor.postAuthorize(authorizationManager);
80100
}
81101

@@ -95,4 +115,12 @@ static <T> ReactiveAuthorizationManager<T> manager(ReactiveAuthorizationManager<
95115
return new DeferringObservationReactiveAuthorizationManager<>(registryProvider, delegate);
96116
}
97117

118+
@Override
119+
public void setImportMetadata(AnnotationMetadata importMetadata) {
120+
EnableReactiveMethodSecurity annotation = importMetadata.getAnnotations()
121+
.get(EnableReactiveMethodSecurity.class)
122+
.synthesize();
123+
this.useAnnotationParameters = annotation.useAnnotationParameters();
124+
}
125+
98126
}

config/src/test/java/org/springframework/security/config/annotation/method/configuration/MethodSecurityServiceImpl.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.

config/src/test/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfigurationTests.java

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
package org.springframework.security.config.annotation.method.configuration;
1818

1919
import java.io.Serializable;
20+
import java.lang.annotation.Retention;
21+
import java.lang.annotation.RetentionPolicy;
2022
import java.util.ArrayList;
2123
import java.util.Arrays;
2224
import java.util.List;
@@ -49,6 +51,10 @@
4951
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
5052
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
5153
import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
54+
import org.springframework.security.access.prepost.PostAuthorize;
55+
import org.springframework.security.access.prepost.PostFilter;
56+
import org.springframework.security.access.prepost.PreAuthorize;
57+
import org.springframework.security.access.prepost.PreFilter;
5258
import org.springframework.security.authorization.AuthorizationDecision;
5359
import org.springframework.security.authorization.AuthorizationEventPublisher;
5460
import org.springframework.security.authorization.AuthorizationManager;
@@ -587,6 +593,74 @@ public void allAnnotationsWhenAdviceAfterAllOffsetThenReturnsFilteredList() {
587593
assertThat(filtered).containsExactly("DoNotDrop");
588594
}
589595

596+
@Test
597+
@WithMockUser
598+
public void methodeWhenParameterizedPreAuthorizeMetaAnnotationThenPasses() {
599+
this.spring.register(MetaAnnotationPlaceholderConfig.class).autowire();
600+
MetaAnnotationService service = this.spring.getContext().getBean(MetaAnnotationService.class);
601+
assertThat(service.hasRole("USER")).isTrue();
602+
}
603+
604+
@Test
605+
@WithMockUser
606+
public void methodRoleWhenPreAuthorizeMetaAnnotationHardcodedParameterThenPasses() {
607+
this.spring.register(MetaAnnotationPlaceholderConfig.class).autowire();
608+
MetaAnnotationService service = this.spring.getContext().getBean(MetaAnnotationService.class);
609+
assertThat(service.hasUserRole()).isTrue();
610+
}
611+
612+
@Test
613+
public void methodWhenParameterizedAnnotationThenFails() {
614+
this.spring.register(MetaAnnotationPlaceholderConfig.class).autowire();
615+
MetaAnnotationService service = this.spring.getContext().getBean(MetaAnnotationService.class);
616+
assertThatExceptionOfType(IllegalArgumentException.class)
617+
.isThrownBy(service::placeholdersOnlyResolvedByMetaAnnotations);
618+
}
619+
620+
@Test
621+
@WithMockUser(authorities = "SCOPE_message:read")
622+
public void methodWhenMultiplePlaceholdersHasAuthorityThenPasses() {
623+
this.spring.register(MetaAnnotationPlaceholderConfig.class).autowire();
624+
MetaAnnotationService service = this.spring.getContext().getBean(MetaAnnotationService.class);
625+
assertThat(service.readMessage()).isEqualTo("message");
626+
}
627+
628+
@Test
629+
@WithMockUser(roles = "ADMIN")
630+
public void methodWhenMultiplePlaceholdersHasRoleThenPasses() {
631+
this.spring.register(MetaAnnotationPlaceholderConfig.class).autowire();
632+
MetaAnnotationService service = this.spring.getContext().getBean(MetaAnnotationService.class);
633+
assertThat(service.readMessage()).isEqualTo("message");
634+
}
635+
636+
@Test
637+
@WithMockUser
638+
public void methodWhenPostAuthorizeMetaAnnotationThenAuthorizes() {
639+
this.spring.register(MetaAnnotationPlaceholderConfig.class).autowire();
640+
MetaAnnotationService service = this.spring.getContext().getBean(MetaAnnotationService.class);
641+
service.startsWithDave("daveMatthews");
642+
assertThatExceptionOfType(AccessDeniedException.class)
643+
.isThrownBy(() -> service.startsWithDave("jenniferHarper"));
644+
}
645+
646+
@Test
647+
@WithMockUser
648+
public void methodWhenPreFilterMetaAnnotationThenFilters() {
649+
this.spring.register(MetaAnnotationPlaceholderConfig.class).autowire();
650+
MetaAnnotationService service = this.spring.getContext().getBean(MetaAnnotationService.class);
651+
assertThat(service.parametersContainDave(new ArrayList<>(List.of("dave", "carla", "vanessa", "paul"))))
652+
.containsExactly("dave");
653+
}
654+
655+
@Test
656+
@WithMockUser
657+
public void methodWhenPostFilterMetaAnnotationThenFilters() {
658+
this.spring.register(MetaAnnotationPlaceholderConfig.class).autowire();
659+
MetaAnnotationService service = this.spring.getContext().getBean(MetaAnnotationService.class);
660+
assertThat(service.resultsContainDave(new ArrayList<>(List.of("dave", "carla", "vanessa", "paul"))))
661+
.containsExactly("dave");
662+
}
663+
590664
private static Consumer<ConfigurableWebApplicationContext> disallowBeanOverriding() {
591665
return (context) -> ((AnnotationConfigWebApplicationContext) context).setAllowBeanDefinitionOverriding(false);
592666
}
@@ -890,4 +964,95 @@ Authz authz() {
890964

891965
}
892966

967+
@Configuration
968+
@EnableMethodSecurity(useAnnotationParameters = true)
969+
static class MetaAnnotationPlaceholderConfig {
970+
971+
@Bean
972+
MetaAnnotationService methodSecurityService() {
973+
return new MetaAnnotationService();
974+
}
975+
976+
}
977+
978+
static class MetaAnnotationService {
979+
980+
@RequireRole(role = "#role")
981+
boolean hasRole(String role) {
982+
return true;
983+
}
984+
985+
@RequireRole(role = "'USER'")
986+
boolean hasUserRole() {
987+
return true;
988+
}
989+
990+
@PreAuthorize("hasRole({role})")
991+
void placeholdersOnlyResolvedByMetaAnnotations() {
992+
}
993+
994+
@HasClaim(claim = "message:read", roles = { "'ADMIN'" })
995+
String readMessage() {
996+
return "message";
997+
}
998+
999+
@ResultStartsWith("dave")
1000+
String startsWithDave(String value) {
1001+
return value;
1002+
}
1003+
1004+
@ParameterContains("dave")
1005+
List<String> parametersContainDave(List<String> list) {
1006+
return list;
1007+
}
1008+
1009+
@ResultContains("dave")
1010+
List<String> resultsContainDave(List<String> list) {
1011+
return list;
1012+
}
1013+
1014+
}
1015+
1016+
@Retention(RetentionPolicy.RUNTIME)
1017+
@PreAuthorize("hasRole({role})")
1018+
@interface RequireRole {
1019+
1020+
String role();
1021+
1022+
}
1023+
1024+
@Retention(RetentionPolicy.RUNTIME)
1025+
@PreAuthorize("hasAuthority('SCOPE_{claim}') || hasAnyRole({roles})")
1026+
@interface HasClaim {
1027+
1028+
String claim();
1029+
1030+
String[] roles() default {};
1031+
1032+
}
1033+
1034+
@Retention(RetentionPolicy.RUNTIME)
1035+
@PostAuthorize("returnObject.startsWith('{value}')")
1036+
@interface ResultStartsWith {
1037+
1038+
String value();
1039+
1040+
}
1041+
1042+
@Retention(RetentionPolicy.RUNTIME)
1043+
@PreFilter("filterObject.contains('{value}')")
1044+
@interface ParameterContains {
1045+
1046+
String value();
1047+
1048+
}
1049+
1050+
@Retention(RetentionPolicy.RUNTIME)
1051+
@PostFilter("filterObject.contains('{value}')")
1052+
@interface ResultContains {
1053+
1054+
String value();
1055+
1056+
}
1057+
8931058
}

0 commit comments

Comments
 (0)