Add Authorization Denied Handlers for Method Security

Closes gh-14601
This commit is contained in:
Marcus Hert Da Coregio 2024-03-08 10:46:27 -03:00
parent 19d66c0b8a
commit d85857f905
36 changed files with 2461 additions and 61 deletions

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@ -98,6 +98,7 @@ final class PrePostMethodSecurityConfiguration implements ImportAware, AopInfras
ObjectProvider<ObservationRegistry> registryProvider, ObjectProvider<RoleHierarchy> roleHierarchyProvider,
PrePostMethodSecurityConfiguration configuration, ApplicationContext context) {
PreAuthorizeAuthorizationManager manager = new PreAuthorizeAuthorizationManager();
manager.setApplicationContext(context);
AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor
.preAuthorize(manager(manager, registryProvider));
preAuthorize.setOrder(preAuthorize.getOrder() + configuration.interceptorOrderOffset);
@ -121,6 +122,7 @@ final class PrePostMethodSecurityConfiguration implements ImportAware, AopInfras
ObjectProvider<ObservationRegistry> registryProvider, ObjectProvider<RoleHierarchy> roleHierarchyProvider,
PrePostMethodSecurityConfiguration configuration, ApplicationContext context) {
PostAuthorizeAuthorizationManager manager = new PostAuthorizeAuthorizationManager();
manager.setApplicationContext(context);
AuthorizationManagerAfterMethodInterceptor postAuthorize = AuthorizationManagerAfterMethodInterceptor
.postAuthorize(manager(manager, registryProvider));
postAuthorize.setOrder(postAuthorize.getOrder() + configuration.interceptorOrderOffset);

View File

@ -31,6 +31,7 @@ import org.springframework.aop.framework.AopInfrastructureBean;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Role;
@ -74,9 +75,10 @@ final class ReactiveAuthorizationManagerMethodSecurityConfiguration implements A
static MethodInterceptor preAuthorizeAuthorizationMethodInterceptor(
MethodSecurityExpressionHandler expressionHandler,
ObjectProvider<PrePostTemplateDefaults> defaultsObjectProvider,
ObjectProvider<ObservationRegistry> registryProvider) {
ObjectProvider<ObservationRegistry> registryProvider, ApplicationContext context) {
PreAuthorizeReactiveAuthorizationManager manager = new PreAuthorizeReactiveAuthorizationManager(
expressionHandler);
manager.setApplicationContext(context);
ReactiveAuthorizationManager<MethodInvocation> authorizationManager = manager(manager, registryProvider);
AuthorizationAdvisor interceptor = AuthorizationManagerBeforeReactiveMethodInterceptor
.preAuthorize(authorizationManager);
@ -99,9 +101,10 @@ final class ReactiveAuthorizationManagerMethodSecurityConfiguration implements A
static MethodInterceptor postAuthorizeAuthorizationMethodInterceptor(
MethodSecurityExpressionHandler expressionHandler,
ObjectProvider<PrePostTemplateDefaults> defaultsObjectProvider,
ObjectProvider<ObservationRegistry> registryProvider) {
ObjectProvider<ObservationRegistry> registryProvider, ApplicationContext context) {
PostAuthorizeReactiveAuthorizationManager manager = new PostAuthorizeReactiveAuthorizationManager(
expressionHandler);
manager.setApplicationContext(context);
ReactiveAuthorizationManager<MethodInvocationResult> authorizationManager = manager(manager, registryProvider);
AuthorizationAdvisor interceptor = AuthorizationManagerAfterReactiveMethodInterceptor
.postAuthorize(authorizationManager);

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -16,23 +16,42 @@
package org.springframework.security.config.annotation.method.configuration;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.List;
import jakarta.annotation.security.DenyAll;
import jakarta.annotation.security.PermitAll;
import jakarta.annotation.security.RolesAllowed;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.context.ApplicationContext;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.security.access.annotation.Secured;
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.security.access.prepost.PostFilter;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.access.prepost.PreFilter;
import org.springframework.security.authorization.AuthorizationResult;
import org.springframework.security.authorization.method.AuthorizeReturnObject;
import org.springframework.security.authorization.method.MethodAuthorizationDeniedHandler;
import org.springframework.security.authorization.method.MethodAuthorizationDeniedPostProcessor;
import org.springframework.security.authorization.method.MethodInvocationResult;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.parameters.P;
import org.springframework.util.StringUtils;
/**
* @author Rob Winch
*/
@MethodSecurityService.Mask("classmask")
public interface MethodSecurityService {
@PreAuthorize("denyAll")
@ -108,4 +127,196 @@ public interface MethodSecurityService {
@RequireAdminRole
void repeatedAnnotations();
@PreAuthorize(value = "hasRole('ADMIN')", handlerClass = StarMaskingHandler.class)
String preAuthorizeGetCardNumberIfAdmin(String cardNumber);
@PreAuthorize(value = "hasRole('ADMIN')", handlerClass = StartMaskingHandlerChild.class)
String preAuthorizeWithHandlerChildGetCardNumberIfAdmin(String cardNumber);
@PreAuthorize(value = "hasRole('ADMIN')", handlerClass = StarMaskingHandler.class)
String preAuthorizeThrowAccessDeniedManually();
@PostAuthorize(value = "hasRole('ADMIN')", postProcessorClass = CardNumberMaskingPostProcessor.class)
String postAuthorizeGetCardNumberIfAdmin(String cardNumber);
@PostAuthorize(value = "hasRole('ADMIN')", postProcessorClass = PostMaskingPostProcessor.class)
String postAuthorizeThrowAccessDeniedManually();
@PreAuthorize(value = "denyAll()", handlerClass = MaskAnnotationHandler.class)
@Mask("methodmask")
String preAuthorizeDeniedMethodWithMaskAnnotation();
@PreAuthorize(value = "denyAll()", handlerClass = MaskAnnotationHandler.class)
String preAuthorizeDeniedMethodWithNoMaskAnnotation();
@NullDenied(role = "ADMIN")
String postAuthorizeDeniedWithNullDenied();
@PostAuthorize(value = "denyAll()", postProcessorClass = MaskAnnotationPostProcessor.class)
@Mask("methodmask")
String postAuthorizeDeniedMethodWithMaskAnnotation();
@PostAuthorize(value = "denyAll()", postProcessorClass = MaskAnnotationPostProcessor.class)
String postAuthorizeDeniedMethodWithNoMaskAnnotation();
@PreAuthorize(value = "hasRole('ADMIN')", handlerClass = MaskAnnotationHandler.class)
@Mask(expression = "@myMasker.getMask()")
String preAuthorizeWithMaskAnnotationUsingBean();
@PostAuthorize(value = "hasRole('ADMIN')", postProcessorClass = MaskAnnotationPostProcessor.class)
@Mask(expression = "@myMasker.getMask(returnObject)")
String postAuthorizeWithMaskAnnotationUsingBean();
@AuthorizeReturnObject
UserRecordWithEmailProtected getUserRecordWithEmailProtected();
@PreAuthorize(value = "hasRole('ADMIN')", handlerClass = UserFallbackDeniedHandler.class)
UserRecordWithEmailProtected getUserWithFallbackWhenUnauthorized();
class StarMaskingHandler implements MethodAuthorizationDeniedHandler {
@Override
public Object handle(MethodInvocation methodInvocation, AuthorizationResult result) {
return "***";
}
}
class StartMaskingHandlerChild extends StarMaskingHandler {
@Override
public Object handle(MethodInvocation methodInvocation, AuthorizationResult result) {
return super.handle(methodInvocation, result) + "-child";
}
}
class MaskAnnotationHandler implements MethodAuthorizationDeniedHandler {
MaskValueResolver maskValueResolver;
MaskAnnotationHandler(ApplicationContext context) {
this.maskValueResolver = new MaskValueResolver(context);
}
@Override
public Object handle(MethodInvocation methodInvocation, AuthorizationResult result) {
Mask mask = AnnotationUtils.getAnnotation(methodInvocation.getMethod(), Mask.class);
if (mask == null) {
mask = AnnotationUtils.getAnnotation(methodInvocation.getMethod().getDeclaringClass(), Mask.class);
}
return this.maskValueResolver.resolveValue(mask, methodInvocation, null);
}
}
class MaskAnnotationPostProcessor implements MethodAuthorizationDeniedPostProcessor {
MaskValueResolver maskValueResolver;
MaskAnnotationPostProcessor(ApplicationContext context) {
this.maskValueResolver = new MaskValueResolver(context);
}
@Override
public Object postProcessResult(MethodInvocationResult methodInvocationResult,
AuthorizationResult authorizationResult) {
MethodInvocation mi = methodInvocationResult.getMethodInvocation();
Mask mask = AnnotationUtils.getAnnotation(mi.getMethod(), Mask.class);
if (mask == null) {
mask = AnnotationUtils.getAnnotation(mi.getMethod().getDeclaringClass(), Mask.class);
}
return this.maskValueResolver.resolveValue(mask, mi, methodInvocationResult.getResult());
}
}
class MaskValueResolver {
DefaultMethodSecurityExpressionHandler expressionHandler;
MaskValueResolver(ApplicationContext context) {
this.expressionHandler = new DefaultMethodSecurityExpressionHandler();
this.expressionHandler.setApplicationContext(context);
}
String resolveValue(Mask mask, MethodInvocation mi, Object returnObject) {
if (StringUtils.hasText(mask.value())) {
return mask.value();
}
Expression expression = this.expressionHandler.getExpressionParser().parseExpression(mask.expression());
EvaluationContext evaluationContext = this.expressionHandler
.createEvaluationContext(() -> SecurityContextHolder.getContext().getAuthentication(), mi);
if (returnObject != null) {
this.expressionHandler.setReturnObject(returnObject, evaluationContext);
}
return expression.getValue(evaluationContext, String.class);
}
}
class PostMaskingPostProcessor implements MethodAuthorizationDeniedPostProcessor {
@Override
public Object postProcessResult(MethodInvocationResult contextObject, AuthorizationResult result) {
return "***";
}
}
class CardNumberMaskingPostProcessor implements MethodAuthorizationDeniedPostProcessor {
static String MASK = "****-****-****-";
@Override
public Object postProcessResult(MethodInvocationResult contextObject, AuthorizationResult result) {
String cardNumber = (String) contextObject.getResult();
return MASK + cardNumber.substring(cardNumber.length() - 4);
}
}
class NullPostProcessor implements MethodAuthorizationDeniedPostProcessor {
@Override
public Object postProcessResult(MethodInvocationResult methodInvocationResult,
AuthorizationResult authorizationResult) {
return null;
}
}
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@interface Mask {
String value() default "";
String expression() default "";
}
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@PostAuthorize(value = "hasRole('{value}')", postProcessorClass = NullPostProcessor.class)
@interface NullDenied {
String role();
}
class UserFallbackDeniedHandler implements MethodAuthorizationDeniedHandler {
private static final UserRecordWithEmailProtected FALLBACK = new UserRecordWithEmailProtected("Protected",
"Protected");
@Override
public Object handle(MethodInvocation methodInvocation, AuthorizationResult authorizationResult) {
return FALLBACK;
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -18,6 +18,7 @@ package org.springframework.security.config.annotation.method.configuration;
import java.util.List;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
@ -126,4 +127,74 @@ public class MethodSecurityServiceImpl implements MethodSecurityService {
public void repeatedAnnotations() {
}
@Override
public String postAuthorizeGetCardNumberIfAdmin(String cardNumber) {
return cardNumber;
}
@Override
public String preAuthorizeGetCardNumberIfAdmin(String cardNumber) {
return cardNumber;
}
@Override
public String preAuthorizeWithHandlerChildGetCardNumberIfAdmin(String cardNumber) {
return cardNumber;
}
@Override
public String preAuthorizeThrowAccessDeniedManually() {
throw new AccessDeniedException("Access Denied");
}
@Override
public String postAuthorizeThrowAccessDeniedManually() {
throw new AccessDeniedException("Access Denied");
}
@Override
public String preAuthorizeDeniedMethodWithMaskAnnotation() {
return "ok";
}
@Override
public String preAuthorizeDeniedMethodWithNoMaskAnnotation() {
return "ok";
}
@Override
public String postAuthorizeDeniedWithNullDenied() {
return "ok";
}
@Override
public String postAuthorizeDeniedMethodWithMaskAnnotation() {
return "ok";
}
@Override
public String postAuthorizeDeniedMethodWithNoMaskAnnotation() {
return "ok";
}
@Override
public String preAuthorizeWithMaskAnnotationUsingBean() {
return "ok";
}
@Override
public String postAuthorizeWithMaskAnnotationUsingBean() {
return "ok";
}
@Override
public UserRecordWithEmailProtected getUserRecordWithEmailProtected() {
return new UserRecordWithEmailProtected("username", "useremail@example.com");
}
@Override
public UserRecordWithEmailProtected getUserWithFallbackWhenUnauthorized() {
return new UserRecordWithEmailProtected("username", "useremail@example.com");
}
}

View File

@ -0,0 +1,29 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.annotation.method.configuration;
public class MyMasker {
public String getMask(String value) {
return value + "-masked";
}
public String getMask() {
return "mask";
}
}

View File

@ -743,6 +743,188 @@ public class PrePostMethodSecurityConfigurationTests {
});
}
@Test
@WithMockUser
void getCardNumberWhenPostAuthorizeAndNotAdminThenReturnMasked() {
this.spring
.register(MethodSecurityServiceEnabledConfig.class,
MethodSecurityService.CardNumberMaskingPostProcessor.class)
.autowire();
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
String cardNumber = service.postAuthorizeGetCardNumberIfAdmin("4444-3333-2222-1111");
assertThat(cardNumber).isEqualTo("****-****-****-1111");
}
@Test
@WithMockUser
void getCardNumberWhenPreAuthorizeAndNotAdminThenReturnMasked() {
this.spring.register(MethodSecurityServiceEnabledConfig.class, MethodSecurityService.StarMaskingHandler.class)
.autowire();
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
String cardNumber = service.preAuthorizeGetCardNumberIfAdmin("4444-3333-2222-1111");
assertThat(cardNumber).isEqualTo("***");
}
@Test
@WithMockUser
void getCardNumberWhenPreAuthorizeAndNotAdminAndChildHandlerThenResolveCorrectHandlerAndReturnMasked() {
this.spring
.register(MethodSecurityServiceEnabledConfig.class, MethodSecurityService.StarMaskingHandler.class,
MethodSecurityService.StartMaskingHandlerChild.class)
.autowire();
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
String cardNumber = service.preAuthorizeWithHandlerChildGetCardNumberIfAdmin("4444-3333-2222-1111");
assertThat(cardNumber).isEqualTo("***-child");
}
@Test
@WithMockUser(roles = "ADMIN")
void preAuthorizeWhenHandlerAndAccessDeniedNotThrownFromPreAuthorizeThenNotHandled() {
this.spring.register(MethodSecurityServiceEnabledConfig.class, MethodSecurityService.StarMaskingHandler.class)
.autowire();
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
assertThatExceptionOfType(AccessDeniedException.class)
.isThrownBy(service::preAuthorizeThrowAccessDeniedManually);
}
@Test
@WithMockUser
void preAuthorizeWhenDeniedAndHandlerWithCustomAnnotationThenHandlerCanUseMaskFromOtherAnnotation() {
this.spring
.register(MethodSecurityServiceEnabledConfig.class, MethodSecurityService.MaskAnnotationHandler.class)
.autowire();
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
String result = service.preAuthorizeDeniedMethodWithMaskAnnotation();
assertThat(result).isEqualTo("methodmask");
}
@Test
@WithMockUser
void preAuthorizeWhenDeniedAndHandlerWithCustomAnnotationInClassThenHandlerCanUseMaskFromOtherAnnotation() {
this.spring
.register(MethodSecurityServiceEnabledConfig.class, MethodSecurityService.MaskAnnotationHandler.class)
.autowire();
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
String result = service.preAuthorizeDeniedMethodWithNoMaskAnnotation();
assertThat(result).isEqualTo("classmask");
}
@Test
@WithMockUser(roles = "ADMIN")
void postAuthorizeWhenHandlerAndAccessDeniedNotThrownFromPostAuthorizeThenNotHandled() {
this.spring
.register(MethodSecurityServiceEnabledConfig.class, MethodSecurityService.PostMaskingPostProcessor.class)
.autowire();
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
assertThatExceptionOfType(AccessDeniedException.class)
.isThrownBy(service::postAuthorizeThrowAccessDeniedManually);
}
@Test
@WithMockUser
void postAuthorizeWhenNullDeniedMetaAnnotationThanWorks() {
this.spring.register(MethodSecurityServiceEnabledConfig.class, MethodSecurityService.NullPostProcessor.class)
.autowire();
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
String result = service.postAuthorizeDeniedWithNullDenied();
assertThat(result).isNull();
}
@Test
@WithMockUser
void postAuthorizeWhenDeniedAndHandlerWithCustomAnnotationThenHandlerCanUseMaskFromOtherAnnotation() {
this.spring
.register(MethodSecurityServiceEnabledConfig.class, MethodSecurityService.MaskAnnotationPostProcessor.class)
.autowire();
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
String result = service.postAuthorizeDeniedMethodWithMaskAnnotation();
assertThat(result).isEqualTo("methodmask");
}
@Test
@WithMockUser
void postAuthorizeWhenDeniedAndHandlerWithCustomAnnotationInClassThenHandlerCanUseMaskFromOtherAnnotation() {
this.spring
.register(MethodSecurityServiceEnabledConfig.class, MethodSecurityService.MaskAnnotationPostProcessor.class)
.autowire();
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
String result = service.postAuthorizeDeniedMethodWithNoMaskAnnotation();
assertThat(result).isEqualTo("classmask");
}
@Test
@WithMockUser
void postAuthorizeWhenDeniedAndHandlerWithCustomAnnotationUsingBeanThenHandlerCanUseMaskFromOtherAnnotation() {
this.spring
.register(MethodSecurityServiceEnabledConfig.class, MethodSecurityService.MaskAnnotationPostProcessor.class,
MyMasker.class)
.autowire();
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
String result = service.postAuthorizeWithMaskAnnotationUsingBean();
assertThat(result).isEqualTo("ok-masked");
}
@Test
@WithMockUser(roles = "ADMIN")
void postAuthorizeWhenAllowedAndHandlerWithCustomAnnotationUsingBeanThenInvokeMethodNormally() {
this.spring
.register(MethodSecurityServiceEnabledConfig.class, MethodSecurityService.MaskAnnotationPostProcessor.class,
MyMasker.class)
.autowire();
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
String result = service.postAuthorizeWithMaskAnnotationUsingBean();
assertThat(result).isEqualTo("ok");
}
@Test
@WithMockUser
void preAuthorizeWhenDeniedAndHandlerWithCustomAnnotationUsingBeanThenHandlerCanUseMaskFromOtherAnnotation() {
this.spring
.register(MethodSecurityServiceEnabledConfig.class, MethodSecurityService.MaskAnnotationHandler.class,
MyMasker.class)
.autowire();
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
String result = service.preAuthorizeWithMaskAnnotationUsingBean();
assertThat(result).isEqualTo("mask");
}
@Test
@WithMockUser(roles = "ADMIN")
void preAuthorizeWhenAllowedAndHandlerWithCustomAnnotationUsingBeanThenInvokeMethodNormally() {
this.spring
.register(MethodSecurityServiceEnabledConfig.class, MethodSecurityService.MaskAnnotationHandler.class,
MyMasker.class)
.autowire();
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
String result = service.preAuthorizeWithMaskAnnotationUsingBean();
assertThat(result).isEqualTo("ok");
}
@Test
@WithMockUser
void getUserWhenAuthorizedAndUserEmailIsProtectedAndNotAuthorizedThenReturnEmailMasked() {
this.spring
.register(MethodSecurityServiceEnabledConfig.class,
UserRecordWithEmailProtected.EmailMaskingPostProcessor.class)
.autowire();
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
UserRecordWithEmailProtected user = service.getUserRecordWithEmailProtected();
assertThat(user.email()).isEqualTo("use******@example.com");
assertThat(user.name()).isEqualTo("username");
}
@Test
@WithMockUser
void getUserWhenNotAuthorizedAndHandlerFallbackValueThenReturnFallbackValue() {
this.spring
.register(MethodSecurityServiceEnabledConfig.class, MethodSecurityService.UserFallbackDeniedHandler.class)
.autowire();
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
UserRecordWithEmailProtected user = service.getUserWithFallbackWhenUnauthorized();
assertThat(user.email()).isEqualTo("Protected");
assertThat(user.name()).isEqualTo("Protected");
}
private static Consumer<ConfigurableWebApplicationContext> disallowBeanOverriding() {
return (context) -> ((AnnotationConfigWebApplicationContext) context).setAllowBeanDefinitionOverriding(false);
}
@ -756,6 +938,16 @@ public class PrePostMethodSecurityConfigurationTests {
return advisor;
}
@Configuration
static class AuthzConfig {
@Bean
Authz authz() {
return new Authz();
}
}
@Configuration
@EnableCustomMethodSecurity
static class CustomMethodSecurityServiceConfig {

View File

@ -0,0 +1,220 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.annotation.method.configuration;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import reactor.test.StepVerifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.config.test.SpringTestContext;
import org.springframework.security.config.test.SpringTestContextExtension;
import org.springframework.security.test.context.annotation.SecurityTestExecutionListeners;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.junit.jupiter.SpringExtension;
@ExtendWith({ SpringExtension.class, SpringTestContextExtension.class })
@SecurityTestExecutionListeners
public class PrePostReactiveMethodSecurityConfigurationTests {
public final SpringTestContext spring = new SpringTestContext(this);
@Test
@WithMockUser
void getCardNumberWhenPostAuthorizeAndNotAdminThenReturnMasked() {
this.spring
.register(MethodSecurityServiceEnabledConfig.class,
ReactiveMethodSecurityService.CardNumberMaskingPostProcessor.class)
.autowire();
ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class);
StepVerifier.create(service.postAuthorizeGetCardNumberIfAdmin("4444-3333-2222-1111"))
.expectNext("****-****-****-1111")
.verifyComplete();
}
@Test
@WithMockUser
void getCardNumberWhenPreAuthorizeAndNotAdminThenReturnMasked() {
this.spring
.register(MethodSecurityServiceEnabledConfig.class, ReactiveMethodSecurityService.StarMaskingHandler.class)
.autowire();
ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class);
StepVerifier.create(service.preAuthorizeGetCardNumberIfAdmin("4444-3333-2222-1111"))
.expectNext("***")
.verifyComplete();
}
@Test
@WithMockUser
void getCardNumberWhenPreAuthorizeAndNotAdminAndChildHandlerThenResolveCorrectHandlerAndReturnMasked() {
this.spring
.register(MethodSecurityServiceEnabledConfig.class, ReactiveMethodSecurityService.StarMaskingHandler.class,
ReactiveMethodSecurityService.StartMaskingHandlerChild.class)
.autowire();
ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class);
StepVerifier.create(service.preAuthorizeWithHandlerChildGetCardNumberIfAdmin("4444-3333-2222-1111"))
.expectNext("***-child")
.verifyComplete();
}
@Test
@WithMockUser(roles = "ADMIN")
void preAuthorizeWhenHandlerAndAccessDeniedNotThrownFromPreAuthorizeThenNotHandled() {
this.spring
.register(MethodSecurityServiceEnabledConfig.class, ReactiveMethodSecurityService.StarMaskingHandler.class)
.autowire();
ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class);
StepVerifier.create(service.preAuthorizeThrowAccessDeniedManually())
.expectError(AccessDeniedException.class)
.verify();
}
@Test
@WithMockUser
void preAuthorizeWhenDeniedAndHandlerWithCustomAnnotationThenHandlerCanUseMaskFromOtherAnnotation() {
this.spring
.register(MethodSecurityServiceEnabledConfig.class,
ReactiveMethodSecurityService.MaskAnnotationHandler.class)
.autowire();
ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class);
StepVerifier.create(service.preAuthorizeDeniedMethodWithMaskAnnotation())
.expectNext("methodmask")
.verifyComplete();
}
@Test
@WithMockUser
void preAuthorizeWhenDeniedAndHandlerWithCustomAnnotationInClassThenHandlerCanUseMaskFromOtherAnnotation() {
this.spring
.register(MethodSecurityServiceEnabledConfig.class,
ReactiveMethodSecurityService.MaskAnnotationHandler.class)
.autowire();
ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class);
StepVerifier.create(service.preAuthorizeDeniedMethodWithNoMaskAnnotation())
.expectNext("classmask")
.verifyComplete();
}
@Test
@WithMockUser(roles = "ADMIN")
void postAuthorizeWhenHandlerAndAccessDeniedNotThrownFromPostAuthorizeThenNotHandled() {
this.spring
.register(MethodSecurityServiceEnabledConfig.class,
ReactiveMethodSecurityService.PostMaskingPostProcessor.class)
.autowire();
ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class);
StepVerifier.create(service.postAuthorizeThrowAccessDeniedManually())
.expectError(AccessDeniedException.class)
.verify();
}
@Test
@WithMockUser
void postAuthorizeWhenNullDeniedMetaAnnotationThanWorks() {
this.spring
.register(MethodSecurityServiceEnabledConfig.class, ReactiveMethodSecurityService.NullPostProcessor.class)
.autowire();
ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class);
StepVerifier.create(service.postAuthorizeDeniedWithNullDenied()).verifyComplete();
}
@Test
@WithMockUser
void postAuthorizeWhenDeniedAndHandlerWithCustomAnnotationThenHandlerCanUseMaskFromOtherAnnotation() {
this.spring
.register(MethodSecurityServiceEnabledConfig.class,
ReactiveMethodSecurityService.MaskAnnotationPostProcessor.class)
.autowire();
ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class);
StepVerifier.create(service.postAuthorizeDeniedMethodWithMaskAnnotation())
.expectNext("methodmask")
.verifyComplete();
}
@Test
@WithMockUser
void postAuthorizeWhenDeniedAndHandlerWithCustomAnnotationInClassThenHandlerCanUseMaskFromOtherAnnotation() {
this.spring
.register(MethodSecurityServiceEnabledConfig.class,
ReactiveMethodSecurityService.MaskAnnotationPostProcessor.class)
.autowire();
ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class);
StepVerifier.create(service.postAuthorizeDeniedMethodWithNoMaskAnnotation())
.expectNext("classmask")
.verifyComplete();
}
@Test
@WithMockUser
void postAuthorizeWhenDeniedAndHandlerWithCustomAnnotationUsingBeanThenHandlerCanUseMaskFromOtherAnnotation() {
this.spring
.register(MethodSecurityServiceEnabledConfig.class,
ReactiveMethodSecurityService.MaskAnnotationPostProcessor.class, MyMasker.class)
.autowire();
ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class);
StepVerifier.create(service.postAuthorizeWithMaskAnnotationUsingBean())
.expectNext("ok-masked")
.verifyComplete();
}
@Test
@WithMockUser(roles = "ADMIN")
void postAuthorizeWhenAllowedAndHandlerWithCustomAnnotationUsingBeanThenInvokeMethodNormally() {
this.spring
.register(MethodSecurityServiceEnabledConfig.class,
ReactiveMethodSecurityService.MaskAnnotationPostProcessor.class, MyMasker.class)
.autowire();
ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class);
StepVerifier.create(service.postAuthorizeWithMaskAnnotationUsingBean()).expectNext("ok").verifyComplete();
}
@Test
@WithMockUser
void preAuthorizeWhenDeniedAndHandlerWithCustomAnnotationUsingBeanThenHandlerCanUseMaskFromOtherAnnotation() {
this.spring
.register(MethodSecurityServiceEnabledConfig.class,
ReactiveMethodSecurityService.MaskAnnotationHandler.class, MyMasker.class)
.autowire();
ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class);
StepVerifier.create(service.preAuthorizeWithMaskAnnotationUsingBean()).expectNext("mask").verifyComplete();
}
@Test
@WithMockUser(roles = "ADMIN")
void preAuthorizeWhenAllowedAndHandlerWithCustomAnnotationUsingBeanThenInvokeMethodNormally() {
this.spring
.register(MethodSecurityServiceEnabledConfig.class,
ReactiveMethodSecurityService.MaskAnnotationHandler.class, MyMasker.class)
.autowire();
ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class);
StepVerifier.create(service.preAuthorizeWithMaskAnnotationUsingBean()).expectNext("ok").verifyComplete();
}
@Configuration
@EnableReactiveMethodSecurity
static class MethodSecurityServiceEnabledConfig {
@Bean
ReactiveMethodSecurityService methodSecurityService() {
return new ReactiveMethodSecurityServiceImpl();
}
}
}

