Skip to content

Commit 7e47831

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

18 files changed

+616
-120
lines changed

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

+6-4
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,9 @@ static PreFilterAuthorizationReactiveMethodInterceptor preFilterInterceptor(
5858
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
5959
static AuthorizationManagerBeforeReactiveMethodInterceptor preAuthorizeInterceptor(
6060
MethodSecurityExpressionHandler expressionHandler, ObjectProvider<ObservationRegistry> registryProvider) {
61-
ReactiveAuthorizationManager<MethodInvocation> authorizationManager = manager(
62-
new PreAuthorizeReactiveAuthorizationManager(expressionHandler), registryProvider);
61+
PreAuthorizeReactiveAuthorizationManager manager = new PreAuthorizeReactiveAuthorizationManager(
62+
expressionHandler);
63+
ReactiveAuthorizationManager<MethodInvocation> authorizationManager = manager(manager, registryProvider);
6364
return AuthorizationManagerBeforeReactiveMethodInterceptor.preAuthorize(authorizationManager);
6465
}
6566

@@ -74,8 +75,9 @@ static PostFilterAuthorizationReactiveMethodInterceptor postFilterInterceptor(
7475
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
7576
static AuthorizationManagerAfterReactiveMethodInterceptor postAuthorizeInterceptor(
7677
MethodSecurityExpressionHandler expressionHandler, ObjectProvider<ObservationRegistry> registryProvider) {
77-
ReactiveAuthorizationManager<MethodInvocationResult> authorizationManager = manager(
78-
new PostAuthorizeReactiveAuthorizationManager(expressionHandler), registryProvider);
78+
PostAuthorizeReactiveAuthorizationManager manager = new PostAuthorizeReactiveAuthorizationManager(
79+
expressionHandler);
80+
ReactiveAuthorizationManager<MethodInvocationResult> authorizationManager = manager(manager, registryProvider);
7981
return AuthorizationManagerAfterReactiveMethodInterceptor.postAuthorize(authorizationManager);
8082
}
8183

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

+1-1
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

+210
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,12 +51,21 @@
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;
5561
import org.springframework.security.authorization.method.AuthorizationInterceptorsOrder;
62+
import org.springframework.security.authorization.method.AuthorizationManagerAfterMethodInterceptor;
5663
import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor;
5764
import org.springframework.security.authorization.method.MethodInvocationResult;
65+
import org.springframework.security.authorization.method.PostAuthorizeAuthorizationManager;
66+
import org.springframework.security.authorization.method.PostFilterAuthorizationMethodInterceptor;
67+
import org.springframework.security.authorization.method.PreAuthorizeAuthorizationManager;
68+
import org.springframework.security.authorization.method.PreFilterAuthorizationMethodInterceptor;
5869
import org.springframework.security.config.annotation.SecurityContextChangedListenerConfig;
5970
import org.springframework.security.config.core.GrantedAuthorityDefaults;
6071
import org.springframework.security.config.test.SpringTestContext;
@@ -587,6 +598,74 @@ public void allAnnotationsWhenAdviceAfterAllOffsetThenReturnsFilteredList() {
587598
assertThat(filtered).containsExactly("DoNotDrop");
588599
}
589600

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

891970
}
892971

972+
@Configuration
973+
@EnableMethodSecurity(prePostEnabled = false)
974+
static class MetaAnnotationPlaceholderConfig {
975+
976+
@Bean
977+
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
978+
Advisor preAuthorize(SecurityContextHolderStrategy strategy) {
979+
PreAuthorizeAuthorizationManager preAuthorize = new PreAuthorizeAuthorizationManager();
980+
preAuthorize.setUseTemplates(true);
981+
AuthorizationManagerBeforeMethodInterceptor interceptor = AuthorizationManagerBeforeMethodInterceptor
982+
.preAuthorize(preAuthorize);
983+
interceptor.setSecurityContextHolderStrategy(strategy);
984+
return interceptor;
985+
}
986+
987+
@Bean
988+
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
989+
Advisor postAuthorize(SecurityContextHolderStrategy strategy) {
990+
PostAuthorizeAuthorizationManager postAuthorize = new PostAuthorizeAuthorizationManager();
991+
postAuthorize.setUseTemplates(true);
992+
AuthorizationManagerAfterMethodInterceptor interceptor = AuthorizationManagerAfterMethodInterceptor
993+
.postAuthorize(postAuthorize);
994+
interceptor.setSecurityContextHolderStrategy(strategy);
995+
return interceptor;
996+
}
997+
998+
@Bean
999+
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
1000+
Advisor preFilter(SecurityContextHolderStrategy strategy) {
1001+
PreFilterAuthorizationMethodInterceptor preFilter = new PreFilterAuthorizationMethodInterceptor();
1002+
preFilter.setUseTemplates(true);
1003+
preFilter.setSecurityContextHolderStrategy(strategy);
1004+
return preFilter;
1005+
}
1006+
1007+
@Bean
1008+
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
1009+
Advisor postFilter(SecurityContextHolderStrategy strategy) {
1010+
PostFilterAuthorizationMethodInterceptor postFilter = new PostFilterAuthorizationMethodInterceptor();
1011+
postFilter.setUseTemplates(true);
1012+
postFilter.setSecurityContextHolderStrategy(strategy);
1013+
return postFilter;
1014+
}
1015+
1016+
@Bean
1017+
MetaAnnotationService methodSecurityService() {
1018+
return new MetaAnnotationService();
1019+
}
1020+
1021+
}
1022+
1023+
static class MetaAnnotationService {
1024+
1025+
@RequireRole(role = "#role")
1026+
boolean hasRole(String role) {
1027+
return true;
1028+
}
1029+
1030+
@RequireRole(role = "'USER'")
1031+
boolean hasUserRole() {
1032+
return true;
1033+
}
1034+
1035+
@PreAuthorize("hasRole({role})")
1036+
void placeholdersOnlyResolvedByMetaAnnotations() {
1037+
}
1038+
1039+
@HasClaim(claim = "message:read", roles = { "'ADMIN'" })
1040+
String readMessage() {
1041+
return "message";
1042+
}
1043+
1044+
@ResultStartsWith("dave")
1045+
String startsWithDave(String value) {
1046+
return value;
1047+
}
1048+
1049+
@ParameterContains("dave")
1050+
List<String> parametersContainDave(List<String> list) {
1051+
return list;
1052+
}
1053+
1054+
@ResultContains("dave")
1055+
List<String> resultsContainDave(List<String> list) {
1056+
return list;
1057+
}
1058+
1059+
}
1060+
1061+
@Retention(RetentionPolicy.RUNTIME)
1062+
@PreAuthorize("hasRole({role})")
1063+
@interface RequireRole {
1064+
1065+
String role();
1066+
1067+
}
1068+
1069+
@Retention(RetentionPolicy.RUNTIME)
1070+
@PreAuthorize("hasAuthority('SCOPE_{claim}') || hasAnyRole({roles})")
1071+
@interface HasClaim {
1072+
1073+
String claim();
1074+
1075+
String[] roles() default {};
1076+
1077+
}
1078+
1079+
@Retention(RetentionPolicy.RUNTIME)
1080+
@PostAuthorize("returnObject.startsWith('{value}')")
1081+
@interface ResultStartsWith {
1082+
1083+
String value();
1084+
1085+
}
1086+
1087+
@Retention(RetentionPolicy.RUNTIME)
1088+
@PreFilter("filterObject.contains('{value}')")
1089+
@interface ParameterContains {
1090+
1091+
String value();
1092+
1093+
}
1094+
1095+
@Retention(RetentionPolicy.RUNTIME)
1096+
@PostFilter("filterObject.contains('{value}')")
1097+
@interface ResultContains {
1098+
1099+
String value();
1100+
1101+
}
1102+
8931103
}

