Skip to content

Commit 618c9b6

Browse files
committed
Handle SpEL AuthorizationDeniedExceptions
Closes gh-14600
1 parent 61eba00 commit 618c9b6

13 files changed

+262
-26
lines changed

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

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import org.springframework.security.access.prepost.PostAuthorize;
3131
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
3232
import org.springframework.security.authorization.AuthorizationDecision;
33+
import org.springframework.security.authorization.AuthorizationDeniedException;
3334
import org.springframework.security.authorization.AuthorizationEventPublisher;
3435
import org.springframework.security.authorization.AuthorizationManager;
3536
import org.springframework.security.core.Authentication;
@@ -172,7 +173,13 @@ public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy strat
172173
private Object attemptAuthorization(MethodInvocation mi, Object result) {
173174
this.logger.debug(LogMessage.of(() -> "Authorizing method invocation " + mi));
174175
MethodInvocationResult object = new MethodInvocationResult(mi, result);
175-
AuthorizationDecision decision = this.authorizationManager.check(this::getAuthentication, object);
176+
AuthorizationDecision decision;
177+
try {
178+
decision = this.authorizationManager.check(this::getAuthentication, object);
179+
}
180+
catch (AuthorizationDeniedException denied) {
181+
return postProcess(object, denied);
182+
}
176183
this.eventPublisher.publishAuthorizationEvent(this::getAuthentication, object, decision);
177184
if (decision != null && !decision.isGranted()) {
178185
this.logger.debug(LogMessage.of(() -> "Failed to authorize " + mi + " with authorization manager "
@@ -183,6 +190,13 @@ private Object attemptAuthorization(MethodInvocation mi, Object result) {
183190
return result;
184191
}
185192

193+
private Object postProcess(MethodInvocationResult mi, AuthorizationDeniedException denied) {
194+
if (this.authorizationManager instanceof MethodAuthorizationDeniedPostProcessor postProcessableDecision) {
195+
return postProcessableDecision.postProcessResult(mi, denied);
196+
}
197+
return this.defaultPostProcessor.postProcessResult(mi, denied);
198+
}
199+
186200
private Object postProcess(MethodInvocationResult mi, AuthorizationDecision decision) {
187201
if (this.authorizationManager instanceof MethodAuthorizationDeniedPostProcessor postProcessableDecision) {
188202
return postProcessableDecision.postProcessResult(mi, decision);

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

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import org.springframework.core.ReactiveAdapterRegistry;
3535
import org.springframework.security.access.prepost.PostAuthorize;
3636
import org.springframework.security.authorization.AuthorizationDecision;
37+
import org.springframework.security.authorization.AuthorizationDeniedException;
3738
import org.springframework.security.authorization.ReactiveAuthorizationManager;
3839
import org.springframework.security.core.Authentication;
3940
import org.springframework.util.Assert;
@@ -151,7 +152,32 @@ private Mono<Object> postAuthorize(Mono<Authentication> authentication, MethodIn
151152
MethodInvocationResult invocationResult = new MethodInvocationResult(mi, result);
152153
return this.authorizationManager.check(authentication, invocationResult)
153154
.switchIfEmpty(Mono.just(new AuthorizationDecision(false)))
154-
.flatMap((decision) -> postProcess(decision, invocationResult));
155+
.materialize()
156+
.flatMap((signal) -> {
157+
if (!signal.hasError()) {
158+
AuthorizationDecision decision = signal.get();
159+
return postProcess(decision, invocationResult);
160+
}
161+
if (signal.getThrowable() instanceof AuthorizationDeniedException denied) {
162+
return postProcess(denied, invocationResult);
163+
}
164+
return Mono.error(signal.getThrowable());
165+
});
166+
}
167+
168+
private Mono<Object> postProcess(AuthorizationDeniedException denied,
169+
MethodInvocationResult methodInvocationResult) {
170+
return Mono.fromSupplier(() -> {
171+
if (this.authorizationManager instanceof MethodAuthorizationDeniedPostProcessor postProcessableDecision) {
172+
return postProcessableDecision.postProcessResult(methodInvocationResult, denied);
173+
}
174+
return this.defaultPostProcessor.postProcessResult(methodInvocationResult, denied);
175+
}).flatMap((processedResult) -> {
176+
if (Mono.class.isAssignableFrom(processedResult.getClass())) {
177+
return (Mono<?>) processedResult;
178+
}
179+
return Mono.justOrEmpty(processedResult);
180+
});
155181
}
156182

157183
private Mono<Object> postProcess(AuthorizationDecision decision, MethodInvocationResult methodInvocationResult) {

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

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,10 @@
3434
import org.springframework.security.access.prepost.PreAuthorize;
3535
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
3636
import org.springframework.security.authorization.AuthorizationDecision;
37+
import org.springframework.security.authorization.AuthorizationDeniedException;
3738
import org.springframework.security.authorization.AuthorizationEventPublisher;
3839
import org.springframework.security.authorization.AuthorizationManager;
40+
import org.springframework.security.authorization.AuthorizationResult;
3941
import org.springframework.security.core.Authentication;
4042
import org.springframework.security.core.context.SecurityContextHolder;
4143
import org.springframework.security.core.context.SecurityContextHolderStrategy;
@@ -245,7 +247,13 @@ public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy secur
245247

246248
private Object attemptAuthorization(MethodInvocation mi) throws Throwable {
247249
this.logger.debug(LogMessage.of(() -> "Authorizing method invocation " + mi));
248-
AuthorizationDecision decision = this.authorizationManager.check(this::getAuthentication, mi);
250+
AuthorizationDecision decision;
251+
try {
252+
decision = this.authorizationManager.check(this::getAuthentication, mi);
253+
}
254+
catch (AuthorizationDeniedException denied) {
255+
return handle(mi, denied);
256+
}
249257
this.eventPublisher.publishAuthorizationEvent(this::getAuthentication, mi, decision);
250258
if (decision != null && !decision.isGranted()) {
251259
this.logger.debug(LogMessage.of(() -> "Failed to authorize " + mi + " with authorization manager "
@@ -256,7 +264,14 @@ private Object attemptAuthorization(MethodInvocation mi) throws Throwable {
256264
return mi.proceed();
257265
}
258266

259-
private Object handle(MethodInvocation mi, AuthorizationDecision decision) {
267+
private Object handle(MethodInvocation mi, AuthorizationDeniedException denied) {
268+
if (this.authorizationManager instanceof MethodAuthorizationDeniedHandler handler) {
269+
return handler.handle(mi, denied);
270+
}
271+
return this.defaultHandler.handle(mi, denied);
272+
}
273+
274+
private Object handle(MethodInvocation mi, AuthorizationResult decision) {
260275
if (this.authorizationManager instanceof MethodAuthorizationDeniedHandler handler) {
261276
return handler.handle(mi, decision);
262277
}

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

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import org.springframework.core.ReactiveAdapterRegistry;
3434
import org.springframework.security.access.prepost.PreAuthorize;
3535
import org.springframework.security.authorization.AuthorizationDecision;
36+
import org.springframework.security.authorization.AuthorizationDeniedException;
3637
import org.springframework.security.authorization.ReactiveAuthorizationManager;
3738
import org.springframework.security.core.Authentication;
3839
import org.springframework.util.Assert;
@@ -140,26 +141,56 @@ private Flux<Object> preAuthorized(MethodInvocation mi, Flux<Object> mapping) {
140141
Mono<Authentication> authentication = ReactiveAuthenticationUtils.getAuthentication();
141142
return this.authorizationManager.check(authentication, mi)
142143
.switchIfEmpty(Mono.just(new AuthorizationDecision(false)))
143-
.flatMapMany((decision) -> {
144-
if (decision.isGranted()) {
145-
return mapping;
144+
.materialize()
145+
.flatMapMany((signal) -> {
146+
if (!signal.hasError()) {
147+
AuthorizationDecision decision = signal.get();
148+
if (decision.isGranted()) {
149+
return mapping;
150+
}
151+
return postProcess(decision, mi);
146152
}
147-
return postProcess(decision, mi);
153+
if (signal.getThrowable() instanceof AuthorizationDeniedException denied) {
154+
return postProcess(denied, mi);
155+
}
156+
return Mono.error(signal.getThrowable());
148157
});
149158
}
150159

151160
private Mono<Object> preAuthorized(MethodInvocation mi, Mono<Object> mapping) {
152161
Mono<Authentication> authentication = ReactiveAuthenticationUtils.getAuthentication();
153162
return this.authorizationManager.check(authentication, mi)
154163
.switchIfEmpty(Mono.just(new AuthorizationDecision(false)))
155-
.flatMap((decision) -> {
156-
if (decision.isGranted()) {
157-
return mapping;
164+
.materialize()
165+
.flatMap((signal) -> {
166+
if (!signal.hasError()) {
167+
AuthorizationDecision decision = signal.get();
168+
if (decision.isGranted()) {
169+
return mapping;
170+
}
171+
return postProcess(decision, mi);
172+
}
173+
if (signal.getThrowable() instanceof AuthorizationDeniedException denied) {
174+
return postProcess(denied, mi);
158175
}
159-
return postProcess(decision, mi);
176+
return Mono.error(signal.getThrowable());
160177
});
161178
}
162179

180+
private Mono<Object> postProcess(AuthorizationDeniedException denied, MethodInvocation mi) {
181+
return Mono.fromSupplier(() -> {
182+
if (this.authorizationManager instanceof MethodAuthorizationDeniedHandler handler) {
183+
return handler.handle(mi, denied);
184+
}
185+
return this.defaultHandler.handle(mi, denied);
186+
}).flatMap((processedResult) -> {
187+
if (Mono.class.isAssignableFrom(processedResult.getClass())) {
188+
return (Mono<?>) processedResult;
189+
}
190+
return Mono.justOrEmpty(processedResult);
191+
});
192+
}
193+
163194
private Mono<Object> postProcess(AuthorizationDecision decision, MethodInvocation mi) {
164195
return Mono.fromSupplier(() -> {
165196
if (this.authorizationManager instanceof MethodAuthorizationDeniedHandler handler) {

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import org.aopalliance.intercept.MethodInvocation;
2020

2121
import org.springframework.lang.Nullable;
22+
import org.springframework.security.authorization.AuthorizationDeniedException;
2223
import org.springframework.security.authorization.AuthorizationResult;
2324

2425
/**
@@ -43,4 +44,18 @@ public interface MethodAuthorizationDeniedHandler {
4344
@Nullable
4445
Object handle(MethodInvocation methodInvocation, AuthorizationResult authorizationResult);
4546

47+
/**
48+
* Handle denied method invocations, implementations might either throw an
49+
* {@link org.springframework.security.access.AccessDeniedException} or a replacement
50+
* result instead of invoking the method, e.g. a masked value.
51+
* @param methodInvocation the {@link MethodInvocation} related to the authorization
52+
* denied
53+
* @param authorizationDenied the authorization denied exception
54+
* @return a replacement result for the denied method invocation, or null, or a
55+
* {@link reactor.core.publisher.Mono} for reactive applications
56+
*/
57+
default Object handle(MethodInvocation methodInvocation, AuthorizationDeniedException authorizationDenied) {
58+
return handle(methodInvocation, authorizationDenied.getAuthorizationResult());
59+
}
60+
4661
}

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.springframework.security.authorization.method;
1818

1919
import org.springframework.lang.Nullable;
20+
import org.springframework.security.authorization.AuthorizationDeniedException;
2021
import org.springframework.security.authorization.AuthorizationResult;
2122

2223
/**
@@ -43,4 +44,21 @@ public interface MethodAuthorizationDeniedPostProcessor {
4344
@Nullable
4445
Object postProcessResult(MethodInvocationResult methodInvocationResult, AuthorizationResult authorizationResult);
4546

47+
/**
48+
* Post-process the denied result produced by a method invocation, implementations
49+
* might either throw an
50+
* {@link org.springframework.security.access.AccessDeniedException} or return a
51+
* replacement result instead of the denied result, e.g. a masked value.
52+
* @param methodInvocationResult the object containing the method invocation and the
53+
* result produced
54+
* @param authorizationDenied the {@link AuthorizationDeniedException} containing the
55+
* authorization denied details
56+
* @return a replacement result for the denied result, or null, or a
57+
* {@link reactor.core.publisher.Mono} for reactive applications
58+
*/
59+
default Object postProcessResult(MethodInvocationResult methodInvocationResult,
60+
AuthorizationDeniedException authorizationDenied) {
61+
return postProcessResult(methodInvocationResult, authorizationDenied.getAuthorizationResult());
62+
}
63+
4664
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,9 @@ public Object handle(MethodInvocation methodInvocation, AuthorizationResult resu
3535
throw new AuthorizationDeniedException("Access Denied", result);
3636
}
3737

38+
@Override
39+
public Object handle(MethodInvocation methodInvocation, AuthorizationDeniedException authorizationDenied) {
40+
throw authorizationDenied;
41+
}
42+
3843
}

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,10 @@ public Object postProcessResult(MethodInvocationResult methodInvocationResult, A
3333
throw new AuthorizationDeniedException("Access Denied", result);
3434
}
3535

36+
@Override
37+
public Object postProcessResult(MethodInvocationResult methodInvocationResult,
38+
AuthorizationDeniedException authorizationDenied) {
39+
throw authorizationDenied;
40+
}
41+
3642
}

core/src/test/java/org/springframework/security/authorization/method/AuthorizationManagerAfterMethodInterceptorTests.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,10 @@
2626
import org.springframework.security.authentication.TestingAuthenticationToken;
2727
import org.springframework.security.authorization.AuthenticatedAuthorizationManager;
2828
import org.springframework.security.authorization.AuthorizationDecision;
29+
import org.springframework.security.authorization.AuthorizationDeniedException;
2930
import org.springframework.security.authorization.AuthorizationEventPublisher;
3031
import org.springframework.security.authorization.AuthorizationManager;
32+
import org.springframework.security.authorization.AuthorizationResult;
3133
import org.springframework.security.core.Authentication;
3234
import org.springframework.security.core.authority.AuthorityUtils;
3335
import org.springframework.security.core.context.SecurityContext;
@@ -36,6 +38,7 @@
3638
import org.springframework.security.core.context.SecurityContextImpl;
3739

3840
import static org.assertj.core.api.Assertions.assertThat;
41+
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
3942
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
4043
import static org.mockito.ArgumentMatchers.any;
4144
import static org.mockito.BDDMockito.given;
@@ -139,4 +142,24 @@ public void invokeWhenAuthorizationEventPublisherThenUses() throws Throwable {
139142
any(AuthorizationDecision.class));
140143
}
141144

145+
@Test
146+
public void invokeWhenCustomAuthorizationDeniedExceptionThenThrows() throws Throwable {
147+
MethodInvocation mi = mock(MethodInvocation.class);
148+
given(mi.proceed()).willReturn("ok");
149+
AuthorizationManager<MethodInvocationResult> manager = mock(AuthorizationManager.class);
150+
given(manager.check(any(), any()))
151+
.willThrow(new MyAuthzDeniedException("denied", new AuthorizationDecision(false)));
152+
AuthorizationManagerAfterMethodInterceptor advice = new AuthorizationManagerAfterMethodInterceptor(
153+
Pointcut.TRUE, manager);
154+
assertThatExceptionOfType(MyAuthzDeniedException.class).isThrownBy(() -> advice.invoke(mi));
155+
}
156+
157+
static class MyAuthzDeniedException extends AuthorizationDeniedException {
158+
159+
MyAuthzDeniedException(String msg, AuthorizationResult authorizationResult) {
160+
super(msg, authorizationResult);
161+
}
162+
163+
}
164+
142165
}

0 commit comments

Comments
 (0)