Skip to content

Commit ec59107

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

13 files changed

+211
-29
lines changed

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

Lines changed: 18 additions & 5 deletions
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;
@@ -173,16 +174,28 @@ private Object attemptAuthorization(MethodInvocation mi, Object result) {
173174
this.logger.debug(LogMessage.of(() -> "Authorizing method invocation " + mi));
174175
MethodInvocationResult object = new MethodInvocationResult(mi, result);
175176
AuthorizationDecision decision = this.authorizationManager.check(this::getAuthentication, object);
176-
this.eventPublisher.publishAuthorizationEvent(this::getAuthentication, object, decision);
177-
if (decision != null && !decision.isGranted()) {
178-
this.logger.debug(LogMessage.of(() -> "Failed to authorize " + mi + " with authorization manager "
179-
+ this.authorizationManager + " and decision " + decision));
180-
return postProcess(object, decision);
177+
try {
178+
this.eventPublisher.publishAuthorizationEvent(this::getAuthentication, object, decision);
179+
if (decision != null && !decision.isGranted()) {
180+
this.logger.debug(LogMessage.of(() -> "Failed to authorize " + mi + " with authorization manager "
181+
+ this.authorizationManager + " and decision " + decision));
182+
return postProcess(object, decision);
183+
}
184+
}
185+
catch (AuthorizationDeniedException denied) {
186+
return postProcess(object, denied);
181187
}
182188
this.logger.debug(LogMessage.of(() -> "Authorized method invocation " + mi));
183189
return result;
184190
}
185191

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

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

Lines changed: 18 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,23 @@ 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+
.flatMap((decision) -> postProcess(decision, invocationResult))
156+
.onErrorResume(AuthorizationDeniedException.class, (denied) -> postProcess(denied, invocationResult));
157+
}
158+
159+
private Mono<Object> postProcess(AuthorizationDeniedException denied,
160+
MethodInvocationResult methodInvocationResult) {
161+
return Mono.fromSupplier(() -> {
162+
if (this.authorizationManager instanceof MethodAuthorizationDeniedPostProcessor postProcessableDecision) {
163+
return postProcessableDecision.postProcessResult(methodInvocationResult, denied);
164+
}
165+
return this.defaultPostProcessor.postProcessResult(methodInvocationResult, denied);
166+
}).flatMap((processedResult) -> {
167+
if (Mono.class.isAssignableFrom(processedResult.getClass())) {
168+
return (Mono<?>) processedResult;
169+
}
170+
return Mono.justOrEmpty(processedResult);
171+
});
155172
}
156173

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

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

Lines changed: 21 additions & 7 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,18 +247,30 @@ 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);
249-
this.eventPublisher.publishAuthorizationEvent(this::getAuthentication, mi, decision);
250-
if (decision != null && !decision.isGranted()) {
251-
this.logger.debug(LogMessage.of(() -> "Failed to authorize " + mi + " with authorization manager "
252-
+ this.authorizationManager + " and decision " + decision));
253-
return handle(mi, decision);
250+
try {
251+
AuthorizationDecision decision = this.authorizationManager.check(this::getAuthentication, mi);
252+
this.eventPublisher.publishAuthorizationEvent(this::getAuthentication, mi, decision);
253+
if (decision != null && !decision.isGranted()) {
254+
this.logger.debug(LogMessage.of(() -> "Failed to authorize " + mi + " with authorization manager "
255+
+ this.authorizationManager + " and decision " + decision));
256+
return handle(mi, decision);
257+
}
258+
}
259+
catch (AuthorizationDeniedException denied) {
260+
return handle(mi, denied);
254261
}
255262
this.logger.debug(LogMessage.of(() -> "Authorized method invocation " + mi));
256263
return mi.proceed();
257264
}
258265

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

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

Lines changed: 19 additions & 2 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;
@@ -145,7 +146,8 @@ private Flux<Object> preAuthorized(MethodInvocation mi, Flux<Object> mapping) {
145146
return mapping;
146147
}
147148
return postProcess(decision, mi);
148-
});
149+
})
150+
.onErrorResume(AuthorizationDeniedException.class, (denied) -> postProcess(denied, mi));
149151
}
150152