View File

@ -0,0 +1,222 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.annotation.method.configuration;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.aopalliance.intercept.MethodInvocation;
import reactor.core.publisher.Mono;
import org.springframework.context.ApplicationContext;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.authorization.AuthorizationResult;
import org.springframework.security.authorization.method.MethodAuthorizationDeniedHandler;
import org.springframework.security.authorization.method.MethodAuthorizationDeniedPostProcessor;
import org.springframework.security.authorization.method.MethodInvocationResult;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.StringUtils;
/**
* @author Rob Winch
*/
@ReactiveMethodSecurityService.Mask("classmask")
public interface ReactiveMethodSecurityService {
@PreAuthorize(value = "hasRole('ADMIN')", handlerClass = StarMaskingHandler.class)
Mono<String> preAuthorizeGetCardNumberIfAdmin(String cardNumber);
@PreAuthorize(value = "hasRole('ADMIN')", handlerClass = StartMaskingHandlerChild.class)
Mono<String> preAuthorizeWithHandlerChildGetCardNumberIfAdmin(String cardNumber);
@PreAuthorize(value = "hasRole('ADMIN')", handlerClass = StarMaskingHandler.class)
Mono<String> preAuthorizeThrowAccessDeniedManually();
@PostAuthorize(value = "hasRole('ADMIN')", postProcessorClass = CardNumberMaskingPostProcessor.class)
Mono<String> postAuthorizeGetCardNumberIfAdmin(String cardNumber);
@PostAuthorize(value = "hasRole('ADMIN')", postProcessorClass = PostMaskingPostProcessor.class)
Mono<String> postAuthorizeThrowAccessDeniedManually();
@PreAuthorize(value = "denyAll()", handlerClass = MaskAnnotationHandler.class)
@Mask("methodmask")
Mono<String> preAuthorizeDeniedMethodWithMaskAnnotation();
@PreAuthorize(value = "denyAll()", handlerClass = MaskAnnotationHandler.class)
Mono<String> preAuthorizeDeniedMethodWithNoMaskAnnotation();
@NullDenied(role = "ADMIN")
Mono<String> postAuthorizeDeniedWithNullDenied();
@PostAuthorize(value = "denyAll()", postProcessorClass = MaskAnnotationPostProcessor.class)
@Mask("methodmask")
Mono<String> postAuthorizeDeniedMethodWithMaskAnnotation();
@PostAuthorize(value = "denyAll()", postProcessorClass = MaskAnnotationPostProcessor.class)
Mono<String> postAuthorizeDeniedMethodWithNoMaskAnnotation();
@PreAuthorize(value = "hasRole('ADMIN')", handlerClass = MaskAnnotationHandler.class)
@Mask(expression = "@myMasker.getMask()")
Mono<String> preAuthorizeWithMaskAnnotationUsingBean();
@PostAuthorize(value = "hasRole('ADMIN')", postProcessorClass = MaskAnnotationPostProcessor.class)
@Mask(expression = "@myMasker.getMask(returnObject)")
Mono<String> postAuthorizeWithMaskAnnotationUsingBean();
class StarMaskingHandler implements MethodAuthorizationDeniedHandler {
@Override
public Object handle(MethodInvocation methodInvocation, AuthorizationResult result) {
return "***";
}
}
class StartMaskingHandlerChild extends StarMaskingHandler {
@Override
public Object handle(MethodInvocation methodInvocation, AuthorizationResult result) {
return super.handle(methodInvocation, result) + "-child";
}
}
class MaskAnnotationHandler implements MethodAuthorizationDeniedHandler {
MaskValueResolver maskValueResolver;
MaskAnnotationHandler(ApplicationContext context) {
this.maskValueResolver = new MaskValueResolver(context);
}
@Override
public Object handle(MethodInvocation methodInvocation, AuthorizationResult result) {
Mask mask = AnnotationUtils.getAnnotation(methodInvocation.getMethod(), Mask.class);
if (mask == null) {
mask = AnnotationUtils.getAnnotation(methodInvocation.getMethod().getDeclaringClass(), Mask.class);
}
return this.maskValueResolver.resolveValue(mask, methodInvocation, null);
}
}
class MaskAnnotationPostProcessor implements MethodAuthorizationDeniedPostProcessor {
MaskValueResolver maskValueResolver;
MaskAnnotationPostProcessor(ApplicationContext context) {
this.maskValueResolver = new MaskValueResolver(context);
}
@Override
public Object postProcessResult(MethodInvocationResult methodInvocationResult,
AuthorizationResult authorizationResult) {
MethodInvocation mi = methodInvocationResult.getMethodInvocation();
Mask mask = AnnotationUtils.getAnnotation(mi.getMethod(), Mask.class);
if (mask == null) {
mask = AnnotationUtils.getAnnotation(mi.getMethod().getDeclaringClass(), Mask.class);
}
return this.maskValueResolver.resolveValue(mask, mi, methodInvocationResult.getResult());
}
}
class MaskValueResolver {
DefaultMethodSecurityExpressionHandler expressionHandler;
MaskValueResolver(ApplicationContext context) {
this.expressionHandler = new DefaultMethodSecurityExpressionHandler();
this.expressionHandler.setApplicationContext(context);
}
Mono<String> resolveValue(Mask mask, MethodInvocation mi, Object returnObject) {
if (StringUtils.hasText(mask.value())) {
return Mono.just(mask.value());
}
Expression expression = this.expressionHandler.getExpressionParser().parseExpression(mask.expression());
EvaluationContext evaluationContext = this.expressionHandler
.createEvaluationContext(() -> SecurityContextHolder.getContext().getAuthentication(), mi);
if (returnObject != null) {
this.expressionHandler.setReturnObject(returnObject, evaluationContext);
}
return Mono.just(expression.getValue(evaluationContext, String.class));
}
}
class PostMaskingPostProcessor implements MethodAuthorizationDeniedPostProcessor {
@Override
public Object postProcessResult(MethodInvocationResult contextObject, AuthorizationResult result) {
return "***";
}
}
class CardNumberMaskingPostProcessor implements MethodAuthorizationDeniedPostProcessor {
static String MASK = "****-****-****-";
@Override
public Object postProcessResult(MethodInvocationResult contextObject, AuthorizationResult result) {
String cardNumber = (String) contextObject.getResult();
return MASK + cardNumber.substring(cardNumber.length() - 4);
}
}
class NullPostProcessor implements MethodAuthorizationDeniedPostProcessor {
@Override
public Object postProcessResult(MethodInvocationResult methodInvocationResult,
AuthorizationResult authorizationResult) {
return null;
}
}
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@interface Mask {
String value() default "";
String expression() default "";
}
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@PostAuthorize(value = "hasRole('{value}')", postProcessorClass = NullPostProcessor.class)
@interface NullDenied {
String role();
}
}

