Add Authorization Denied Handlers for Method Security
Closes gh-14601
This commit is contained in:
parent
19d66c0b8a
commit
d85857f905
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
|
||||
}
|
|
@ -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*");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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`
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in New Issue