Skip to content

Commit 8439a8c

Browse files
committed
Support SpEL Returning AuthorizationDecision
Closes gh-14599
1 parent 9b79f68 commit 8439a8c

File tree

28 files changed

+520
-199
lines changed

28 files changed

+520
-199
lines changed

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

+30-1
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,30 @@
1919
import java.util.function.Supplier;
2020

2121
import io.micrometer.observation.ObservationRegistry;
22+
import org.aopalliance.intercept.MethodInvocation;
2223

2324
import org.springframework.beans.factory.ObjectProvider;
2425
import org.springframework.security.authorization.AuthorizationDecision;
2526
import org.springframework.security.authorization.AuthorizationManager;
27+
import org.springframework.security.authorization.AuthorizationResult;
2628
import org.springframework.security.authorization.ObservationAuthorizationManager;
29+
import org.springframework.security.authorization.method.MethodAuthorizationDeniedHandler;
30+
import org.springframework.security.authorization.method.MethodAuthorizationDeniedPostProcessor;
31+
import org.springframework.security.authorization.method.MethodInvocationResult;
32+
import org.springframework.security.authorization.method.ThrowingMethodAuthorizationDeniedHandler;
33+
import org.springframework.security.authorization.method.ThrowingMethodAuthorizationDeniedPostProcessor;
2734
import org.springframework.security.core.Authentication;
2835
import org.springframework.util.function.SingletonSupplier;
2936

30-
final class DeferringObservationAuthorizationManager<T> implements AuthorizationManager<T> {
37+
final class DeferringObservationAuthorizationManager<T>
38+
implements AuthorizationManager<T>, MethodAuthorizationDeniedHandler, MethodAuthorizationDeniedPostProcessor {
3139

3240
private final Supplier<AuthorizationManager<T>> delegate;
3341

42+
private MethodAuthorizationDeniedHandler handler = new ThrowingMethodAuthorizationDeniedHandler();
43+
44+
private MethodAuthorizationDeniedPostProcessor postProcessor = new ThrowingMethodAuthorizationDeniedPostProcessor();
45+
3446
DeferringObservationAuthorizationManager(ObjectProvider<ObservationRegistry> provider,
3547
AuthorizationManager<T> delegate) {
3648
this.delegate = SingletonSupplier.of(() -> {
@@ -40,11 +52,28 @@ final class DeferringObservationAuthorizationManager<T> implements Authorization
4052
}
4153
return new ObservationAuthorizationManager<>(registry, delegate);
4254
});
55+
if (delegate instanceof MethodAuthorizationDeniedHandler h) {
56+
this.handler = h;
57+
}
58+
if (delegate instanceof MethodAuthorizationDeniedPostProcessor p) {
59+
this.postProcessor = p;
60+
}
4361
}
4462

4563
@Override
4664
public AuthorizationDecision check(Supplier<Authentication> authentication, T object) {
4765
return this.delegate.get().check(authentication, object);
4866
}
4967

68+
@Override
69+
public Object handle(MethodInvocation methodInvocation, AuthorizationResult authorizationResult) {
70+
return this.handler.handle(methodInvocation, authorizationResult);
71+
}
72+
73+
@Override
74+
public Object postProcessResult(MethodInvocationResult methodInvocationResult,
75+
AuthorizationResult authorizationResult) {
76+
return this.postProcessor.postProcessResult(methodInvocationResult, authorizationResult);
77+
}
78+
5079
}

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

+30-1
Original file line numberDiff line numberDiff line change
@@ -19,19 +19,31 @@
1919
import java.util.function.Supplier;
2020

2121
import io.micrometer.observation.ObservationRegistry;
22+
import org.aopalliance.intercept.MethodInvocation;
2223
import reactor.core.publisher.Mono;
2324

2425
import org.springframework.beans.factory.ObjectProvider;
2526
import org.springframework.security.authorization.AuthorizationDecision;
27+
import org.springframework.security.authorization.AuthorizationResult;
2628
import org.springframework.security.authorization.ObservationReactiveAuthorizationManager;
2729
import org.springframework.security.authorization.ReactiveAuthorizationManager;
30+
import org.springframework.security.authorization.method.MethodAuthorizationDeniedHandler;
31+
import org.springframework.security.authorization.method.MethodAuthorizationDeniedPostProcessor;
32+
import org.springframework.security.authorization.method.MethodInvocationResult;
33+
import org.springframework.security.authorization.method.ThrowingMethodAuthorizationDeniedHandler;
34+
import org.springframework.security.authorization.method.ThrowingMethodAuthorizationDeniedPostProcessor;
2835
import org.springframework.security.core.Authentication;
2936
import org.springframework.util.function.SingletonSupplier;
3037

