Skip to content

Commit ed5305c

Browse files
committed
Add meta-annotation parameter support
1 parent e771267 commit ed5305c

21 files changed

+496
-127
lines changed

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

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

100+
/**
101+
* Indicate whether to resolve placeholders specified in authorization annotations
102+
* when used in a meta-annotation.
103+
* @return whether to resolve placeholders or not
104+
* @since 6.3
105+
*/
106+
boolean resolveAnnotationPlaceholders() default false;
107+
100108
}

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

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

77+
/**
78+
* Indicate whether to resolve placeholders specified in authorization annotations
79+
* when used in a meta-annotation.
80+
* @return whether to resolve placeholders or not
81+
* @since 6.3
82+
*/
83+
boolean resolveAnnotationPlaceholders() default false;
84+
7785
}

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 resolveAnnotationPlaceholders;
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.setResolveAnnotationPlaceholders(configuration.resolveAnnotationPlaceholders);
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.setResolveAnnotationPlaceholders(configuration.resolveAnnotationPlaceholders);
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.setResolveAnnotationPlaceholders(configuration.resolveAnnotationPlaceholders);
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.setResolveAnnotationPlaceholders(configuration.resolveAnnotationPlaceholders);
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.resolveAnnotationPlaceholders = annotation.resolveAnnotationPlaceholders();
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 resolveAnnotationPlaceholders;
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.setResolveAnnotationPlaceholders(configuration.resolveAnnotationPlaceholders);
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.setResolveAnnotationPlaceholders(configuration.resolveAnnotationPlaceholders);
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.setResolveAnnotationPlaceholders(configuration.resolveAnnotationPlaceholders);
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.setResolveAnnotationPlaceholders(configuration.resolveAnnotationPlaceholders);
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.resolveAnnotationPlaceholders = annotation.resolveAnnotationPlaceholders();
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: 166 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;
@@ -39,6 +41,7 @@
3941
import org.springframework.context.annotation.Import;
4042
import org.springframework.context.annotation.Role;
4143
import org.springframework.core.annotation.AnnotationConfigurationException;
44+
import org.springframework.expression.spel.SpelParseException;
4245
import org.springframework.security.access.AccessDeniedException;
4346
import org.springframework.security.access.PermissionEvaluator;
4447
import org.springframework.security.access.annotation.BusinessService;
@@ -49,6 +52,10 @@
4952
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
5053
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
5154
import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
55+
import org.springframework.security.access.prepost.PostAuthorize;
56+
import org.springframework.security.access.prepost.PostFilter;
57+
import org.springframework.security.access.prepost.PreAuthorize;
58+
import org.springframework.security.access.prepost.PreFilter;
5259
import org.springframework.security.authorization.AuthorizationDecision;
5360
import org.springframework.security.authorization.AuthorizationEventPublisher;
5461
import org.springframework.security.authorization.AuthorizationManager;
@@ -587,6 +594,74 @@ public void allAnnotationsWhenAdviceAfterAllOffsetThenReturnsFilteredList() {
587594
assertThat(filtered).containsExactly("DoNotDrop");
588595
}
589596

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

891966
}
892967

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

0 commit comments

Comments
 (0)