151153
private Mono<Object> preAuthorized(MethodInvocation mi, Mono<Object> mapping) {
@@ -157,7 +159,22 @@ private Mono<Object> preAuthorized(MethodInvocation mi, Mono<Object> mapping) {
157159
return mapping;
158160
}
159161
return postProcess(decision, mi);
160-
});
162+
})
163+
.onErrorResume(AuthorizationDeniedException.class, (denied) -> postProcess(denied, mi));
164+
}
165+
166+
private Mono<Object> postProcess(AuthorizationDeniedException denied, MethodInvocation mi) {
167+
return Mono.fromSupplier(() -> {
168+
if (this.authorizationManager instanceof MethodAuthorizationDeniedHandler handler) {
169+
return handler.handle(mi, denied);
170+
}
171+
return this.defaultHandler.handle(mi, denied);
172+
}).flatMap((processedResult) -> {
173+
if (Mono.class.isAssignableFrom(processedResult.getClass())) {
174+
return (Mono<?>) processedResult;
175+
}
176+
return Mono.justOrEmpty(processedResult);
177+
});
161178
}
162179

163180
private Mono<Object> postProcess(AuthorizationDecision decision, MethodInvocation mi) {

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

Lines changed: 5 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,8 @@ public interface MethodAuthorizationDeniedHandler {
4344
@Nullable
4445
Object handle(MethodInvocation methodInvocation, AuthorizationResult authorizationResult);
4546

47+
default Object handle(MethodInvocation methodInvocation, AuthorizationDeniedException authorizationDenied) {
48+
return handle(methodInvocation, authorizationDenied.getAuthorizationResult());
49+
}
50+
4651
}

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

Lines changed: 6 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,9 @@ public interface MethodAuthorizationDeniedPostProcessor {
4344
@Nullable
4445
Object postProcessResult(MethodInvocationResult methodInvocationResult, AuthorizationResult authorizationResult);
4546

47+
default Object postProcessResult(MethodInvocationResult methodInvocationResult,
48+
AuthorizationDeniedException authorizationDenied) {
49+
return postProcessResult(methodInvocationResult, authorizationDenied.getAuthorizationResult());
50+
}
51+
4652
}

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
}

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

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.springframework.security.access.intercept.method.MockMethodInvocation;
2929
import org.springframework.security.authorization.AuthorizationDecision;
3030
import org.springframework.security.authorization.AuthorizationDeniedException;
31+
import org.springframework.security.authorization.AuthorizationResult;
3132
import org.springframework.security.authorization.ReactiveAuthorizationManager;
3233