View File

@ -0,0 +1,85 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.annotation.method.configuration;
import reactor.core.publisher.Mono;
import org.springframework.security.access.AccessDeniedException;
public class ReactiveMethodSecurityServiceImpl implements ReactiveMethodSecurityService {
@Override
public Mono<String> preAuthorizeGetCardNumberIfAdmin(String cardNumber) {
return Mono.just(cardNumber);
}
@Override
public Mono<String> preAuthorizeWithHandlerChildGetCardNumberIfAdmin(String cardNumber) {
return Mono.just(cardNumber);
}
@Override
public Mono<String> preAuthorizeThrowAccessDeniedManually() {
return Mono.error(new AccessDeniedException("Access Denied"));
}
@Override
public Mono<String> postAuthorizeGetCardNumberIfAdmin(String cardNumber) {
return Mono.just(cardNumber);
}
@Override
public Mono<String> postAuthorizeThrowAccessDeniedManually() {
return Mono.error(new AccessDeniedException("Access Denied"));
}
@Override
public Mono<String> preAuthorizeDeniedMethodWithMaskAnnotation() {
return Mono.just("ok");
}
@Override
public Mono<String> preAuthorizeDeniedMethodWithNoMaskAnnotation() {
return Mono.just("ok");
}
@Override
public Mono<String> postAuthorizeDeniedWithNullDenied() {
return Mono.just("ok");
}
@Override
public Mono<String> postAuthorizeDeniedMethodWithMaskAnnotation() {
return Mono.just("ok");
}
@Override
public Mono<String> postAuthorizeDeniedMethodWithNoMaskAnnotation() {
return Mono.just("ok");
}
@Override
public Mono<String> preAuthorizeWithMaskAnnotationUsingBean() {
return Mono.just("ok");
}
@Override
public Mono<String> postAuthorizeWithMaskAnnotationUsingBean() {
return Mono.just("ok");
}
}

View File

