Skip to content

Handle SpEL AuthorizationDeniedExceptions #14882

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationDeniedException;
import org.springframework.security.authorization.AuthorizationEventPublisher;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.core.Authentication;
Expand Down Expand Up @@ -172,7 +173,13 @@ public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy strat
private Object attemptAuthorization(MethodInvocation mi, Object result) {
this.logger.debug(LogMessage.of(() -> "Authorizing method invocation " + mi));
MethodInvocationResult object = new MethodInvocationResult(mi, result);
AuthorizationDecision decision = this.authorizationManager.check(this::getAuthentication, object);
AuthorizationDecision decision;
try {
decision = this.authorizationManager.check(this::getAuthentication, object);
}
catch (AuthorizationDeniedException denied) {
return postProcess(object, denied);
}
this.eventPublisher.publishAuthorizationEvent(this::getAuthentication, object, decision);
if (decision != null && !decision.isGranted()) {
this.logger.debug(LogMessage.of(() -> "Failed to authorize " + mi + " with authorization manager "
Expand All @@ -183,6 +190,13 @@ private Object attemptAuthorization(MethodInvocation mi, Object result) {
return result;
}

private Object postProcess(MethodInvocationResult mi, AuthorizationDeniedException denied) {
if (this.authorizationManager instanceof MethodAuthorizationDeniedPostProcessor postProcessableDecision) {
return postProcessableDecision.postProcessResult(mi, denied);
}
return this.defaultPostProcessor.postProcessResult(mi, denied);
}

private Object postProcess(MethodInvocationResult mi, AuthorizationDecision decision) {
if (this.authorizationManager instanceof MethodAuthorizationDeniedPostProcessor postProcessableDecision) {
return postProcessableDecision.postProcessResult(mi, decision);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationDeniedException;
import org.springframework.security.authorization.ReactiveAuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.util.Assert;
Expand Down Expand Up @@ -151,7 +152,32 @@ private Mono<Object> postAuthorize(Mono<Authentication> authentication, MethodIn
MethodInvocationResult invocationResult = new MethodInvocationResult(mi, result);
return this.authorizationManager.check(authentication, invocationResult)
.switchIfEmpty(Mono.just(new AuthorizationDecision(false)))
.flatMap((decision) -> postProcess(decision, invocationResult));
.materialize()
.flatMap((signal) -> {
if (!signal.hasError()) {
AuthorizationDecision decision = signal.get();
return postProcess(decision, invocationResult);
}
if (signal.getThrowable() instanceof AuthorizationDeniedException denied) {
return postProcess(denied, invocationResult);
}
return Mono.error(signal.getThrowable());
});
}

private Mono<Object> postProcess(AuthorizationDeniedException denied,
MethodInvocationResult methodInvocationResult) {
return Mono.fromSupplier(() -> {
if (this.authorizationManager instanceof MethodAuthorizationDeniedPostProcessor postProcessableDecision) {
return postProcessableDecision.postProcessResult(methodInvocationResult, denied);
}
return this.defaultPostProcessor.postProcessResult(methodInvocationResult, denied);
}).flatMap((processedResult) -> {
if (Mono.class.isAssignableFrom(processedResult.getClass())) {
return (Mono<?>) processedResult;
}
return Mono.justOrEmpty(processedResult);
});
}

private Mono<Object> postProcess(AuthorizationDecision decision, MethodInvocationResult methodInvocationResult) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,10 @@
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationDeniedException;
import org.springframework.security.authorization.AuthorizationEventPublisher;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.authorization.AuthorizationResult;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextHolderStrategy;
Expand Down Expand Up @@ -245,7 +247,13 @@ public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy secur

private Object attemptAuthorization(MethodInvocation mi) throws Throwable {
this.logger.debug(LogMessage.of(() -> "Authorizing method invocation " + mi));
AuthorizationDecision decision = this.authorizationManager.check(this::getAuthentication, mi);
AuthorizationDecision decision;
try {
decision = this.authorizationManager.check(this::getAuthentication, mi);
}
catch (AuthorizationDeniedException denied) {
return handle(mi, denied);
}
this.eventPublisher.publishAuthorizationEvent(this::getAuthentication, mi, decision);
if (decision != null && !decision.isGranted()) {
this.logger.debug(LogMessage.of(() -> "Failed to authorize " + mi + " with authorization manager "
Expand All @@ -256,7 +264,14 @@ private Object attemptAuthorization(MethodInvocation mi) throws Throwable {
return mi.proceed();
}

private Object handle(MethodInvocation mi, AuthorizationDecision decision) {
private Object handle(MethodInvocation mi, AuthorizationDeniedException denied) {
if (this.authorizationManager instanceof MethodAuthorizationDeniedHandler handler) {
return handler.handle(mi, denied);
}
return this.defaultHandler.handle(mi, denied);
}

private Object handle(MethodInvocation mi, AuthorizationResult decision) {
if (this.authorizationManager instanceof MethodAuthorizationDeniedHandler handler) {
return handler.handle(mi, decision);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationDeniedException;
import org.springframework.security.authorization.ReactiveAuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.util.Assert;
Expand Down Expand Up @@ -140,26 +141,56 @@ private Flux<Object> preAuthorized(MethodInvocation mi, Flux<Object> mapping) {
Mono<Authentication> authentication = ReactiveAuthenticationUtils.getAuthentication();
return this.authorizationManager.check(authentication, mi)
.switchIfEmpty(Mono.just(new AuthorizationDecision(false)))
.flatMapMany((decision) -> {
if (decision.isGranted()) {
return mapping;
.materialize()
.flatMapMany((signal) -> {
if (!signal.hasError()) {
AuthorizationDecision decision = signal.get();
if (decision.isGranted()) {
return mapping;
}
return postProcess(decision, mi);
}
return postProcess(decision, mi);
if (signal.getThrowable() instanceof AuthorizationDeniedException denied) {
return postProcess(denied, mi);
}
return Mono.error(signal.getThrowable());
});
}

private Mono<Object> preAuthorized(MethodInvocation mi, Mono<Object> mapping) {
Mono<Authentication> authentication = ReactiveAuthenticationUtils.getAuthentication();
return this.authorizationManager.check(authentication, mi)
.switchIfEmpty(Mono.just(new AuthorizationDecision(false)))
.flatMap((decision) -> {
if (decision.isGranted()) {
return mapping;
.materialize()
.flatMap((signal) -> {
if (!signal.hasError()) {
AuthorizationDecision decision = signal.get();
if (decision.isGranted()) {
return mapping;
}
return postProcess(decision, mi);
}
if (signal.getThrowable() instanceof AuthorizationDeniedException denied) {
return postProcess(denied, mi);
}
return postProcess(decision, mi);
return Mono.error(signal.getThrowable());
});
}

private Mono<Object> postProcess(AuthorizationDeniedException denied, MethodInvocation mi) {
return Mono.fromSupplier(() -> {
if (this.authorizationManager instanceof MethodAuthorizationDeniedHandler handler) {
return handler.handle(mi, denied);
}
return this.defaultHandler.handle(mi, denied);
}).flatMap((processedResult) -> {
if (Mono.class.isAssignableFrom(processedResult.getClass())) {
return (Mono<?>) processedResult;
}
return Mono.justOrEmpty(processedResult);
});
}

private Mono<Object> postProcess(AuthorizationDecision decision, MethodInvocation mi) {
return Mono.fromSupplier(() -> {
if (this.authorizationManager instanceof MethodAuthorizationDeniedHandler handler) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import org.aopalliance.intercept.MethodInvocation;

import org.springframework.lang.Nullable;
import org.springframework.security.authorization.AuthorizationDeniedException;
import org.springframework.security.authorization.AuthorizationResult;

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

/**
* Handle denied method invocations, implementations might either throw an
* {@link org.springframework.security.access.AccessDeniedException} or a replacement
* result instead of invoking the method, e.g. a masked value.
* @param methodInvocation the {@link MethodInvocation} related to the authorization
* denied
* @param authorizationDenied the authorization denied exception
* @return a replacement result for the denied method invocation, or null, or a
* {@link reactor.core.publisher.Mono} for reactive applications
*/
default Object handle(MethodInvocation methodInvocation, AuthorizationDeniedException authorizationDenied) {
return handle(methodInvocation, authorizationDenied.getAuthorizationResult());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package org.springframework.security.authorization.method;

import org.springframework.lang.Nullable;
import org.springframework.security.authorization.AuthorizationDeniedException;
import org.springframework.security.authorization.AuthorizationResult;

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

/**
* Post-process the denied result produced by a method invocation, implementations
* might either throw an
* {@link org.springframework.security.access.AccessDeniedException} or return a
* replacement result instead of the denied result, e.g. a masked value.
* @param methodInvocationResult the object containing the method invocation and the
* result produced
* @param authorizationDenied the {@link AuthorizationDeniedException} containing the
* authorization denied details
* @return a replacement result for the denied result, or null, or a
* {@link reactor.core.publisher.Mono} for reactive applications
*/
default Object postProcessResult(MethodInvocationResult methodInvocationResult,
AuthorizationDeniedException authorizationDenied) {
return postProcessResult(methodInvocationResult, authorizationDenied.getAuthorizationResult());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,9 @@ public Object handle(MethodInvocation methodInvocation, AuthorizationResult resu
throw new AuthorizationDeniedException("Access Denied", result);
}

@Override
public Object handle(MethodInvocation methodInvocation, AuthorizationDeniedException authorizationDenied) {
throw authorizationDenied;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,10 @@ public Object postProcessResult(MethodInvocationResult methodInvocationResult, A
throw new AuthorizationDeniedException("Access Denied", result);
}

@Override
public Object postProcessResult(MethodInvocationResult methodInvocationResult,
AuthorizationDeniedException authorizationDenied) {
throw authorizationDenied;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.authorization.AuthenticatedAuthorizationManager;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationDeniedException;
import org.springframework.security.authorization.AuthorizationEventPublisher;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.authorization.AuthorizationResult;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContext;
Expand All @@ -36,6 +38,7 @@
import org.springframework.security.core.context.SecurityContextImpl;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
Expand Down Expand Up @@ -139,4 +142,24 @@ public void invokeWhenAuthorizationEventPublisherThenUses() throws Throwable {
any(AuthorizationDecision.class));
}

@Test
public void invokeWhenCustomAuthorizationDeniedExceptionThenThrows() throws Throwable {
MethodInvocation mi = mock(MethodInvocation.class);
given(mi.proceed()).willReturn("ok");
AuthorizationManager<MethodInvocationResult> manager = mock(AuthorizationManager.class);
given(manager.check(any(), any()))
.willThrow(new MyAuthzDeniedException("denied", new AuthorizationDecision(false)));
AuthorizationManagerAfterMethodInterceptor advice = new AuthorizationManagerAfterMethodInterceptor(
Pointcut.TRUE, manager);
assertThatExceptionOfType(MyAuthzDeniedException.class).isThrownBy(() -> advice.invoke(mi));
}

static class MyAuthzDeniedException extends AuthorizationDeniedException {

MyAuthzDeniedException(String msg, AuthorizationResult authorizationResult) {
super(msg, authorizationResult);
}

}

}
Loading
Loading