31-
final class DeferringObservationReactiveAuthorizationManager<T> implements ReactiveAuthorizationManager<T> {
38+
final class DeferringObservationReactiveAuthorizationManager<T> implements ReactiveAuthorizationManager<T>,
39+
MethodAuthorizationDeniedHandler, MethodAuthorizationDeniedPostProcessor {
3240

3341
private final Supplier<ReactiveAuthorizationManager<T>> delegate;
3442

43+
private MethodAuthorizationDeniedHandler handler = new ThrowingMethodAuthorizationDeniedHandler();
44+
45+
private MethodAuthorizationDeniedPostProcessor postProcessor = new ThrowingMethodAuthorizationDeniedPostProcessor();
46+
3547
DeferringObservationReactiveAuthorizationManager(ObjectProvider<ObservationRegistry> provider,
3648
ReactiveAuthorizationManager<T> delegate) {
3749
this.delegate = SingletonSupplier.of(() -> {
@@ -41,11 +53,28 @@ final class DeferringObservationReactiveAuthorizationManager<T> implements React
4153
}
4254
return new ObservationReactiveAuthorizationManager<>(registry, delegate);
4355
});
56+
if (delegate instanceof MethodAuthorizationDeniedHandler h) {
57+
this.handler = h;
58+
}
59+
if (delegate instanceof MethodAuthorizationDeniedPostProcessor p) {
60+
this.postProcessor = p;
61+
}
4462
}
4563

4664
@Override
4765
public Mono<AuthorizationDecision> check(Mono<Authentication> authentication, T object) {
4866
return this.delegate.get().check(authentication, object);
4967
}
5068

69+
@Override
70+
public Object handle(MethodInvocation methodInvocation, AuthorizationResult authorizationResult) {
71+
return this.handler.handle(methodInvocation, authorizationResult);
72+
}
73+
74+
@Override
75+
public Object postProcessResult(MethodInvocationResult methodInvocationResult,
76+
AuthorizationResult authorizationResult) {
77+
return this.postProcessor.postProcessResult(methodInvocationResult, authorizationResult);
78+
}
79+
5180
}

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

+18
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818

1919
import reactor.core.publisher.Mono;
2020

21+
import org.springframework.security.authorization.AuthorizationDecision;
22+
import org.springframework.security.authorization.AuthorizationResult;
2123
import org.springframework.security.core.Authentication;
2224
import org.springframework.stereotype.Component;
2325

@@ -45,4 +47,20 @@ public boolean check(Authentication authentication, String message) {
4547
return message != null && message.contains(authentication.getName());
4648
}
4749

50+
public AuthorizationResult checkResult(boolean result) {
51+
return new AuthzResult(result);
52+
}
53+
54+
public Mono<AuthorizationResult> checkReactiveResult(boolean result) {
55+
return Mono.just(checkResult(result));
56+
}
57+
58+
public static class AuthzResult extends AuthorizationDecision {
59+
60+
public AuthzResult(boolean granted) {
61+
super(granted);
62+
}
63+
64+
}
65+
4866
}

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

+5
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,11 @@ public interface MethodSecurityService {
173173
@PreAuthorize(value = "hasRole('ADMIN')", handlerClass = UserFallbackDeniedHandler.class)
174174
UserRecordWithEmailProtected getUserWithFallbackWhenUnauthorized();
175175

176+
@PreAuthorize(value = "@authz.checkResult(#result)", handlerClass = MethodAuthorizationDeniedHandler.class)
177+
@PostAuthorize(value = "@authz.checkResult(!#result)",
178+
postProcessorClass = MethodAuthorizationDeniedPostProcessor.class)
179+
String checkCustomResult(boolean result);
180+
176181
class StarMaskingHandler implements MethodAuthorizationDeniedHandler {
177182

178183
@Override

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

+10
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,14 @@ MethodSecurityService service() {
2828
return new MethodSecurityServiceImpl();
2929
}
3030

31+
@Bean
32+
ReactiveMethodSecurityService reactiveService() {
33+
return new ReactiveMethodSecurityServiceImpl();
34+
}
35+
36+
@Bean
37+
Authz authz() {
38+
return new Authz();
39+
}
40+
3141
}

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

+5
Original file line numberDiff line numberDiff line change
@@ -197,4 +197,9 @@ public UserRecordWithEmailProtected getUserWithFallbackWhenUnauthorized() {
197197
return new UserRecordWithEmailProtected("username", "[email protected]");
198198
}
199199

200+
@Override
201+
public String checkCustomResult(boolean result) {
202+
return "ok";
203+
}
204+
200205
}

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