3334
import static org.assertj.core.api.Assertions.assertThat;
@@ -126,7 +127,8 @@ public void invokeFluxWhenAllValuesDeniedAndPostProcessorThenPostProcessorApplie
126127
given(mockMethodInvocation.proceed()).willReturn(Flux.just("john", "bob"));
127128
HandlingReactiveAuthorizationManager mockReactiveAuthorizationManager = mock(
128129
HandlingReactiveAuthorizationManager.class);
129-
given(mockReactiveAuthorizationManager.postProcessResult(any(), any())).willAnswer(this::masking);
130+
given(mockReactiveAuthorizationManager.postProcessResult(any(), any(AuthorizationResult.class)))
131+
.willAnswer(this::masking);
130132
given(mockReactiveAuthorizationManager.check(any(), any())).willReturn(Mono.empty());
131133
AuthorizationManagerAfterReactiveMethodInterceptor interceptor = new AuthorizationManagerAfterReactiveMethodInterceptor(
132134
Pointcut.TRUE, mockReactiveAuthorizationManager);
@@ -145,13 +147,14 @@ public void invokeFluxWhenOneValueDeniedAndPostProcessorThenPostProcessorApplied
145147
given(mockMethodInvocation.proceed()).willReturn(Flux.just("john", "bob"));
146148
HandlingReactiveAuthorizationManager mockReactiveAuthorizationManager = mock(
147149
HandlingReactiveAuthorizationManager.class);
148-
given(mockReactiveAuthorizationManager.postProcessResult(any(), any())).willAnswer((invocation) -> {
149-
MethodInvocationResult argument = invocation.getArgument(0);
150-
if (!"john".equals(argument.getResult())) {
151-
return monoMasking(invocation);
152-
}
153-
return Mono.just(argument.getResult());
154-
});
150+
given(mockReactiveAuthorizationManager.postProcessResult(any(), any(AuthorizationResult.class)))
151+
.willAnswer((invocation) -> {
152+
MethodInvocationResult argument = invocation.getArgument(0);
153+
if (!"john".equals(argument.getResult())) {
154+
return monoMasking(invocation);
155+
}
156+
return Mono.just(argument.getResult());
157+
});
155158
given(mockReactiveAuthorizationManager.check(any(), any())).willReturn(Mono.empty());
156159
AuthorizationManagerAfterReactiveMethodInterceptor interceptor = new AuthorizationManagerAfterReactiveMethodInterceptor(
157160
Pointcut.TRUE, mockReactiveAuthorizationManager);
@@ -170,7 +173,8 @@ public void invokeMonoWhenPostProcessableDecisionThenPostProcess() throws Throwa
170173
given(mockMethodInvocation.proceed()).willReturn(Mono.just("john"));
171174
HandlingReactiveAuthorizationManager mockReactiveAuthorizationManager = mock(
172175
HandlingReactiveAuthorizationManager.class);
173-
given(mockReactiveAuthorizationManager.postProcessResult(any(), any())).willAnswer(this::masking);
176+
given(mockReactiveAuthorizationManager.postProcessResult(any(), any(AuthorizationResult.class)))
177+
.willAnswer(this::masking);
174178
given(mockReactiveAuthorizationManager.check(any(), any())).willReturn(Mono.empty());
175179
AuthorizationManagerAfterReactiveMethodInterceptor interceptor = new AuthorizationManagerAfterReactiveMethodInterceptor(
176180
Pointcut.TRUE, mockReactiveAuthorizationManager);
@@ -188,7 +192,8 @@ public void invokeMonoWhenPostProcessableDecisionAndPostProcessResultIsMonoThenP
188192
given(mockMethodInvocation.proceed()).willReturn(Mono.just("john"));
189193
HandlingReactiveAuthorizationManager mockReactiveAuthorizationManager = mock(
190194
HandlingReactiveAuthorizationManager.class);
191-
given(mockReactiveAuthorizationManager.postProcessResult(any(), any())).willAnswer(this::monoMasking);
195+
given(mockReactiveAuthorizationManager.postProcessResult(any(), any(AuthorizationResult.class)))
196+
.willAnswer(this::monoMasking);
192197
given(mockReactiveAuthorizationManager.check(any(), any())).willReturn(Mono.empty());
193198
AuthorizationManagerAfterReactiveMethodInterceptor interceptor = new AuthorizationManagerAfterReactiveMethodInterceptor(
194199
Pointcut.TRUE, mockReactiveAuthorizationManager);
@@ -206,7 +211,8 @@ public void invokeMonoWhenPostProcessableDecisionAndPostProcessResultIsNullThenP
206211
given(mockMethodInvocation.proceed()).willReturn(Mono.just("john"));
207212
HandlingReactiveAuthorizationManager mockReactiveAuthorizationManager = mock(
208213
HandlingReactiveAuthorizationManager.class);
209-
given(mockReactiveAuthorizationManager.postProcessResult(any(), any())).willReturn(null);
214+
given(mockReactiveAuthorizationManager.postProcessResult(any(), any(AuthorizationResult.class)))
215+
.willReturn(null);
210216
given(mockReactiveAuthorizationManager.check(any(), any())).willReturn(Mono.empty());
211217
AuthorizationManagerAfterReactiveMethodInterceptor interceptor = new AuthorizationManagerAfterReactiveMethodInterceptor(
212218
Pointcut.TRUE, mockReactiveAuthorizationManager);
@@ -235,6 +241,20 @@ public void invokeMonoWhenEmptyDecisionThenUseDefaultPostProcessor() throws Thro
235241
verify(mockReactiveAuthorizationManager).check(any(), any());
236242
}
237243

244+
@Test
245+
public void invokeWhenCustomAuthorizationDeniedExceptionThenThrows() throws Throwable {
246+
MethodInvocation mockMethodInvocation = spy(
247+
new MockMethodInvocation(new Sample(), Sample.class.getDeclaredMethod("mono")));
248+
given(mockMethodInvocation.proceed()).willReturn(Mono.just("ok"));
249+
ReactiveAuthorizationManager<MethodInvocationResult> manager = mock(ReactiveAuthorizationManager.class);
250+
given(manager.check(any(), any()))
251+
.willReturn(Mono.error(new MyAuthzDeniedException("denied", new AuthorizationDecision(false))));
252+
AuthorizationManagerAfterReactiveMethodInterceptor advice = new AuthorizationManagerAfterReactiveMethodInterceptor(
253+
Pointcut.TRUE, manager);
254+
assertThatExceptionOfType(MyAuthzDeniedException.class)
255+
.isThrownBy(() -> ((Mono<?>) advice.invoke(mockMethodInvocation)).block());
256+
}
257+
238258
private Object masking(InvocationOnMock invocation) {
239259
MethodInvocationResult result = invocation.getArgument(0);
240260
return result.getResult() + "-masked";
@@ -262,4 +282,12 @@ Flux<String> flux() {
262282

263283
}
264284

285+
static class MyAuthzDeniedException extends AuthorizationDeniedException {
286+
287+
MyAuthzDeniedException(String msg, AuthorizationResult authorizationResult) {
288+
super(msg, authorizationResult);
289+
}
290+
291+
}
292+
265293
}

0 commit comments

Comments
 (0)