@ -0,0 +1,55 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.annotation.method.configuration;
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.security.authorization.AuthorizationResult;
import org.springframework.security.authorization.method.MethodAuthorizationDeniedPostProcessor;
import org.springframework.security.authorization.method.MethodInvocationResult;
public class UserRecordWithEmailProtected {
private final String name;
private final String email;
public UserRecordWithEmailProtected(String name, String email) {
this.name = name;
this.email = email;
}
public String name() {
return this.name;
}
@PostAuthorize(value = "hasRole('ADMIN')", postProcessorClass = EmailMaskingPostProcessor.class)
public String email() {
return this.email;
}
public static class EmailMaskingPostProcessor implements MethodAuthorizationDeniedPostProcessor {
@Override
public Object postProcessResult(MethodInvocationResult methodInvocationResult,
AuthorizationResult authorizationResult) {
String email = (String) methodInvocationResult.getResult();
return email.replaceAll("(^[^@]{3}|(?!^)\\G)[^@]", "$1*");
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -23,6 +23,9 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.security.authorization.method.MethodAuthorizationDeniedPostProcessor;
import org.springframework.security.authorization.method.ThrowingMethodAuthorizationDeniedPostProcessor;
/**
* Annotation for specifying a method access-control expression which will be evaluated
* after a method has been invoked.
@ -42,4 +45,10 @@ public @interface PostAuthorize {
*/
String value();
/**
* @return the {@link MethodAuthorizationDeniedPostProcessor} class used to
* post-process access denied
*/
Class<? extends MethodAuthorizationDeniedPostProcessor> postProcessorClass() default ThrowingMethodAuthorizationDeniedPostProcessor.class;
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -23,6 +23,9 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.security.authorization.method.MethodAuthorizationDeniedHandler;
import org.springframework.security.authorization.method.ThrowingMethodAuthorizationDeniedHandler;
/**
* Annotation for specifying a method access-control expression which will be evaluated to
* decide whether a method invocation is allowed or not.
@ -42,4 +45,10 @@ public @interface PreAuthorize {
*/
String value();
/**
* @return the {@link MethodAuthorizationDeniedHandler} class used to handle access
* denied
*/
Class<? extends MethodAuthorizationDeniedHandler> handlerClass() default ThrowingMethodAuthorizationDeniedHandler.class;
}

View File

@ -0,0 +1,43 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.authorization;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.util.Assert;
/**
* An {@link AccessDeniedException} that contains the {@link AuthorizationResult}
*
* @author Marcus da Coregio
* @since 6.3
*/
public class AuthorizationDeniedException extends AccessDeniedException {
private final AuthorizationResult result;
public AuthorizationDeniedException(String msg, AuthorizationResult authorizationResult) {
super(msg);
Assert.notNull(authorizationResult, "authorizationResult cannot be null");
Assert.isTrue(!authorizationResult.isGranted(), "Granted authorization results are not supported");
this.result = authorizationResult;
}
public AuthorizationResult getAuthorizationResult() {
return this.result;
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -55,6 +55,8 @@ public final class AuthorizationManagerAfterMethodInterceptor implements Authori
private final AuthorizationManager<MethodInvocationResult> authorizationManager;
private final MethodAuthorizationDeniedPostProcessor defaultPostProcessor = new ThrowingMethodAuthorizationDeniedPostProcessor();
private int order;
private AuthorizationEventPublisher eventPublisher = AuthorizationManagerAfterMethodInterceptor::noPublish;
@ -116,8 +118,7 @@ public final class AuthorizationManagerAfterMethodInterceptor implements Authori
@Override
public Object invoke(MethodInvocation mi) throws Throwable {
Object result = mi.proceed();
attemptAuthorization(mi, result);
return result;
return attemptAuthorization(mi, result);
}
@Override
@ -168,7 +169,7 @@ public final class AuthorizationManagerAfterMethodInterceptor implements Authori
this.securityContextHolderStrategy = () -> strategy;
}
private void attemptAuthorization(MethodInvocation mi, Object result) {
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);
@ -176,9 +177,17 @@ public final class AuthorizationManagerAfterMethodInterceptor implements Authori
if (decision != null && !decision.isGranted()) {
this.logger.debug(LogMessage.of(() -> "Failed to authorize " + mi + " with authorization manager "
+ this.authorizationManager + " and decision " + decision));
throw new AccessDeniedException("Access Denied");
return postProcess(object, decision);
}
this.logger.debug(LogMessage.of(() -> "Authorized method invocation " + mi));
return result;
}
private Object postProcess(MethodInvocationResult mi, AuthorizationDecision decision) {
if (decision instanceof MethodAuthorizationDeniedPostProcessor postProcessableDecision) {
return postProcessableDecision.postProcessResult(mi, decision);
}
return this.defaultPostProcessor.postProcessResult(mi, decision);
}
private Authentication getAuthentication() {

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -33,6 +33,7 @@ import org.springframework.core.MethodParameter;
import org.springframework.core.ReactiveAdapter;
import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.ReactiveAuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.util.Assert;
@ -57,6 +58,8 @@ public final class AuthorizationManagerAfterReactiveMethodInterceptor implements
private int order = AuthorizationInterceptorsOrder.LAST.getOrder();
private final MethodAuthorizationDeniedPostProcessor defaultPostProcessor = new ThrowingMethodAuthorizationDeniedPostProcessor();
/**
* Creates an instance for the {@link PostAuthorize} annotation.
* @return the {@link AuthorizationManagerAfterReactiveMethodInterceptor} to use
@ -144,9 +147,28 @@ public final class AuthorizationManagerAfterReactiveMethodInterceptor implements
return adapter != null && adapter.isMultiValue();
}
private Mono<?> postAuthorize(Mono<Authentication> authentication, MethodInvocation mi, Object result) {
return this.authorizationManager.verify(authentication, new MethodInvocationResult(mi, result))
.thenReturn(result);
private Mono<Object> postAuthorize(Mono<Authentication> authentication, MethodInvocation mi, Object result) {
MethodInvocationResult invocationResult = new MethodInvocationResult(mi, result);
return this.authorizationManager.check(authentication, invocationResult)
.switchIfEmpty(Mono.just(new AuthorizationDecision(false)))
.flatMap((decision) -> postProcess(decision, invocationResult));
}
private Mono<Object> postProcess(AuthorizationDecision decision, MethodInvocationResult methodInvocationResult) {
if (decision.isGranted()) {
return Mono.just(methodInvocationResult.getResult());
}
return Mono.fromSupplier(() -> {
if (decision instanceof MethodAuthorizationDeniedPostProcessor postProcessableDecision) {
return postProcessableDecision.postProcessResult(methodInvocationResult, decision);
}
return this.defaultPostProcessor.postProcessResult(methodInvocationResult, decision);
}).flatMap((processedResult) -> {
if (Mono.class.isAssignableFrom(processedResult.getClass())) {
return (Mono<?>) processedResult;
}
return Mono.justOrEmpty(processedResult);
});
}
@Override

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -59,6 +59,8 @@ public final class AuthorizationManagerBeforeMethodInterceptor implements Author
private final AuthorizationManager<MethodInvocation> authorizationManager;
private final MethodAuthorizationDeniedHandler defaultHandler = new ThrowingMethodAuthorizationDeniedHandler();
private int order = AuthorizationInterceptorsOrder.FIRST.getOrder();
private AuthorizationEventPublisher eventPublisher = AuthorizationManagerBeforeMethodInterceptor::noPublish;
@ -190,8 +192,7 @@ public final class AuthorizationManagerBeforeMethodInterceptor implements Author
*/
@Override
public Object invoke(MethodInvocation mi) throws Throwable {
attemptAuthorization(mi);
return mi.proceed();
return attemptAuthorization(mi);
}
@Override
@ -242,16 +243,24 @@ public final class AuthorizationManagerBeforeMethodInterceptor implements Author
this.securityContextHolderStrategy = () -> securityContextHolderStrategy;
}
private void attemptAuthorization(MethodInvocation mi) {
private Object attemptAuthorization(MethodInvocation mi) throws Throwable {
this.logger.debug(LogMessage.of(() -> "Authorizing method invocation " + mi));
AuthorizationDecision decision = this.authorizationManager.check(this::getAuthentication, mi);
this.eventPublisher.publishAuthorizationEvent(this::getAuthentication, mi, decision);
if (decision != null && !decision.isGranted()) {
this.logger.debug(LogMessage.of(() -> "Failed to authorize " + mi + " with authorization manager "
+ this.authorizationManager + " and decision " + decision));
throw new AccessDeniedException("Access Denied");
return handle(mi, decision);
}
this.logger.debug(LogMessage.of(() -> "Authorized method invocation " + mi));
return mi.proceed();
}
private Object handle(MethodInvocation mi, AuthorizationDecision decision) {
if (decision instanceof MethodAuthorizationDeniedHandler handler) {
return handler.handle(mi, decision);
}
return this.defaultHandler.handle(mi, decision);
}
private Authentication getAuthentication() {

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -32,6 +32,7 @@ import org.springframework.core.MethodParameter;
import org.springframework.core.ReactiveAdapter;
import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.ReactiveAuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.util.Assert;
@ -57,6 +58,8 @@ public final class AuthorizationManagerBeforeReactiveMethodInterceptor implement
private int order = AuthorizationInterceptorsOrder.FIRST.getOrder();
private final MethodAuthorizationDeniedHandler defaultHandler = new ThrowingMethodAuthorizationDeniedHandler();
/**
* Creates an instance for the {@link PreAuthorize} annotation.
* @return the {@link AuthorizationManagerBeforeReactiveMethodInterceptor} to use
@ -112,31 +115,65 @@ public final class AuthorizationManagerBeforeReactiveMethodInterceptor implement
+ " must return an instance of org.reactivestreams.Publisher "
+ "(for example, a Mono or Flux) or the function must be a Kotlin coroutine "
+ "in order to support Reactor Context");
Mono<Authentication> authentication = ReactiveAuthenticationUtils.getAuthentication();
ReactiveAdapter adapter = ReactiveAdapterRegistry.getSharedInstance().getAdapter(type);
Mono<Void> preAuthorize = this.authorizationManager.verify(authentication, mi);
if (hasFlowReturnType) {
if (isSuspendingFunction) {
return preAuthorize.thenMany(Flux.defer(() -> ReactiveMethodInvocationUtils.proceed(mi)));
return preAuthorized(mi, Flux.defer(() -> ReactiveMethodInvocationUtils.proceed(mi)));
}
else {
Assert.state(adapter != null, () -> "The returnType " + type + " on " + method
+ " must have a org.springframework.core.ReactiveAdapter registered");
Flux<?> response = preAuthorize
.thenMany(Flux.defer(() -> adapter.toPublisher(ReactiveMethodInvocationUtils.proceed(mi))));
Flux<Object> response = preAuthorized(mi,
Flux.defer(() -> adapter.toPublisher(ReactiveMethodInvocationUtils.proceed(mi))));
return KotlinDelegate.asFlow(response);
}
}
if (isMultiValue(type, adapter)) {
Publisher<?> publisher = Flux.defer(() -> ReactiveMethodInvocationUtils.proceed(mi));
Flux<?> result = preAuthorize.thenMany(publisher);
Flux<?> result = preAuthorized(mi, Flux.defer(() -> ReactiveMethodInvocationUtils.proceed(mi)));
return (adapter != null) ? adapter.fromPublisher(result) : result;
}
Mono<?> publisher = Mono.defer(() -> ReactiveMethodInvocationUtils.proceed(mi));
Mono<?> result = preAuthorize.then(publisher);
Mono<?> result = preAuthorized(mi, Mono.defer(() -> ReactiveMethodInvocationUtils.proceed(mi)));
return (adapter != null) ? adapter.fromPublisher(result) : result;
}
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;
}
return postProcess(decision, mi);
});
}
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;
}
return postProcess(decision, mi);
});
}
private Mono<Object> postProcess(AuthorizationDecision decision, MethodInvocation mi) {
return Mono.fromSupplier(() -> {
if (decision instanceof MethodAuthorizationDeniedHandler handler) {
return handler.handle(mi, decision);
}
return this.defaultHandler.handle(mi, decision);
}).flatMap((result) -> {
if (Mono.class.isAssignableFrom(result.getClass())) {
return (Mono<?>) result;
}
return Mono.justOrEmpty(result);
});
}
private boolean isMultiValue(Class<?> returnType, ReactiveAdapter adapter) {
if (Flux.class.isAssignableFrom(returnType)) {
return true;

View File

@ -0,0 +1,46 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.authorization.method;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.lang.Nullable;
import org.springframework.security.authorization.AuthorizationResult;
/**
* An interface used to define a strategy to handle denied method invocations
*
* @author Marcus da Coregio
* @since 6.3
* @see org.springframework.security.access.prepost.PreAuthorize
*/
public interface MethodAuthorizationDeniedHandler {
/**
* 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 authorizationResult the authorization denied result
* @return a replacement result for the denied method invocation, or null, or a
* {@link reactor.core.publisher.Mono} for reactive applications
*/
@Nullable
Object handle(MethodInvocation methodInvocation, AuthorizationResult authorizationResult);
}

View File

@ -0,0 +1,46 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.authorization.method;
import org.springframework.lang.Nullable;
import org.springframework.security.authorization.AuthorizationResult;
/**
* An interface to define a strategy to handle denied method invocation results
*
* @author Marcus da Coregio
* @since 6.3
* @see org.springframework.security.access.prepost.PostAuthorize
*/
public interface MethodAuthorizationDeniedPostProcessor {
/**
* 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 authorizationResult the {@link AuthorizationResult} 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
*/
@Nullable
Object postProcessResult(MethodInvocationResult methodInvocationResult, AuthorizationResult authorizationResult);
}

View File

@ -0,0 +1,41 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.authorization.method;
import org.springframework.expression.Expression;
import org.springframework.security.authorization.AuthorizationResult;
import org.springframework.security.authorization.ExpressionAuthorizationDecision;
import org.springframework.util.Assert;
class PostAuthorizeAuthorizationDecision extends ExpressionAuthorizationDecision
implements MethodAuthorizationDeniedPostProcessor {
private final MethodAuthorizationDeniedPostProcessor postProcessor;
PostAuthorizeAuthorizationDecision(boolean granted, Expression expression,
MethodAuthorizationDeniedPostProcessor postProcessor) {
super(granted, expression);
Assert.notNull(postProcessor, "postProcessor cannot be null");
this.postProcessor = postProcessor;
}
@Override
public Object postProcessResult(MethodInvocationResult methodInvocationResult, AuthorizationResult result) {
return this.postProcessor.postProcessResult(methodInvocationResult, result);
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -20,13 +20,13 @@ import java.util.function.Supplier;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.context.ApplicationContext;
import org.springframework.expression.EvaluationContext;
import org.springframework.security.access.expression.ExpressionUtils;
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.authorization.ExpressionAuthorizationDecision;
import org.springframework.security.core.Authentication;
/**
@ -61,6 +61,18 @@ public final class PostAuthorizeAuthorizationManager implements AuthorizationMan
this.registry.setTemplateDefaults(defaults);
}
/**
* Invokes
* {@link PostAuthorizeExpressionAttributeRegistry#setApplicationContext(ApplicationContext)}
* with the provided {@link ApplicationContext}.
* @param context the {@link ApplicationContext}
* @since 6.3
* @see PreAuthorizeExpressionAttributeRegistry#setApplicationContext(ApplicationContext)
*/
public void setApplicationContext(ApplicationContext context) {
this.registry.setApplicationContext(context);
}
/**
* Determine if an {@link Authentication} has access to the returned object by
* evaluating the {@link PostAuthorize} annotation that the {@link MethodInvocation}
@ -76,11 +88,13 @@ public final class PostAuthorizeAuthorizationManager implements AuthorizationMan
if (attribute == ExpressionAttribute.NULL_ATTRIBUTE) {
return null;
}
PostAuthorizeExpressionAttribute postAuthorizeAttribute = (PostAuthorizeExpressionAttribute) attribute;
MethodSecurityExpressionHandler expressionHandler = this.registry.getExpressionHandler();
EvaluationContext ctx = expressionHandler.createEvaluationContext(authentication, mi.getMethodInvocation());
expressionHandler.setReturnObject(mi.getResult(), ctx);
boolean granted = ExpressionUtils.evaluateAsBoolean(attribute.getExpression(), ctx);
return new ExpressionAuthorizationDecision(granted, attribute.getExpression());
boolean granted = ExpressionUtils.evaluateAsBoolean(postAuthorizeAttribute.getExpression(), ctx);
return new PostAuthorizeAuthorizationDecision(granted, postAuthorizeAttribute.getExpression(),
postAuthorizeAttribute.getPostProcessor());
}
}

View File

@ -0,0 +1,42 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.authorization.method;
import org.springframework.expression.Expression;
import org.springframework.util.Assert;
/**
* An {@link ExpressionAttribute} that carries additional properties for
* {@code @PostAuthorize}.
*
* @author Marcus da Coregio
*/
class PostAuthorizeExpressionAttribute extends ExpressionAttribute {
private final MethodAuthorizationDeniedPostProcessor postProcessor;
PostAuthorizeExpressionAttribute(Expression expression, MethodAuthorizationDeniedPostProcessor postProcessor) {
super(expression);
Assert.notNull(postProcessor, "postProcessor cannot be null");
this.postProcessor = postProcessor;
}
MethodAuthorizationDeniedPostProcessor getPostProcessor() {
return this.postProcessor;
}
}

View File

@ -18,13 +18,16 @@ package org.springframework.security.authorization.method;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.function.Function;
import reactor.util.annotation.NonNull;
import org.springframework.aop.support.AopUtils;
import org.springframework.context.ApplicationContext;
import org.springframework.expression.Expression;
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.util.Assert;
/**
* For internal use only, as this contract is likely to change.
@ -35,6 +38,14 @@ import org.springframework.security.access.prepost.PostAuthorize;
*/
final class PostAuthorizeExpressionAttributeRegistry extends AbstractExpressionAttributeRegistry<ExpressionAttribute> {
private final MethodAuthorizationDeniedPostProcessor defaultPostProcessor = new ThrowingMethodAuthorizationDeniedPostProcessor();
private Function<Class<? extends MethodAuthorizationDeniedPostProcessor>, MethodAuthorizationDeniedPostProcessor> postProcessorResolver;
PostAuthorizeExpressionAttributeRegistry() {
this.postProcessorResolver = (clazz) -> this.defaultPostProcessor;
}
@NonNull
@Override
ExpressionAttribute resolveAttribute(Method method, Class<?> targetClass) {
@ -44,7 +55,9 @@ final class PostAuthorizeExpressionAttributeRegistry extends AbstractExpressionA
return ExpressionAttribute.NULL_ATTRIBUTE;
}
Expression expression = getExpressionHandler().getExpressionParser().parseExpression(postAuthorize.value());
return new ExpressionAttribute(expression);
MethodAuthorizationDeniedPostProcessor postProcessor = this.postProcessorResolver
.apply(postAuthorize.postProcessorClass());
return new PostAuthorizeExpressionAttribute(expression, postProcessor);
}
private PostAuthorize findPostAuthorizeAnnotation(Method method, Class<?> targetClass) {
@ -53,4 +66,30 @@ final class PostAuthorizeExpressionAttributeRegistry extends AbstractExpressionA
return (postAuthorize != null) ? postAuthorize : lookup.apply(targetClass(method, targetClass));
}
/**
* Uses the provided {@link ApplicationContext} to resolve the
* {@link MethodAuthorizationDeniedPostProcessor} from {@link PostAuthorize}
* @param context the {@link ApplicationContext} to use
*/
void setApplicationContext(ApplicationContext context) {
Assert.notNull(context, "context cannot be null");
this.postProcessorResolver = (postProcessorClass) -> resolvePostProcessor(context, postProcessorClass);
}
private MethodAuthorizationDeniedPostProcessor resolvePostProcessor(ApplicationContext context,
Class<? extends MethodAuthorizationDeniedPostProcessor> postProcessorClass) {
if (postProcessorClass == this.defaultPostProcessor.getClass()) {
return this.defaultPostProcessor;
}
String[] beanNames = context.getBeanNamesForType(postProcessorClass);
if (beanNames.length == 0) {
throw new IllegalStateException("Could not find a bean of type " + postProcessorClass.getName());
}
if (beanNames.length > 1) {
throw new IllegalStateException("Expected to find a single bean of type " + postProcessorClass.getName()
+ " but found " + Arrays.toString(beanNames));
}
return context.getBean(beanNames[0], postProcessorClass);
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -19,6 +19,7 @@ package org.springframework.security.authorization.method;
import org.aopalliance.intercept.MethodInvocation;
import reactor.core.publisher.Mono;
import org.springframework.context.ApplicationContext;
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
import org.springframework.security.access.prepost.PostAuthorize;
@ -61,6 +62,10 @@ public final class PostAuthorizeReactiveAuthorizationManager
this.registry.setTemplateDefaults(defaults);
}
public void setApplicationContext(ApplicationContext context) {
this.registry.setApplicationContext(context);
}
/**
* Determines if an {@link Authentication} has access to the returned object from the
* {@link MethodInvocation} by evaluating an expression from the {@link PostAuthorize}
@ -77,13 +82,14 @@ public final class PostAuthorizeReactiveAuthorizationManager
if (attribute == ExpressionAttribute.NULL_ATTRIBUTE) {
return Mono.empty();
}
PostAuthorizeExpressionAttribute postAuthorizeAttribute = (PostAuthorizeExpressionAttribute) attribute;
MethodSecurityExpressionHandler expressionHandler = this.registry.getExpressionHandler();
// @formatter:off
return authentication
.map((auth) -> expressionHandler.createEvaluationContext(auth, mi))
.doOnNext((ctx) -> expressionHandler.setReturnObject(result.getResult(), ctx))
.flatMap((ctx) -> ReactiveExpressionUtils.evaluateAsBoolean(attribute.getExpression(), ctx))
.map((granted) -> new ExpressionAttributeAuthorizationDecision(granted, attribute));
.map((granted) -> new PostAuthorizeAuthorizationDecision(granted, postAuthorizeAttribute.getExpression(), postAuthorizeAttribute.getPostProcessor()));
// @formatter:on
}

View File

@ -0,0 +1,43 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.authorization.method;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.expression.Expression;
import org.springframework.security.authorization.AuthorizationResult;
import org.springframework.security.authorization.ExpressionAuthorizationDecision;
import org.springframework.util.Assert;
class PreAuthorizeAuthorizationDecision extends ExpressionAuthorizationDecision
implements MethodAuthorizationDeniedHandler {
private final MethodAuthorizationDeniedHandler handler;
PreAuthorizeAuthorizationDecision(boolean granted, Expression expression,
MethodAuthorizationDeniedHandler handler) {
super(granted, expression);
Assert.notNull(handler, "handler cannot be null");
this.handler = handler;
}
@Override
public Object handle(MethodInvocation methodInvocation, AuthorizationResult result) {
return this.handler.handle(methodInvocation, result);
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -20,13 +20,13 @@ import java.util.function.Supplier;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.context.ApplicationContext;
import org.springframework.expression.EvaluationContext;
import org.springframework.security.access.expression.ExpressionUtils;
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.authorization.ExpressionAuthorizationDecision;
import org.springframework.security.core.Authentication;
/**
@ -61,6 +61,10 @@ public final class PreAuthorizeAuthorizationManager implements AuthorizationMana
this.registry.setTemplateDefaults(defaults);
}
public void setApplicationContext(ApplicationContext context) {
this.registry.setApplicationContext(context);
}
/**
* Determine if an {@link Authentication} has access to a method by evaluating an
* expression from the {@link PreAuthorize} annotation that the
@ -76,9 +80,11 @@ public final class PreAuthorizeAuthorizationManager implements AuthorizationMana
if (attribute == ExpressionAttribute.NULL_ATTRIBUTE) {
return null;
}
PreAuthorizeExpressionAttribute preAuthorizeAttribute = (PreAuthorizeExpressionAttribute) attribute;
EvaluationContext ctx = this.registry.getExpressionHandler().createEvaluationContext(authentication, mi);
boolean granted = ExpressionUtils.evaluateAsBoolean(attribute.getExpression(), ctx);
return new ExpressionAuthorizationDecision(granted, attribute.getExpression());
boolean granted = ExpressionUtils.evaluateAsBoolean(preAuthorizeAttribute.getExpression(), ctx);
return new PreAuthorizeAuthorizationDecision(granted, preAuthorizeAttribute.getExpression(),
preAuthorizeAttribute.getHandler());
}
}

View File

@ -0,0 +1,42 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.authorization.method;
import org.springframework.expression.Expression;
import org.springframework.util.Assert;
/**
* An {@link ExpressionAttribute} that carries additional properties for
* {@code @PreAuthorize}.
*
* @author Marcus da Coregio
*/
class PreAuthorizeExpressionAttribute extends ExpressionAttribute {
private final MethodAuthorizationDeniedHandler handler;
PreAuthorizeExpressionAttribute(Expression expression, MethodAuthorizationDeniedHandler handler) {
super(expression);
Assert.notNull(handler, "handler cannot be null");
this.handler = handler;
}
MethodAuthorizationDeniedHandler getHandler() {
return this.handler;
}
}

View File

@ -18,13 +18,16 @@ package org.springframework.security.authorization.method;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.function.Function;
import reactor.util.annotation.NonNull;
import org.springframework.aop.support.AopUtils;
import org.springframework.context.ApplicationContext;
import org.springframework.expression.Expression;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.util.Assert;
/**
* For internal use only, as this contract is likely to change.
@ -35,6 +38,14 @@ import org.springframework.security.access.prepost.PreAuthorize;
*/
final class PreAuthorizeExpressionAttributeRegistry extends AbstractExpressionAttributeRegistry<ExpressionAttribute> {
private final MethodAuthorizationDeniedHandler defaultHandler = new ThrowingMethodAuthorizationDeniedHandler();
private Function<Class<? extends MethodAuthorizationDeniedHandler>, MethodAuthorizationDeniedHandler> handlerResolver;
PreAuthorizeExpressionAttributeRegistry() {
this.handlerResolver = (clazz) -> this.defaultHandler;
}
@NonNull
@Override
ExpressionAttribute resolveAttribute(Method method, Class<?> targetClass) {
@ -44,7 +55,8 @@ final class PreAuthorizeExpressionAttributeRegistry extends AbstractExpressionAt
return ExpressionAttribute.NULL_ATTRIBUTE;
}
Expression expression = getExpressionHandler().getExpressionParser().parseExpression(preAuthorize.value());
return new ExpressionAttribute(expression);
MethodAuthorizationDeniedHandler handler = this.handlerResolver.apply(preAuthorize.handlerClass());
return new PreAuthorizeExpressionAttribute(expression, handler);
}
private PreAuthorize findPreAuthorizeAnnotation(Method method, Class<?> targetClass) {
@ -53,4 +65,30 @@ final class PreAuthorizeExpressionAttributeRegistry extends AbstractExpressionAt
return (preAuthorize != null) ? preAuthorize : lookup.apply(targetClass(method, targetClass));
}
/**
* Uses the provided {@link ApplicationContext} to resolve the
* {@link MethodAuthorizationDeniedHandler} from {@link PreAuthorize}.
* @param context the {@link ApplicationContext} to use
*/
void setApplicationContext(ApplicationContext context) {
Assert.notNull(context, "context cannot be null");
this.handlerResolver = (clazz) -> resolveHandler(context, clazz);
}
private MethodAuthorizationDeniedHandler resolveHandler(ApplicationContext context,
Class<? extends MethodAuthorizationDeniedHandler> handlerClass) {
if (handlerClass == this.defaultHandler.getClass()) {
return this.defaultHandler;
}
String[] beanNames = context.getBeanNamesForType(handlerClass);
if (beanNames.length == 0) {
throw new IllegalStateException("Could not find a bean of type " + handlerClass.getName());
}
if (beanNames.length > 1) {
throw new IllegalStateException("Expected to find a single bean of type " + handlerClass.getName()
+ " but found " + Arrays.toString(beanNames));
}
return context.getBean(beanNames[0], handlerClass);
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -19,6 +19,7 @@ package org.springframework.security.authorization.method;
import org.aopalliance.intercept.MethodInvocation;
import reactor.core.publisher.Mono;
import org.springframework.context.ApplicationContext;
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
import org.springframework.security.access.prepost.PreAuthorize;
@ -60,6 +61,10 @@ public final class PreAuthorizeReactiveAuthorizationManager implements ReactiveA
this.registry.setTemplateDefaults(defaults);
}
public void setApplicationContext(ApplicationContext context) {
this.registry.setApplicationContext(context);
}
/**
* Determines if an {@link Authentication} has access to the {@link MethodInvocation}
* by evaluating an expression from the {@link PreAuthorize} annotation.
@ -74,11 +79,12 @@ public final class PreAuthorizeReactiveAuthorizationManager implements ReactiveA
if (attribute == ExpressionAttribute.NULL_ATTRIBUTE) {
return Mono.empty();
}
PreAuthorizeExpressionAttribute preAuthorizeAttribute = (PreAuthorizeExpressionAttribute) attribute;
// @formatter:off
return authentication
.map((auth) -> this.registry.getExpressionHandler().createEvaluationContext(auth, mi))
.flatMap((ctx) -> ReactiveExpressionUtils.evaluateAsBoolean(attribute.getExpression(), ctx))
.map((granted) -> new ExpressionAttributeAuthorizationDecision(granted, attribute));
.map((granted) -> new PreAuthorizeAuthorizationDecision(granted, preAuthorizeAttribute.getExpression(), preAuthorizeAttribute.getHandler()));
// @formatter:on
}

View File

@ -0,0 +1,38 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.authorization.method;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.security.authorization.AuthorizationDeniedException;
import org.springframework.security.authorization.AuthorizationResult;
/**
* An implementation of {@link MethodAuthorizationDeniedHandler} that throws
* {@link AuthorizationDeniedException}
*
* @author Marcus da Coregio
* @since 6.3
*/
public final class ThrowingMethodAuthorizationDeniedHandler implements MethodAuthorizationDeniedHandler {
@Override
public Object handle(MethodInvocation methodInvocation, AuthorizationResult result) {
throw new AuthorizationDeniedException("Access Denied", result);
}
}

View File

@ -0,0 +1,36 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.authorization.method;
import org.springframework.security.authorization.AuthorizationDeniedException;
import org.springframework.security.authorization.AuthorizationResult;
/**
* An implementation of {@link MethodAuthorizationDeniedPostProcessor} that throws
* {@link AuthorizationDeniedException}
*
* @author Marcus da Coregio
* @since 6.3
*/
public final class ThrowingMethodAuthorizationDeniedPostProcessor implements MethodAuthorizationDeniedPostProcessor {
@Override
public Object postProcessResult(MethodInvocationResult methodInvocationResult, AuthorizationResult result) {
throw new AuthorizationDeniedException("Access Denied", result);
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -23,8 +23,12 @@ import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.aop.Pointcut;
import org.springframework.expression.common.LiteralExpression;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.intercept.method.MockMethodInvocation;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationDeniedException;
import org.springframework.security.authorization.AuthorizationResult;
import org.springframework.security.authorization.ReactiveAuthorizationManager;
import static org.assertj.core.api.Assertions.assertThat;
@ -66,14 +70,15 @@ public class AuthorizationManagerAfterReactiveMethodInterceptorTests {
given(mockMethodInvocation.proceed()).willReturn(Mono.just("john"));
ReactiveAuthorizationManager<MethodInvocationResult> mockReactiveAuthorizationManager = mock(
ReactiveAuthorizationManager.class);
given(mockReactiveAuthorizationManager.verify(any(), any())).willReturn(Mono.empty());
given(mockReactiveAuthorizationManager.check(any(), any()))
.willReturn(Mono.just(new AuthorizationDecision(true)));
AuthorizationManagerAfterReactiveMethodInterceptor interceptor = new AuthorizationManagerAfterReactiveMethodInterceptor(
Pointcut.TRUE, mockReactiveAuthorizationManager);
Object result = interceptor.invoke(mockMethodInvocation);
assertThat(result).asInstanceOf(InstanceOfAssertFactories.type(Mono.class))
.extracting(Mono::block)
.isEqualTo("john");
verify(mockReactiveAuthorizationManager).verify(any(), any());
verify(mockReactiveAuthorizationManager).check(any(), any());
}
@Test
@ -83,7 +88,8 @@ public class AuthorizationManagerAfterReactiveMethodInterceptorTests {
given(mockMethodInvocation.proceed()).willReturn(Flux.just("john", "bob"));
ReactiveAuthorizationManager<MethodInvocationResult> mockReactiveAuthorizationManager = mock(
ReactiveAuthorizationManager.class);
given(mockReactiveAuthorizationManager.verify(any(), any())).willReturn(Mono.empty());
given(mockReactiveAuthorizationManager.check(any(), any()))
.willReturn(Mono.just(new AuthorizationDecision(true)));
AuthorizationManagerAfterReactiveMethodInterceptor interceptor = new AuthorizationManagerAfterReactiveMethodInterceptor(
Pointcut.TRUE, mockReactiveAuthorizationManager);
Object result = interceptor.invoke(mockMethodInvocation);
@ -91,7 +97,7 @@ public class AuthorizationManagerAfterReactiveMethodInterceptorTests {
.extracting(Flux::collectList)
.extracting(Mono::block, InstanceOfAssertFactories.list(String.class))
.containsExactly("john", "bob");
verify(mockReactiveAuthorizationManager, times(2)).verify(any(), any());
verify(mockReactiveAuthorizationManager, times(2)).check(any(), any());
}
@Test
@ -101,8 +107,8 @@ public class AuthorizationManagerAfterReactiveMethodInterceptorTests {
given(mockMethodInvocation.proceed()).willReturn(Mono.just("john"));
ReactiveAuthorizationManager<MethodInvocationResult> mockReactiveAuthorizationManager = mock(
ReactiveAuthorizationManager.class);
given(mockReactiveAuthorizationManager.verify(any(), any()))
.willReturn(Mono.error(new AccessDeniedException("Access Denied")));
given(mockReactiveAuthorizationManager.check(any(), any()))
.willReturn(Mono.just(new AuthorizationDecision(false)));
AuthorizationManagerAfterReactiveMethodInterceptor interceptor = new AuthorizationManagerAfterReactiveMethodInterceptor(
Pointcut.TRUE, mockReactiveAuthorizationManager);
Object result = interceptor.invoke(mockMethodInvocation);
@ -110,7 +116,157 @@ public class AuthorizationManagerAfterReactiveMethodInterceptorTests {
.isThrownBy(() -> assertThat(result).asInstanceOf(InstanceOfAssertFactories.type(Mono.class))
.extracting(Mono::block))
.withMessage("Access Denied");
verify(mockReactiveAuthorizationManager).verify(any(), any());
verify(mockReactiveAuthorizationManager).check(any(), any());
}
@Test
public void invokeFluxWhenAllValuesDeniedAndPostProcessorThenPostProcessorAppliedToEachValueEmitted()
throws Throwable {
MethodInvocation mockMethodInvocation = spy(
new MockMethodInvocation(new Sample(), Sample.class.getDeclaredMethod("flux")));
given(mockMethodInvocation.proceed()).willReturn(Flux.just("john", "bob"));
ReactiveAuthorizationManager<MethodInvocationResult> mockReactiveAuthorizationManager = mock(
ReactiveAuthorizationManager.class);
given(mockReactiveAuthorizationManager.check(any(), any()))
.will((invocation) -> Mono.just(createDecision(new MaskingPostProcessor())));
AuthorizationManagerAfterReactiveMethodInterceptor interceptor = new AuthorizationManagerAfterReactiveMethodInterceptor(
Pointcut.TRUE, mockReactiveAuthorizationManager);
Object result = interceptor.invoke(mockMethodInvocation);
assertThat(result).asInstanceOf(InstanceOfAssertFactories.type(Flux.class))
.extracting(Flux::collectList)
.extracting(Mono::block, InstanceOfAssertFactories.list(String.class))
.containsExactly("john-masked", "bob-masked");
verify(mockReactiveAuthorizationManager, times(2)).check(any(), any());
}
@Test
public void invokeFluxWhenOneValueDeniedAndPostProcessorThenPostProcessorAppliedToDeniedValue() throws Throwable {
MethodInvocation mockMethodInvocation = spy(
new MockMethodInvocation(new Sample(), Sample.class.getDeclaredMethod("flux")));
given(mockMethodInvocation.proceed()).willReturn(Flux.just("john", "bob"));
ReactiveAuthorizationManager<MethodInvocationResult> mockReactiveAuthorizationManager = mock(
ReactiveAuthorizationManager.class);
given(mockReactiveAuthorizationManager.check(any(), any())).willAnswer((invocation) -> {
MethodInvocationResult argument = invocation.getArgument(1);
if ("john".equals(argument.getResult())) {
return Mono.just(new AuthorizationDecision(true));
}
return Mono.just(createDecision(new MaskingPostProcessor()));
});
AuthorizationManagerAfterReactiveMethodInterceptor interceptor = new AuthorizationManagerAfterReactiveMethodInterceptor(
Pointcut.TRUE, mockReactiveAuthorizationManager);
Object result = interceptor.invoke(mockMethodInvocation);
assertThat(result).asInstanceOf(InstanceOfAssertFactories.type(Flux.class))
.extracting(Flux::collectList)
.extracting(Mono::block, InstanceOfAssertFactories.list(String.class))
.containsExactly("john", "bob-masked");
verify(mockReactiveAuthorizationManager, times(2)).check(any(), any());
}
@Test
public void invokeMonoWhenPostProcessableDecisionThenPostProcess() throws Throwable {
MethodInvocation mockMethodInvocation = spy(
new MockMethodInvocation(new Sample(), Sample.class.getDeclaredMethod("mono")));
given(mockMethodInvocation.proceed()).willReturn(Mono.just("john"));
ReactiveAuthorizationManager<MethodInvocationResult> mockReactiveAuthorizationManager = mock(
ReactiveAuthorizationManager.class);
PostAuthorizeAuthorizationDecision decision = new PostAuthorizeAuthorizationDecision(false,
new LiteralExpression("1234"), new MaskingPostProcessor());
given(mockReactiveAuthorizationManager.check(any(), any())).willReturn(Mono.just(decision));
AuthorizationManagerAfterReactiveMethodInterceptor interceptor = new AuthorizationManagerAfterReactiveMethodInterceptor(
Pointcut.TRUE, mockReactiveAuthorizationManager);
Object result = interceptor.invoke(mockMethodInvocation);
assertThat(result).asInstanceOf(InstanceOfAssertFactories.type(Mono.class))
.extracting(Mono::block)
.isEqualTo("john-masked");
verify(mockReactiveAuthorizationManager).check(any(), any());
}
@Test
public void invokeMonoWhenPostProcessableDecisionAndPostProcessResultIsMonoThenPostProcessWorks() throws Throwable {
MethodInvocation mockMethodInvocation = spy(
new MockMethodInvocation(new Sample(), Sample.class.getDeclaredMethod("mono")));
given(mockMethodInvocation.proceed()).willReturn(Mono.just("john"));
ReactiveAuthorizationManager<MethodInvocationResult> mockReactiveAuthorizationManager = mock(
ReactiveAuthorizationManager.class);
PostAuthorizeAuthorizationDecision decision = new PostAuthorizeAuthorizationDecision(false,
new LiteralExpression("1234"), new MonoMaskingPostProcessor());
given(mockReactiveAuthorizationManager.check(any(), any())).willReturn(Mono.just(decision));
AuthorizationManagerAfterReactiveMethodInterceptor interceptor = new AuthorizationManagerAfterReactiveMethodInterceptor(
Pointcut.TRUE, mockReactiveAuthorizationManager);
Object result = interceptor.invoke(mockMethodInvocation);
assertThat(result).asInstanceOf(InstanceOfAssertFactories.type(Mono.class))
.extracting(Mono::block)
.isEqualTo("john-masked");
verify(mockReactiveAuthorizationManager).check(any(), any());
}
@Test
public void invokeMonoWhenPostProcessableDecisionAndPostProcessResultIsNullThenPostProcessWorks() throws Throwable {
MethodInvocation mockMethodInvocation = spy(
new MockMethodInvocation(new Sample(), Sample.class.getDeclaredMethod("mono")));
given(mockMethodInvocation.proceed()).willReturn(Mono.just("john"));
ReactiveAuthorizationManager<MethodInvocationResult> mockReactiveAuthorizationManager = mock(
ReactiveAuthorizationManager.class);
PostAuthorizeAuthorizationDecision decision = new PostAuthorizeAuthorizationDecision(false,
new LiteralExpression("1234"), new NullPostProcessor());
given(mockReactiveAuthorizationManager.check(any(), any())).willReturn(Mono.just(decision));
AuthorizationManagerAfterReactiveMethodInterceptor interceptor = new AuthorizationManagerAfterReactiveMethodInterceptor(
Pointcut.TRUE, mockReactiveAuthorizationManager);
Object result = interceptor.invoke(mockMethodInvocation);
assertThat(result).asInstanceOf(InstanceOfAssertFactories.type(Mono.class))
.extracting(Mono::block)
.isEqualTo(null);
verify(mockReactiveAuthorizationManager).check(any(), any());
}
@Test
public void invokeMonoWhenEmptyDecisionThenUseDefaultPostProcessor() throws Throwable {
MethodInvocation mockMethodInvocation = spy(
new MockMethodInvocation(new Sample(), Sample.class.getDeclaredMethod("mono")));
given(mockMethodInvocation.proceed()).willReturn(Mono.just("john"));
ReactiveAuthorizationManager<MethodInvocationResult> mockReactiveAuthorizationManager = mock(
ReactiveAuthorizationManager.class);
given(mockReactiveAuthorizationManager.check(any(), any())).willReturn(Mono.empty());
AuthorizationManagerAfterReactiveMethodInterceptor interceptor = new AuthorizationManagerAfterReactiveMethodInterceptor(
Pointcut.TRUE, mockReactiveAuthorizationManager);
Object result = interceptor.invoke(mockMethodInvocation);
assertThatExceptionOfType(AuthorizationDeniedException.class)
.isThrownBy(() -> assertThat(result).asInstanceOf(InstanceOfAssertFactories.type(Mono.class))
.extracting(Mono::block))
.withMessage("Access Denied");
verify(mockReactiveAuthorizationManager).check(any(), any());
}
private PostAuthorizeAuthorizationDecision createDecision(MethodAuthorizationDeniedPostProcessor postProcessor) {
return new PostAuthorizeAuthorizationDecision(false, new LiteralExpression("1234"), postProcessor);
}
static class MaskingPostProcessor implements MethodAuthorizationDeniedPostProcessor {
@Override
public Object postProcessResult(MethodInvocationResult contextObject, AuthorizationResult result) {
return contextObject.getResult() + "-masked";
}
}
static class MonoMaskingPostProcessor implements MethodAuthorizationDeniedPostProcessor {
@Override
public Object postProcessResult(MethodInvocationResult contextObject, AuthorizationResult result) {
return Mono.just(contextObject.getResult() + "-masked");
}
}
static class NullPostProcessor implements MethodAuthorizationDeniedPostProcessor {
@Override
public Object postProcessResult(MethodInvocationResult contextObject, AuthorizationResult result) {
return null;
}
}
class Sample {

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -23,8 +23,12 @@ import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.aop.Pointcut;
import org.springframework.expression.common.LiteralExpression;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.intercept.method.MockMethodInvocation;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationDeniedException;
import org.springframework.security.authorization.AuthorizationResult;
import org.springframework.security.authorization.ReactiveAuthorizationManager;
import static org.assertj.core.api.Assertions.assertThat;
@ -67,14 +71,15 @@ public class AuthorizationManagerBeforeReactiveMethodInterceptorTests {
given(mockMethodInvocation.proceed()).willReturn(Mono.just("john"));
ReactiveAuthorizationManager<MethodInvocation> mockReactiveAuthorizationManager = mock(
ReactiveAuthorizationManager.class);
given(mockReactiveAuthorizationManager.verify(any(), eq(mockMethodInvocation))).willReturn(Mono.empty());
given(mockReactiveAuthorizationManager.check(any(), eq(mockMethodInvocation)))
.willReturn(Mono.just(new AuthorizationDecision(true)));
AuthorizationManagerBeforeReactiveMethodInterceptor interceptor = new AuthorizationManagerBeforeReactiveMethodInterceptor(
Pointcut.TRUE, mockReactiveAuthorizationManager);
Object result = interceptor.invoke(mockMethodInvocation);
assertThat(result).asInstanceOf(InstanceOfAssertFactories.type(Mono.class))
.extracting(Mono::block)
.isEqualTo("john");
verify(mockReactiveAuthorizationManager).verify(any(), eq(mockMethodInvocation));
verify(mockReactiveAuthorizationManager).check(any(), eq(mockMethodInvocation));
}
@Test
@ -84,7 +89,8 @@ public class AuthorizationManagerBeforeReactiveMethodInterceptorTests {
given(mockMethodInvocation.proceed()).willReturn(Flux.just("john", "bob"));
ReactiveAuthorizationManager<MethodInvocation> mockReactiveAuthorizationManager = mock(
ReactiveAuthorizationManager.class);
given(mockReactiveAuthorizationManager.verify(any(), eq(mockMethodInvocation))).willReturn(Mono.empty());
given(mockReactiveAuthorizationManager.check(any(), eq(mockMethodInvocation)))
.willReturn(Mono.just(new AuthorizationDecision((true))));
AuthorizationManagerBeforeReactiveMethodInterceptor interceptor = new AuthorizationManagerBeforeReactiveMethodInterceptor(
Pointcut.TRUE, mockReactiveAuthorizationManager);
Object result = interceptor.invoke(mockMethodInvocation);
@ -92,7 +98,7 @@ public class AuthorizationManagerBeforeReactiveMethodInterceptorTests {
.extracting(Flux::collectList)
.extracting(Mono::block, InstanceOfAssertFactories.list(String.class))
.containsExactly("john", "bob");
verify(mockReactiveAuthorizationManager).verify(any(), eq(mockMethodInvocation));
verify(mockReactiveAuthorizationManager).check(any(), eq(mockMethodInvocation));
}
@Test
@ -102,8 +108,8 @@ public class AuthorizationManagerBeforeReactiveMethodInterceptorTests {
given(mockMethodInvocation.proceed()).willReturn(Mono.just("john"));
ReactiveAuthorizationManager<MethodInvocation> mockReactiveAuthorizationManager = mock(
ReactiveAuthorizationManager.class);
given(mockReactiveAuthorizationManager.verify(any(), eq(mockMethodInvocation)))
.willReturn(Mono.error(new AccessDeniedException("Access Denied")));
given(mockReactiveAuthorizationManager.check(any(), eq(mockMethodInvocation)))
.willReturn(Mono.just(new AuthorizationDecision(false)));
AuthorizationManagerBeforeReactiveMethodInterceptor interceptor = new AuthorizationManagerBeforeReactiveMethodInterceptor(
Pointcut.TRUE, mockReactiveAuthorizationManager);
Object result = interceptor.invoke(mockMethodInvocation);
@ -111,7 +117,119 @@ public class AuthorizationManagerBeforeReactiveMethodInterceptorTests {
.isThrownBy(() -> assertThat(result).asInstanceOf(InstanceOfAssertFactories.type(Mono.class))
.extracting(Mono::block))
.withMessage("Access Denied");
verify(mockReactiveAuthorizationManager).verify(any(), eq(mockMethodInvocation));
verify(mockReactiveAuthorizationManager).check(any(), eq(mockMethodInvocation));
}
@Test
public void invokeMonoWhenDeniedAndPostProcessorThenInvokePostProcessor() throws Throwable {
MethodInvocation mockMethodInvocation = spy(
new MockMethodInvocation(new Sample(), Sample.class.getDeclaredMethod("mono")));
given(mockMethodInvocation.proceed()).willReturn(Mono.just("john"));
ReactiveAuthorizationManager<MethodInvocation> mockReactiveAuthorizationManager = mock(
ReactiveAuthorizationManager.class);
PreAuthorizeAuthorizationDecision decision = new PreAuthorizeAuthorizationDecision(false,
new LiteralExpression("1234"), new MaskingPostProcessor());
given(mockReactiveAuthorizationManager.check(any(), eq(mockMethodInvocation))).willReturn(Mono.just(decision));
AuthorizationManagerBeforeReactiveMethodInterceptor interceptor = new AuthorizationManagerBeforeReactiveMethodInterceptor(
Pointcut.TRUE, mockReactiveAuthorizationManager);
Object result = interceptor.invoke(mockMethodInvocation);
assertThat(result).asInstanceOf(InstanceOfAssertFactories.type(Mono.class))
.extracting(Mono::block)
.isEqualTo("***");
verify(mockReactiveAuthorizationManager).check(any(), eq(mockMethodInvocation));
}
@Test
public void invokeMonoWhenDeniedAndMonoPostProcessorThenInvokePostProcessor() throws Throwable {
MethodInvocation mockMethodInvocation = spy(
new MockMethodInvocation(new Sample(), Sample.class.getDeclaredMethod("mono")));
given(mockMethodInvocation.proceed()).willReturn(Mono.just("john"));
ReactiveAuthorizationManager<MethodInvocation> mockReactiveAuthorizationManager = mock(
ReactiveAuthorizationManager.class);
PreAuthorizeAuthorizationDecision decision = new PreAuthorizeAuthorizationDecision(false,
new LiteralExpression("1234"), new MonoMaskingPostProcessor());
given(mockReactiveAuthorizationManager.check(any(), eq(mockMethodInvocation))).willReturn(Mono.just(decision));
AuthorizationManagerBeforeReactiveMethodInterceptor interceptor = new AuthorizationManagerBeforeReactiveMethodInterceptor(
Pointcut.TRUE, mockReactiveAuthorizationManager);
Object result = interceptor.invoke(mockMethodInvocation);
assertThat(result).asInstanceOf(InstanceOfAssertFactories.type(Mono.class))
.extracting(Mono::block)
.isEqualTo("***");
verify(mockReactiveAuthorizationManager).check(any(), eq(mockMethodInvocation));
}
@Test
public void invokeFluxWhenDeniedAndPostProcessorThenInvokePostProcessor() throws Throwable {
MethodInvocation mockMethodInvocation = spy(
new MockMethodInvocation(new Sample(), Sample.class.getDeclaredMethod("flux")));
given(mockMethodInvocation.proceed()).willReturn(Flux.just("john", "bob"));
ReactiveAuthorizationManager<MethodInvocation> mockReactiveAuthorizationManager = mock(
ReactiveAuthorizationManager.class);
PreAuthorizeAuthorizationDecision decision = new PreAuthorizeAuthorizationDecision(false,
new LiteralExpression("1234"), new MonoMaskingPostProcessor());
given(mockReactiveAuthorizationManager.check(any(), eq(mockMethodInvocation))).willReturn(Mono.just(decision));
AuthorizationManagerBeforeReactiveMethodInterceptor interceptor = new AuthorizationManagerBeforeReactiveMethodInterceptor(
Pointcut.TRUE, mockReactiveAuthorizationManager);
Object result = interceptor.invoke(mockMethodInvocation);
assertThat(result).asInstanceOf(InstanceOfAssertFactories.type(Flux.class))
.extracting(Flux::collectList)
.extracting(Mono::block, InstanceOfAssertFactories.list(String.class))
.containsExactly("***");
verify(mockReactiveAuthorizationManager).check(any(), eq(mockMethodInvocation));
}
@Test
public void invokeMonoWhenEmptyDecisionThenInvokeDefaultPostProcessor() throws Throwable {
MethodInvocation mockMethodInvocation = spy(
new MockMethodInvocation(new Sample(), Sample.class.getDeclaredMethod("mono")));
given(mockMethodInvocation.proceed()).willReturn(Mono.just("john"));
ReactiveAuthorizationManager<MethodInvocation> mockReactiveAuthorizationManager = mock(
ReactiveAuthorizationManager.class);
given(mockReactiveAuthorizationManager.check(any(), eq(mockMethodInvocation))).willReturn(Mono.empty());
AuthorizationManagerBeforeReactiveMethodInterceptor interceptor = new AuthorizationManagerBeforeReactiveMethodInterceptor(
Pointcut.TRUE, mockReactiveAuthorizationManager);
Object result = interceptor.invoke(mockMethodInvocation);
assertThatExceptionOfType(AuthorizationDeniedException.class)
.isThrownBy(() -> assertThat(result).asInstanceOf(InstanceOfAssertFactories.type(Mono.class))
.extracting(Mono::block))
.withMessage("Access Denied");
verify(mockReactiveAuthorizationManager).check(any(), eq(mockMethodInvocation));
}
@Test
public void invokeFluxWhenEmptyDecisionThenInvokeDefaultPostProcessor() throws Throwable {
MethodInvocation mockMethodInvocation = spy(
new MockMethodInvocation(new Sample(), Sample.class.getDeclaredMethod("flux")));
given(mockMethodInvocation.proceed()).willReturn(Flux.just("john", "bob"));
ReactiveAuthorizationManager<MethodInvocation> mockReactiveAuthorizationManager = mock(
ReactiveAuthorizationManager.class);
given(mockReactiveAuthorizationManager.check(any(), eq(mockMethodInvocation))).willReturn(Mono.empty());
AuthorizationManagerBeforeReactiveMethodInterceptor interceptor = new AuthorizationManagerBeforeReactiveMethodInterceptor(
Pointcut.TRUE, mockReactiveAuthorizationManager);
Object result = interceptor.invoke(mockMethodInvocation);
assertThatExceptionOfType(AuthorizationDeniedException.class)
.isThrownBy(() -> assertThat(result).asInstanceOf(InstanceOfAssertFactories.type(Flux.class))
.extracting(Flux::blockFirst))
.withMessage("Access Denied");
verify(mockReactiveAuthorizationManager).check(any(), eq(mockMethodInvocation));
}
static class MaskingPostProcessor implements MethodAuthorizationDeniedHandler {
@Override
public Object handle(MethodInvocation methodInvocation, AuthorizationResult result) {
return "***";
}
}
static class MonoMaskingPostProcessor implements MethodAuthorizationDeniedHandler {
@Override
public Object handle(MethodInvocation methodInvocation, AuthorizationResult result) {
return Mono.just("***");
}
}
class Sample {

View File

@ -44,6 +44,7 @@ Consider learning about the following use cases:
* Understanding <<method-security-architecture,how method security works>> and reasons to use it
* Comparing <<request-vs-method,request-level and method-level authorization>>
* Authorizing methods with <<use-preauthorize,`@PreAuthorize`>> and <<use-postauthorize,`@PostAuthorize`>>
* Providing <<fallback-values-authorization-denied,fallback values when authorization is denied>>
* Filtering methods with <<use-prefilter,`@PreFilter`>> and <<use-postfilter,`@PostFilter`>>
* Authorizing methods with <<use-jsr250,JSR-250 annotations>>
* Authorizing methods with <<use-aspectj,AspectJ expressions>>
@ -2208,6 +2209,459 @@ And if they do have that authority, they'll see:
You can also add the Spring Boot property `spring.jackson.default-property-inclusion=non_null` to exclude the null value, if you also don't want to reveal the JSON key to an unauthorized user.
====
[[fallback-values-authorization-denied]]
== Providing Fallback Values When Authorization is Denied
There are some scenarios where you may not wish to throw an `AccessDeniedException` when a method is invoked without the required permissions.
Instead, you might wish to return a post-processed result, like a masked result, or a default value in cases where access denied happened before invoking the method.
Spring Security provides support for handling and post-processing method access denied with the <<authorizing-with-annotations,`@PreAuthorize` and `@PostAuthorize` annotations>> respectively.
The `@PreAuthorize` annotation works with implementations of `MethodAuthorizationDeniedHandler` while the `@PostAuthorize` annotation works with implementations of `MethodAuthorizationDeniedPostProcessor`.
=== Using with `@PreAuthorize`
Let's consider the example from the <<authorize-object,previous section>>, but instead of creating the `AccessDeniedExceptionInterceptor` to transform an `AccessDeniedException` to a `null` return value, we will use the `handlerClass` attribute from `@PreAuthorize`:
[tabs]
======
Java::
+
[source,java,role="primary"]
----
public class NullMethodAuthorizationDeniedHandler implements MethodAuthorizationDeniedHandler { <1>
@Override
public Object handle(MethodInvocation methodInvocation, AuthorizationResult authorizationResult) {
return null;
}
}
@Configuration
@EnableMethodSecurity
public class SecurityConfig {
@Bean <2>
public NullMethodAuthorizationDeniedHandler nullMethodAuthorizationDeniedHandler() {
return new NullMethodAuthorizationDeniedHandler();
}
}
public class User {
// ...
@PreAuthorize(value = "hasAuthority('user:read')", handlerClass = NullMethodAuthorizationDeniedHandler.class)
public String getEmail() {
return this.email;
}
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
class NullMethodAuthorizationDeniedHandler : MethodAuthorizationDeniedHandler { <1>
override fun handle(methodInvocation: MethodInvocation, authorizationResult: AuthorizationResult): Any {
return null
}
}
@Configuration
@EnableMethodSecurity
class SecurityConfig {
@Bean <2>
fun nullMethodAuthorizationDeniedHandler(): NullMethodAuthorizationDeniedHandler {
return MaskMethodAuthorizationDeniedHandler()
}
}
class User (val name:String, @get:PreAuthorize(value = "hasAuthority('user:read')", handlerClass = NullMethodAuthorizationDeniedHandler::class) val email:String) <3>
----
======
<1> Create an implementation of `MethodAuthorizationDeniedHandler` that returns a `null` value
<2> Register the `NullMethodAuthorizationDeniedHandler` as a bean
<3> Pass the `NullMethodAuthorizationDeniedHandler` to the `handlerClass` attribute of `@PreAuthorize`
And then you can verify that a `null` value is returned instead of the `AccessDeniedException`:
[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Autowired
UserRepository users;
@Test
void getEmailWhenProxiedThenNullEmail() {
Optional<User> securedUser = users.findByName("name");
assertThat(securedUser.get().getEmail()).isNull();
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Autowired
var users:UserRepository? = null
@Test
fun getEmailWhenProxiedThenNullEmail() {
val securedUser: Optional<User> = users.findByName("name")
assertThat(securedUser.get().getEmail()).isNull()
}
----
======
=== Using with `@PostAuthorize`
The same can be achieved with `@PostAuthorize`, however, since `@PostAuthorize` checks are performed after the method is invoked, we have access to the resulting value of the invocation, allowing you to provide fallback values based on the unauthorized results.
Let's continue with the previous example, but instead of returning `null`, we will return a masked value of the email:
[tabs]
======
Java::
+
[source,java,role="primary"]
----
public class EmailMaskingMethodAuthorizationDeniedPostProcessor implements MethodAuthorizationDeniedPostProcessor { <1>
@Override
public Object postProcessResult(MethodInvocationResult methodInvocationResult, AuthorizationResult authorizationResult) {
String email = (String) methodInvocationResult.getResult();
return email.replaceAll("(^[^@]{3}|(?!^)\\G)[^@]", "$1*");
}
}
@Configuration
@EnableMethodSecurity
public class SecurityConfig {
@Bean <2>
public EmailMaskingMethodAuthorizationDeniedPostProcessor emailMaskingMethodAuthorizationDeniedPostProcessor() {
return new EmailMaskingMethodAuthorizationDeniedPostProcessor();
}
}
public class User {
// ...
@PostAuthorize(value = "hasAuthority('user:read')", postProcessorClass = EmailMaskingMethodAuthorizationDeniedPostProcessor.class)
public String getEmail() {
return this.email;
}
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
class EmailMaskingMethodAuthorizationDeniedPostProcessor : MethodAuthorizationDeniedPostProcessor {
override fun postProcessResult(methodInvocationResult: MethodInvocationResult, authorizationResult: AuthorizationResult): Any {
val email = methodInvocationResult.result as String
return email.replace("(^[^@]{3}|(?!^)\\G)[^@]".toRegex(), "$1*")
}
}
@Configuration
@EnableMethodSecurity
class SecurityConfig {
@Bean
fun emailMaskingMethodAuthorizationDeniedPostProcessor(): EmailMaskingMethodAuthorizationDeniedPostProcessor {
return EmailMaskingMethodAuthorizationDeniedPostProcessor()
}
}
class User (val name:String, @PostAuthorize(value = "hasAuthority('user:read')", postProcessorClass = EmailMaskingMethodAuthorizationDeniedPostProcessor::class) val email:String) <3>
----
======
<1> Create an implementation of `MethodAuthorizationDeniedPostProcessor` that returns a masked value of the unauthorized result value
<2> Register the `EmailMaskingMethodAuthorizationDeniedPostProcessor` as a bean
<3> Pass the `EmailMaskingMethodAuthorizationDeniedPostProcessor` to the `postProcessorClass` attribute of `@PostAuthorize`
And then you can verify that a masked email is returned instead of an `AccessDeniedException`:
[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Autowired
UserRepository users;
@Test
void getEmailWhenProxiedThenMaskedEmail() {
Optional<User> securedUser = users.findByName("name");
// email is useremail@example.com
assertThat(securedUser.get().getEmail()).isEqualTo("use******@example.com");
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Autowired
var users:UserRepository? = null
@Test
fun getEmailWhenProxiedThenMaskedEmail() {
val securedUser: Optional<User> = users.findByName("name")
// email is useremail@example.com
assertThat(securedUser.get().getEmail()).isEqualTo("use******@example.com")
}
----
======
When implementing the `MethodAuthorizationDeniedHandler` or the `MethodAuthorizationDeniedPostProcessor` you have a few options on what you can return:
- A `null` value.
- A non-null value, respecting the method's return type.
- Throw an exception, usually an instance of `AccessDeniedException`. This is the default behavior.
- A `Mono` type for reactive applications.
Note that since the handler and the post-processor must be registered as beans, you can inject dependencies into them if you need a more complex logic.
In addition to that, you have available the `MethodInvocation` or the `MethodInvocationResult`, as well as the `AuthorizationResult` for more details related to the authorization decision.
=== Deciding What to Return Based on Available Parameters
Consider a scenario where there might multiple mask values for different methods, it would be not so productive if we had to create a handler or post-processor for each of those methods, although it is perfectly fine to do that.
In such cases, we can use the information passed via parameters to decide what to do.
For example, we can create a custom `@Mask` annotation and a handler that detects that annotation to decide what mask value to return:
[tabs]
======
Java::
+
[source,java,role="primary"]
----
import org.springframework.core.annotation.AnnotationUtils;
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface Mask {
String value();
}
public class MaskAnnotationDeniedHandler implements MethodAuthorizationDeniedHandler {
@Override
public Object handle(MethodInvocation methodInvocation, AuthorizationResult authorizationResult) {
Mask mask = AnnotationUtils.getAnnotation(methodInvocation.getMethod(), Mask.class);
return mask.value();
}
}
@Configuration
@EnableMethodSecurity
public class SecurityConfig {
@Bean
public MaskAnnotationDeniedHandler maskAnnotationDeniedHandler() {
return new MaskAnnotationDeniedHandler();
}
}
@Component
public class MyService {
@PreAuthorize(value = "hasAuthority('user:read')", handlerClass = MaskAnnotationDeniedHandler.class)
@Mask("***")
public String foo() {
return "foo";
}
@PreAuthorize(value = "hasAuthority('user:read')", handlerClass = MaskAnnotationDeniedHandler.class)
@Mask("???")
public String bar() {
return "bar";
}
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
import org.springframework.core.annotation.AnnotationUtils
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class Mask(val value: String)
class MaskAnnotationDeniedHandler : MethodAuthorizationDeniedHandler {
override fun handle(methodInvocation: MethodInvocation, authorizationResult: AuthorizationResult): Any {
val mask = AnnotationUtils.getAnnotation(methodInvocation.method, Mask::class.java)
return mask.value
}
}
@Configuration
@EnableMethodSecurity
class SecurityConfig {
@Bean
fun maskAnnotationDeniedHandler(): MaskAnnotationDeniedHandler {
return MaskAnnotationDeniedHandler()
}
}
@Component
class MyService {
@PreAuthorize(value = "hasAuthority('user:read')", handlerClass = MaskAnnotationDeniedHandler::class)
@Mask("***")
fun foo(): String {
return "foo"
}
@PreAuthorize(value = "hasAuthority('user:read')", handlerClass = MaskAnnotationDeniedHandler::class)
@Mask("???")
fun bar(): String {
return "bar"
}
}
----
======
Now the return values when access is denied will be decided based on the `@Mask` annotation:
[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Autowired
MyService myService;
@Test
void fooWhenDeniedThenReturnStars() {
String value = this.myService.foo();
assertThat(value).isEqualTo("***");
}
@Test
void barWhenDeniedThenReturnQuestionMarks() {
String value = this.myService.foo();
assertThat(value).isEqualTo("???");
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Autowired
var myService: MyService
@Test
fun fooWhenDeniedThenReturnStars() {
val value: String = myService.foo()
assertThat(value).isEqualTo("***")
}
@Test
fun barWhenDeniedThenReturnQuestionMarks() {
val value: String = myService.foo()
assertThat(value).isEqualTo("???")
}
----
======
=== Combining with Meta Annotation Support
Some authorization expressions may be long enough that it can become hard to read or to maintain.
For example, consider the following `@PreAuthorize` expression:
[tabs]
======
Java::
+
[source,java,role="primary"]
----
@PreAuthorize(value = "@myAuthorizationBean.check()", handlerClass = NullAuthorizationDeniedHandler.class)
public String myMethod() {
// ...
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@PreAuthorize(value = "@myAuthorizationBean.check()", handlerClass = NullAuthorizationDeniedHandler::class)
fun myMethod(): String {
// ...
}
----
======
The way it is, it is somewhat hard to read it, but we can do better.
By using the <<meta-annotations,meta annotation support>>, we can simplify it to:
[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize(value = "@myAuthorizationBean.check()", handlerClass = NullAuthorizationDeniedHandler.class)
public @interface NullDenied {}
@NullDenied
public String myMethod() {
// ...
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@PreAuthorize(value = "@myAuthorizationBean.check()", handlerClass = NullAuthorizationDeniedHandler::class)
annotation class NullDenied
@NullDenied
fun myMethod(): String {
// ...
}
----
======
Make sure to read the <<meta-annotations,Meta Annotations Support>> section for more details on the usage.
[[migration-enableglobalmethodsecurity]]
== Migrating from `@EnableGlobalMethodSecurity`

View File

@ -16,6 +16,7 @@ Below are the highlights of the release.
- https://github.com/spring-projects/spring-security/issues/14596[gh-14596] - xref:servlet/authorization/method-security.adoc[docs] - Add Programmatic Proxy Support for Method Security
- https://github.com/spring-projects/spring-security/issues/14597[gh-14597] - xref:servlet/authorization/method-security.adoc[docs] - Add Securing of Return Values
- https://github.com/spring-projects/spring-security/issues/14601[gh-14601] - xref:servlet/authorization/method-security.adoc#fallback-values-authorization-denied[docs] - Add Authorization Denied Handlers for Method Security
== Configuration