core/src/main/java/org/springframework/security/authorization/method/AbstractExpressionAttributeRegistry.java

+32
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,20 @@
1616

1717
package org.springframework.security.authorization.method;
1818

19+
import java.lang.annotation.Annotation;
20+
import java.lang.reflect.AnnotatedElement;
1921
import java.lang.reflect.Method;
2022
import java.util.Map;
2123
import java.util.concurrent.ConcurrentHashMap;
24+
import java.util.function.Function;
2225

2326
import org.aopalliance.intercept.MethodInvocation;
2427

2528
import org.springframework.core.MethodClassKey;
2629
import org.springframework.lang.NonNull;
30+
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
31+
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
32+
import org.springframework.util.Assert;
2733

2834
/**
2935
* For internal use only, as this contract is likely to change
@@ -35,6 +41,10 @@ abstract class AbstractExpressionAttributeRegistry<T extends ExpressionAttribute
3541

3642
private final Map<MethodClassKey, T> cachedAttributes = new ConcurrentHashMap<>();
3743

44+
private MethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
45+
46+
private boolean useAnnotationParameters;
47+
3848
/**
3949
* Returns an {@link ExpressionAttribute} for the {@link MethodInvocation}.
4050
* @param mi the {@link MethodInvocation} to use
@@ -58,6 +68,28 @@ final T getAttribute(Method method, Class<?> targetClass) {
5868
return this.cachedAttributes.computeIfAbsent(cacheKey, (k) -> resolveAttribute(method, targetClass));
5969
}
6070

71+
final <A extends Annotation> Function<AnnotatedElement, A> findUniqueAnnotation(Class<A> type) {
72+
return (this.useAnnotationParameters) ? AuthorizationAnnotationUtils.withPlaceholderResolver(type)
73+
: AuthorizationAnnotationUtils.withDefaults(type);
74+
}
75+
76+
/**
77+
* Returns the {@link MethodSecurityExpressionHandler}.
78+
* @return the {@link MethodSecurityExpressionHandler} to use
79+
*/
80+
MethodSecurityExpressionHandler getExpressionHandler() {
81+
return this.expressionHandler;
82+
}
83+
84+
void setExpressionHandler(MethodSecurityExpressionHandler expressionHandler) {
85+
Assert.notNull(expressionHandler, "expressionHandler cannot be null");
86+
this.expressionHandler = expressionHandler;
87+
}
88+
89+
void setUseAnnotationParameters(boolean useAnnotationParameters) {
90+
this.useAnnotationParameters = useAnnotationParameters;
91+
}
92+
6193
/**
6294
* Subclasses should implement this method to provide the non-null
6395
* {@link ExpressionAttribute} for the method and the target class.

0 commit comments

Comments
 (0)