+40
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@
6666
import org.springframework.security.authorization.method.AuthorizationInterceptorsOrder;
6767
import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor;
6868
import org.springframework.security.authorization.method.AuthorizeReturnObject;
69+
import org.springframework.security.authorization.method.MethodAuthorizationDeniedHandler;
70+
import org.springframework.security.authorization.method.MethodAuthorizationDeniedPostProcessor;
6971
import org.springframework.security.authorization.method.MethodInvocationResult;
7072
import org.springframework.security.authorization.method.PrePostTemplateDefaults;
7173
import org.springframework.security.config.Customizer;
@@ -92,6 +94,8 @@
9294
import static org.mockito.Mockito.atLeastOnce;
9395
import static org.mockito.Mockito.mock;
9496
import static org.mockito.Mockito.verify;
97+
import static org.mockito.Mockito.verifyNoInteractions;
98+
import static org.mockito.Mockito.verifyNoMoreInteractions;
9599

96100
/**
97101
* Tests for {@link PrePostMethodSecurityConfiguration}.
@@ -925,6 +929,23 @@ void getUserWhenNotAuthorizedAndHandlerFallbackValueThenReturnFallbackValue() {
925929
assertThat(user.name()).isEqualTo("Protected");
926930
}
927931

932+
@Test
933+
@WithMockUser
934+
void getUserWhenNotAuthorizedThenHandlerUsesCustomAuthorizationDecision() {
935+
this.spring.register(MethodSecurityServiceConfig.class, CustomResultConfig.class).autowire();
936+
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
937+
MethodAuthorizationDeniedHandler handler = this.spring.getContext()
938+
.getBean(MethodAuthorizationDeniedHandler.class);
939+
MethodAuthorizationDeniedPostProcessor postProcessor = this.spring.getContext()
940+
.getBean(MethodAuthorizationDeniedPostProcessor.class);
941+
assertThat(service.checkCustomResult(false)).isNull();
942+
verify(handler).handle(any(), any(Authz.AuthzResult.class));
943+
verifyNoInteractions(postProcessor);
944+
assertThat(service.checkCustomResult(true)).isNull();
945+
verify(postProcessor).postProcessResult(any(), any(Authz.AuthzResult.class));
946+
verifyNoMoreInteractions(handler);
947+
}
948+
928949
private static Consumer<ConfigurableWebApplicationContext> disallowBeanOverriding() {
929950
return (context) -> ((AnnotationConfigWebApplicationContext) context).setAllowBeanDefinitionOverriding(false);
930951
}
@@ -1449,4 +1470,23 @@ public String getName() {
14491470

14501471
}
14511472

1473+
@EnableMethodSecurity
1474+
static class CustomResultConfig {
1475+
1476+
MethodAuthorizationDeniedHandler handler = mock(MethodAuthorizationDeniedHandler.class);
1477+
1478+
MethodAuthorizationDeniedPostProcessor postProcessor = mock(MethodAuthorizationDeniedPostProcessor.class);
1479+
1480+
@Bean
1481+
MethodAuthorizationDeniedHandler methodAuthorizationDeniedHandler() {
1482+
return this.handler;
1483+
}
1484+
1485+
@Bean
1486+
MethodAuthorizationDeniedPostProcessor methodAuthorizationDeniedPostProcessor() {
1487+
return this.postProcessor;
1488+
}
1489+
1490+
}
1491+
14521492
}

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

+45-1
Original file line numberDiff line numberDiff line change
@@ -47,15 +47,23 @@
4747
import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory;
4848
import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory.TargetVisitor;
4949
import org.springframework.security.authorization.method.AuthorizeReturnObject;
50+
import org.springframework.security.authorization.method.MethodAuthorizationDeniedHandler;
51+
import org.springframework.security.authorization.method.MethodAuthorizationDeniedPostProcessor;
5052
import org.springframework.security.config.Customizer;
5153
import org.springframework.security.config.core.GrantedAuthorityDefaults;
5254
import org.springframework.security.config.test.SpringTestContext;
5355
import org.springframework.security.config.test.SpringTestContextExtension;
5456
import org.springframework.security.core.Authentication;
5557
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
5658
import org.springframework.security.core.userdetails.User;
59+
import org.springframework.security.test.context.support.WithMockUser;
5760

5861
import static org.assertj.core.api.Assertions.assertThat;
62+
import static org.mockito.ArgumentMatchers.any;
63+
import static org.mockito.Mockito.mock;
64+
import static org.mockito.Mockito.verify;
65+
import static org.mockito.Mockito.verifyNoInteractions;
66+
import static org.mockito.Mockito.verifyNoMoreInteractions;
5967

6068
/**
6169
* @author Tadaya Tsuyukubo
@@ -65,7 +73,7 @@ public class ReactiveMethodSecurityConfigurationTests {
6573

6674
public final SpringTestContext spring = new SpringTestContext(this);
6775

68-
@Autowired
76+
@Autowired(required = false)
6977
DefaultMethodSecurityExpressionHandler methodSecurityExpressionHandler;
7078

7179
@Test
@@ -212,6 +220,23 @@ public void findAllWhenNestedPreAuthorizeThenAuthorizes() {
212220
.verifyError(AccessDeniedException.class);
213221
}
214222

223+
@Test
224+
@WithMockUser
225+
void getUserWhenNotAuthorizedThenHandlerUsesCustomAuthorizationDecision() {
226+
this.spring.register(MethodSecurityServiceConfig.class, CustomResultConfig.class).autowire();
227+
ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class);
228+
MethodAuthorizationDeniedHandler handler = this.spring.getContext()
229+
.getBean(MethodAuthorizationDeniedHandler.class);
230+
MethodAuthorizationDeniedPostProcessor postProcessor = this.spring.getContext()
231+
.getBean(MethodAuthorizationDeniedPostProcessor.class);
232+
assertThat(service.checkCustomResult(false).block()).isNull();
233+
verify(handler).handle(any(), any(Authz.AuthzResult.class));
234+
verifyNoInteractions(postProcessor);
235+
assertThat(service.checkCustomResult(true).block()).isNull();
236+
verify(postProcessor).postProcessResult(any(), any(Authz.AuthzResult.class));
237+
verifyNoMoreInteractions(handler);
238+
}
239+
215240
private static Consumer<User.UserBuilder> authorities(String... authorities) {
216241
return (builder) -> builder.authorities(authorities);
217242
}
@@ -353,4 +378,23 @@ public Mono<String> getName() {
353378

354379
}
355380

381+
@EnableReactiveMethodSecurity
382+
static class CustomResultConfig {
383+
384+
MethodAuthorizationDeniedHandler handler = mock(MethodAuthorizationDeniedHandler.class);
385+
386+
MethodAuthorizationDeniedPostProcessor postProcessor = mock(MethodAuthorizationDeniedPostProcessor.class);
387+
388+
@Bean
389+
MethodAuthorizationDeniedHandler methodAuthorizationDeniedHandler() {
390+
return this.handler;
391+
}
392+
393+
@Bean
394+
MethodAuthorizationDeniedPostProcessor methodAuthorizationDeniedPostProcessor() {
395+
return this.postProcessor;
396+
}
397+
398+
}
399+
356400
}

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

+5
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,11 @@ public interface ReactiveMethodSecurityService {
8585
@Mask(expression = "@myMasker.getMask(returnObject)")
8686
Mono<String> postAuthorizeWithMaskAnnotationUsingBean();
8787

88+
@PreAuthorize(value = "@authz.checkReactiveResult(#result)", handlerClass = MethodAuthorizationDeniedHandler.class)
89+
@PostAuthorize(value = "@authz.checkReactiveResult(!#result)",
90+
postProcessorClass = MethodAuthorizationDeniedPostProcessor.class)
91+
Mono<String> checkCustomResult(boolean result);
92+
8893
class StarMaskingHandler implements MethodAuthorizationDeniedHandler {
8994

9095
@Override

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

+5
Original file line numberDiff line numberDiff line change
@@ -82,4 +82,9 @@ public Mono<String> postAuthorizeWithMaskAnnotationUsingBean() {
8282
return Mono.just("ok");
8383
}
8484

85+
@Override
86+
public Mono<String> checkCustomResult(boolean result) {
87+
return Mono.just("ok");
88+
}
89+
8590
}

0 commit comments

Comments
 (0)