mirror of
https://github.com/spring-projects/spring-security.git
synced 2025-06-01 09:42:13 +00:00
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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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,
|
ObjectProvider<ObservationRegistry> registryProvider, ObjectProvider<RoleHierarchy> roleHierarchyProvider,
|
||||||
PrePostMethodSecurityConfiguration configuration, ApplicationContext context) {
|
PrePostMethodSecurityConfiguration configuration, ApplicationContext context) {
|
||||||
PreAuthorizeAuthorizationManager manager = new PreAuthorizeAuthorizationManager();
|
PreAuthorizeAuthorizationManager manager = new PreAuthorizeAuthorizationManager();
|
||||||
|
manager.setApplicationContext(context);
|
||||||
AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor
|
AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor
|
||||||
.preAuthorize(manager(manager, registryProvider));
|
.preAuthorize(manager(manager, registryProvider));
|
||||||
preAuthorize.setOrder(preAuthorize.getOrder() + configuration.interceptorOrderOffset);
|
preAuthorize.setOrder(preAuthorize.getOrder() + configuration.interceptorOrderOffset);
|
||||||
@ -121,6 +122,7 @@ final class PrePostMethodSecurityConfiguration implements ImportAware, AopInfras
|
|||||||
ObjectProvider<ObservationRegistry> registryProvider, ObjectProvider<RoleHierarchy> roleHierarchyProvider,
|
ObjectProvider<ObservationRegistry> registryProvider, ObjectProvider<RoleHierarchy> roleHierarchyProvider,
|
||||||
PrePostMethodSecurityConfiguration configuration, ApplicationContext context) {
|
PrePostMethodSecurityConfiguration configuration, ApplicationContext context) {
|
||||||
PostAuthorizeAuthorizationManager manager = new PostAuthorizeAuthorizationManager();
|
PostAuthorizeAuthorizationManager manager = new PostAuthorizeAuthorizationManager();
|
||||||
|
manager.setApplicationContext(context);
|
||||||
AuthorizationManagerAfterMethodInterceptor postAuthorize = AuthorizationManagerAfterMethodInterceptor
|
AuthorizationManagerAfterMethodInterceptor postAuthorize = AuthorizationManagerAfterMethodInterceptor
|
||||||
.postAuthorize(manager(manager, registryProvider));
|
.postAuthorize(manager(manager, registryProvider));
|
||||||
postAuthorize.setOrder(postAuthorize.getOrder() + configuration.interceptorOrderOffset);
|
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.ObjectProvider;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.beans.factory.config.BeanDefinition;
|
import org.springframework.beans.factory.config.BeanDefinition;
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.context.annotation.Role;
|
import org.springframework.context.annotation.Role;
|
||||||
@ -74,9 +75,10 @@ final class ReactiveAuthorizationManagerMethodSecurityConfiguration implements A
|
|||||||
static MethodInterceptor preAuthorizeAuthorizationMethodInterceptor(
|
static MethodInterceptor preAuthorizeAuthorizationMethodInterceptor(
|
||||||
MethodSecurityExpressionHandler expressionHandler,
|
MethodSecurityExpressionHandler expressionHandler,
|
||||||
ObjectProvider<PrePostTemplateDefaults> defaultsObjectProvider,
|
ObjectProvider<PrePostTemplateDefaults> defaultsObjectProvider,
|
||||||
ObjectProvider<ObservationRegistry> registryProvider) {
|
ObjectProvider<ObservationRegistry> registryProvider, ApplicationContext context) {
|
||||||
PreAuthorizeReactiveAuthorizationManager manager = new PreAuthorizeReactiveAuthorizationManager(
|
PreAuthorizeReactiveAuthorizationManager manager = new PreAuthorizeReactiveAuthorizationManager(
|
||||||
expressionHandler);
|
expressionHandler);
|
||||||
|
manager.setApplicationContext(context);
|
||||||
ReactiveAuthorizationManager<MethodInvocation> authorizationManager = manager(manager, registryProvider);
|
ReactiveAuthorizationManager<MethodInvocation> authorizationManager = manager(manager, registryProvider);
|
||||||
AuthorizationAdvisor interceptor = AuthorizationManagerBeforeReactiveMethodInterceptor
|
AuthorizationAdvisor interceptor = AuthorizationManagerBeforeReactiveMethodInterceptor
|
||||||
.preAuthorize(authorizationManager);
|
.preAuthorize(authorizationManager);
|
||||||
@ -99,9 +101,10 @@ final class ReactiveAuthorizationManagerMethodSecurityConfiguration implements A
|
|||||||
static MethodInterceptor postAuthorizeAuthorizationMethodInterceptor(
|
static MethodInterceptor postAuthorizeAuthorizationMethodInterceptor(
|
||||||
MethodSecurityExpressionHandler expressionHandler,
|
MethodSecurityExpressionHandler expressionHandler,
|
||||||
ObjectProvider<PrePostTemplateDefaults> defaultsObjectProvider,
|
ObjectProvider<PrePostTemplateDefaults> defaultsObjectProvider,
|
||||||
ObjectProvider<ObservationRegistry> registryProvider) {
|
ObjectProvider<ObservationRegistry> registryProvider, ApplicationContext context) {
|
||||||
PostAuthorizeReactiveAuthorizationManager manager = new PostAuthorizeReactiveAuthorizationManager(
|
PostAuthorizeReactiveAuthorizationManager manager = new PostAuthorizeReactiveAuthorizationManager(
|
||||||
expressionHandler);
|
expressionHandler);
|
||||||
|
manager.setApplicationContext(context);
|
||||||
ReactiveAuthorizationManager<MethodInvocationResult> authorizationManager = manager(manager, registryProvider);
|
ReactiveAuthorizationManager<MethodInvocationResult> authorizationManager = manager(manager, registryProvider);
|
||||||
AuthorizationAdvisor interceptor = AuthorizationManagerAfterReactiveMethodInterceptor
|
AuthorizationAdvisor interceptor = AuthorizationManagerAfterReactiveMethodInterceptor
|
||||||
.postAuthorize(authorizationManager);
|
.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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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;
|
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 java.util.List;
|
||||||
|
|
||||||
import jakarta.annotation.security.DenyAll;
|
import jakarta.annotation.security.DenyAll;
|
||||||
import jakarta.annotation.security.PermitAll;
|
import jakarta.annotation.security.PermitAll;
|
||||||
import jakarta.annotation.security.RolesAllowed;
|
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.annotation.Secured;
|
||||||
|
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
|
||||||
import org.springframework.security.access.prepost.PostAuthorize;
|
import org.springframework.security.access.prepost.PostAuthorize;
|
||||||
import org.springframework.security.access.prepost.PostFilter;
|
import org.springframework.security.access.prepost.PostFilter;
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
import org.springframework.security.access.prepost.PreFilter;
|
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.Authentication;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
import org.springframework.security.core.parameters.P;
|
import org.springframework.security.core.parameters.P;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Rob Winch
|
* @author Rob Winch
|
||||||
*/
|
*/
|
||||||
|
@MethodSecurityService.Mask("classmask")
|
||||||
public interface MethodSecurityService {
|
public interface MethodSecurityService {
|
||||||
|
|
||||||
@PreAuthorize("denyAll")
|
@PreAuthorize("denyAll")
|
||||||
@ -108,4 +127,196 @@ public interface MethodSecurityService {
|
|||||||
@RequireAdminRole
|
@RequireAdminRole
|
||||||
void repeatedAnnotations();
|
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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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 java.util.List;
|
||||||
|
|
||||||
|
import org.springframework.security.access.AccessDeniedException;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
|
||||||
@ -126,4 +127,74 @@ public class MethodSecurityServiceImpl implements MethodSecurityService {
|
|||||||
public void repeatedAnnotations() {
|
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() {
|
private static Consumer<ConfigurableWebApplicationContext> disallowBeanOverriding() {
|
||||||
return (context) -> ((AnnotationConfigWebApplicationContext) context).setAllowBeanDefinitionOverriding(false);
|
return (context) -> ((AnnotationConfigWebApplicationContext) context).setAllowBeanDefinitionOverriding(false);
|
||||||
}
|
}
|
||||||
@ -756,6 +938,16 @@ public class PrePostMethodSecurityConfigurationTests {
|
|||||||
return advisor;
|
return advisor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
static class AuthzConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
Authz authz() {
|
||||||
|
return new Authz();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableCustomMethodSecurity
|
@EnableCustomMethodSecurity
|
||||||
static class CustomMethodSecurityServiceConfig {
|
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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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.RetentionPolicy;
|
||||||
import java.lang.annotation.Target;
|
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
|
* Annotation for specifying a method access-control expression which will be evaluated
|
||||||
* after a method has been invoked.
|
* after a method has been invoked.
|
||||||
@ -42,4 +45,10 @@ public @interface PostAuthorize {
|
|||||||
*/
|
*/
|
||||||
String value();
|
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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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.RetentionPolicy;
|
||||||
import java.lang.annotation.Target;
|
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
|
* Annotation for specifying a method access-control expression which will be evaluated to
|
||||||
* decide whether a method invocation is allowed or not.
|
* decide whether a method invocation is allowed or not.
|
||||||
@ -42,4 +45,10 @@ public @interface PreAuthorize {
|
|||||||
*/
|
*/
|
||||||
String value();
|
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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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 AuthorizationManager<MethodInvocationResult> authorizationManager;
|
||||||
|
|
||||||
|
private final MethodAuthorizationDeniedPostProcessor defaultPostProcessor = new ThrowingMethodAuthorizationDeniedPostProcessor();
|
||||||
|
|
||||||
private int order;
|
private int order;
|
||||||
|
|
||||||
private AuthorizationEventPublisher eventPublisher = AuthorizationManagerAfterMethodInterceptor::noPublish;
|
private AuthorizationEventPublisher eventPublisher = AuthorizationManagerAfterMethodInterceptor::noPublish;
|
||||||
@ -116,8 +118,7 @@ public final class AuthorizationManagerAfterMethodInterceptor implements Authori
|
|||||||
@Override
|
@Override
|
||||||
public Object invoke(MethodInvocation mi) throws Throwable {
|
public Object invoke(MethodInvocation mi) throws Throwable {
|
||||||
Object result = mi.proceed();
|
Object result = mi.proceed();
|
||||||
attemptAuthorization(mi, result);
|
return attemptAuthorization(mi, result);
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -168,7 +169,7 @@ public final class AuthorizationManagerAfterMethodInterceptor implements Authori
|
|||||||
this.securityContextHolderStrategy = () -> strategy;
|
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));
|
this.logger.debug(LogMessage.of(() -> "Authorizing method invocation " + mi));
|
||||||
MethodInvocationResult object = new MethodInvocationResult(mi, result);
|
MethodInvocationResult object = new MethodInvocationResult(mi, result);
|
||||||
AuthorizationDecision decision = this.authorizationManager.check(this::getAuthentication, object);
|
AuthorizationDecision decision = this.authorizationManager.check(this::getAuthentication, object);
|
||||||
@ -176,9 +177,17 @@ public final class AuthorizationManagerAfterMethodInterceptor implements Authori
|
|||||||
if (decision != null && !decision.isGranted()) {
|
if (decision != null && !decision.isGranted()) {
|
||||||
this.logger.debug(LogMessage.of(() -> "Failed to authorize " + mi + " with authorization manager "
|
this.logger.debug(LogMessage.of(() -> "Failed to authorize " + mi + " with authorization manager "
|
||||||
+ this.authorizationManager + " and decision " + decision));
|
+ this.authorizationManager + " and decision " + decision));
|
||||||
throw new AccessDeniedException("Access Denied");
|
return postProcess(object, decision);
|
||||||
}
|
}
|
||||||
this.logger.debug(LogMessage.of(() -> "Authorized method invocation " + mi));
|
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() {
|
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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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.ReactiveAdapter;
|
||||||
import org.springframework.core.ReactiveAdapterRegistry;
|
import org.springframework.core.ReactiveAdapterRegistry;
|
||||||
import org.springframework.security.access.prepost.PostAuthorize;
|
import org.springframework.security.access.prepost.PostAuthorize;
|
||||||
|
import org.springframework.security.authorization.AuthorizationDecision;
|
||||||
import org.springframework.security.authorization.ReactiveAuthorizationManager;
|
import org.springframework.security.authorization.ReactiveAuthorizationManager;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
@ -57,6 +58,8 @@ public final class AuthorizationManagerAfterReactiveMethodInterceptor implements
|
|||||||
|
|
||||||
private int order = AuthorizationInterceptorsOrder.LAST.getOrder();
|
private int order = AuthorizationInterceptorsOrder.LAST.getOrder();
|
||||||
|
|
||||||
|
private final MethodAuthorizationDeniedPostProcessor defaultPostProcessor = new ThrowingMethodAuthorizationDeniedPostProcessor();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an instance for the {@link PostAuthorize} annotation.
|
* Creates an instance for the {@link PostAuthorize} annotation.
|
||||||
* @return the {@link AuthorizationManagerAfterReactiveMethodInterceptor} to use
|
* @return the {@link AuthorizationManagerAfterReactiveMethodInterceptor} to use
|
||||||
@ -144,9 +147,28 @@ public final class AuthorizationManagerAfterReactiveMethodInterceptor implements
|
|||||||
return adapter != null && adapter.isMultiValue();
|
return adapter != null && adapter.isMultiValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Mono<?> postAuthorize(Mono<Authentication> authentication, MethodInvocation mi, Object result) {
|
private Mono<Object> postAuthorize(Mono<Authentication> authentication, MethodInvocation mi, Object result) {
|
||||||
return this.authorizationManager.verify(authentication, new MethodInvocationResult(mi, result))
|
MethodInvocationResult invocationResult = new MethodInvocationResult(mi, result);
|
||||||
.thenReturn(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
|
@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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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 AuthorizationManager<MethodInvocation> authorizationManager;
|
||||||
|
|
||||||
|
private final MethodAuthorizationDeniedHandler defaultHandler = new ThrowingMethodAuthorizationDeniedHandler();
|
||||||
|
|
||||||
private int order = AuthorizationInterceptorsOrder.FIRST.getOrder();
|
private int order = AuthorizationInterceptorsOrder.FIRST.getOrder();
|
||||||
|
|
||||||
private AuthorizationEventPublisher eventPublisher = AuthorizationManagerBeforeMethodInterceptor::noPublish;
|
private AuthorizationEventPublisher eventPublisher = AuthorizationManagerBeforeMethodInterceptor::noPublish;
|
||||||
@ -190,8 +192,7 @@ public final class AuthorizationManagerBeforeMethodInterceptor implements Author
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public Object invoke(MethodInvocation mi) throws Throwable {
|
public Object invoke(MethodInvocation mi) throws Throwable {
|
||||||
attemptAuthorization(mi);
|
return attemptAuthorization(mi);
|
||||||
return mi.proceed();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -242,16 +243,24 @@ public final class AuthorizationManagerBeforeMethodInterceptor implements Author
|
|||||||
this.securityContextHolderStrategy = () -> securityContextHolderStrategy;
|
this.securityContextHolderStrategy = () -> securityContextHolderStrategy;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void attemptAuthorization(MethodInvocation mi) {
|
private Object attemptAuthorization(MethodInvocation mi) throws Throwable {
|
||||||
this.logger.debug(LogMessage.of(() -> "Authorizing method invocation " + mi));
|
this.logger.debug(LogMessage.of(() -> "Authorizing method invocation " + mi));
|
||||||
AuthorizationDecision decision = this.authorizationManager.check(this::getAuthentication, mi);
|
AuthorizationDecision decision = this.authorizationManager.check(this::getAuthentication, mi);
|
||||||
this.eventPublisher.publishAuthorizationEvent(this::getAuthentication, mi, decision);
|
this.eventPublisher.publishAuthorizationEvent(this::getAuthentication, mi, decision);
|
||||||
if (decision != null && !decision.isGranted()) {
|
if (decision != null && !decision.isGranted()) {
|
||||||
this.logger.debug(LogMessage.of(() -> "Failed to authorize " + mi + " with authorization manager "
|
this.logger.debug(LogMessage.of(() -> "Failed to authorize " + mi + " with authorization manager "
|
||||||
+ this.authorizationManager + " and decision " + decision));
|
+ this.authorizationManager + " and decision " + decision));
|
||||||
throw new AccessDeniedException("Access Denied");
|
return handle(mi, decision);
|
||||||
}
|
}
|
||||||
this.logger.debug(LogMessage.of(() -> "Authorized method invocation " + mi));
|
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() {
|
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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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.ReactiveAdapter;
|
||||||
import org.springframework.core.ReactiveAdapterRegistry;
|
import org.springframework.core.ReactiveAdapterRegistry;
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
import org.springframework.security.authorization.AuthorizationDecision;
|
||||||
import org.springframework.security.authorization.ReactiveAuthorizationManager;
|
import org.springframework.security.authorization.ReactiveAuthorizationManager;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
@ -57,6 +58,8 @@ public final class AuthorizationManagerBeforeReactiveMethodInterceptor implement
|
|||||||
|
|
||||||
private int order = AuthorizationInterceptorsOrder.FIRST.getOrder();
|
private int order = AuthorizationInterceptorsOrder.FIRST.getOrder();
|
||||||
|
|
||||||
|
private final MethodAuthorizationDeniedHandler defaultHandler = new ThrowingMethodAuthorizationDeniedHandler();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an instance for the {@link PreAuthorize} annotation.
|
* Creates an instance for the {@link PreAuthorize} annotation.
|
||||||
* @return the {@link AuthorizationManagerBeforeReactiveMethodInterceptor} to use
|
* @return the {@link AuthorizationManagerBeforeReactiveMethodInterceptor} to use
|
||||||
@ -112,31 +115,65 @@ public final class AuthorizationManagerBeforeReactiveMethodInterceptor implement
|
|||||||
+ " must return an instance of org.reactivestreams.Publisher "
|
+ " must return an instance of org.reactivestreams.Publisher "
|
||||||
+ "(for example, a Mono or Flux) or the function must be a Kotlin coroutine "
|
+ "(for example, a Mono or Flux) or the function must be a Kotlin coroutine "
|
||||||
+ "in order to support Reactor Context");
|
+ "in order to support Reactor Context");
|
||||||
Mono<Authentication> authentication = ReactiveAuthenticationUtils.getAuthentication();
|
|
||||||
ReactiveAdapter adapter = ReactiveAdapterRegistry.getSharedInstance().getAdapter(type);
|
ReactiveAdapter adapter = ReactiveAdapterRegistry.getSharedInstance().getAdapter(type);
|
||||||
Mono<Void> preAuthorize = this.authorizationManager.verify(authentication, mi);
|
|
||||||
if (hasFlowReturnType) {
|
if (hasFlowReturnType) {
|
||||||
if (isSuspendingFunction) {
|
if (isSuspendingFunction) {
|
||||||
return preAuthorize.thenMany(Flux.defer(() -> ReactiveMethodInvocationUtils.proceed(mi)));
|
return preAuthorized(mi, Flux.defer(() -> ReactiveMethodInvocationUtils.proceed(mi)));
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
Assert.state(adapter != null, () -> "The returnType " + type + " on " + method
|
Assert.state(adapter != null, () -> "The returnType " + type + " on " + method
|
||||||
+ " must have a org.springframework.core.ReactiveAdapter registered");
|
+ " must have a org.springframework.core.ReactiveAdapter registered");
|
||||||
Flux<?> response = preAuthorize
|
Flux<Object> response = preAuthorized(mi,
|
||||||
.thenMany(Flux.defer(() -> adapter.toPublisher(ReactiveMethodInvocationUtils.proceed(mi))));
|
Flux.defer(() -> adapter.toPublisher(ReactiveMethodInvocationUtils.proceed(mi))));
|
||||||
return KotlinDelegate.asFlow(response);
|
return KotlinDelegate.asFlow(response);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (isMultiValue(type, adapter)) {
|
if (isMultiValue(type, adapter)) {
|
||||||
Publisher<?> publisher = Flux.defer(() -> ReactiveMethodInvocationUtils.proceed(mi));
|
Flux<?> result = preAuthorized(mi, Flux.defer(() -> ReactiveMethodInvocationUtils.proceed(mi)));
|
||||||
Flux<?> result = preAuthorize.thenMany(publisher);
|
|
||||||
return (adapter != null) ? adapter.fromPublisher(result) : result;
|
return (adapter != null) ? adapter.fromPublisher(result) : result;
|
||||||
}
|
}
|
||||||
Mono<?> publisher = Mono.defer(() -> ReactiveMethodInvocationUtils.proceed(mi));
|
Mono<?> result = preAuthorized(mi, Mono.defer(() -> ReactiveMethodInvocationUtils.proceed(mi)));
|
||||||
Mono<?> result = preAuthorize.then(publisher);
|
|
||||||
return (adapter != null) ? adapter.fromPublisher(result) : result;
|
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) {
|
private boolean isMultiValue(Class<?> returnType, ReactiveAdapter adapter) {
|
||||||
if (Flux.class.isAssignableFrom(returnType)) {
|
if (Flux.class.isAssignableFrom(returnType)) {
|
||||||
return true;
|
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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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.aopalliance.intercept.MethodInvocation;
|
||||||
|
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
import org.springframework.expression.EvaluationContext;
|
import org.springframework.expression.EvaluationContext;
|
||||||
import org.springframework.security.access.expression.ExpressionUtils;
|
import org.springframework.security.access.expression.ExpressionUtils;
|
||||||
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
|
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
|
||||||
import org.springframework.security.access.prepost.PostAuthorize;
|
import org.springframework.security.access.prepost.PostAuthorize;
|
||||||
import org.springframework.security.authorization.AuthorizationDecision;
|
import org.springframework.security.authorization.AuthorizationDecision;
|
||||||
import org.springframework.security.authorization.AuthorizationManager;
|
import org.springframework.security.authorization.AuthorizationManager;
|
||||||
import org.springframework.security.authorization.ExpressionAuthorizationDecision;
|
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -61,6 +61,18 @@ public final class PostAuthorizeAuthorizationManager implements AuthorizationMan
|
|||||||
this.registry.setTemplateDefaults(defaults);
|
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
|
* Determine if an {@link Authentication} has access to the returned object by
|
||||||
* evaluating the {@link PostAuthorize} annotation that the {@link MethodInvocation}
|
* evaluating the {@link PostAuthorize} annotation that the {@link MethodInvocation}
|
||||||
@ -76,11 +88,13 @@ public final class PostAuthorizeAuthorizationManager implements AuthorizationMan
|
|||||||
if (attribute == ExpressionAttribute.NULL_ATTRIBUTE) {
|
if (attribute == ExpressionAttribute.NULL_ATTRIBUTE) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
PostAuthorizeExpressionAttribute postAuthorizeAttribute = (PostAuthorizeExpressionAttribute) attribute;
|
||||||
MethodSecurityExpressionHandler expressionHandler = this.registry.getExpressionHandler();
|
MethodSecurityExpressionHandler expressionHandler = this.registry.getExpressionHandler();
|
||||||
EvaluationContext ctx = expressionHandler.createEvaluationContext(authentication, mi.getMethodInvocation());
|
EvaluationContext ctx = expressionHandler.createEvaluationContext(authentication, mi.getMethodInvocation());
|
||||||
expressionHandler.setReturnObject(mi.getResult(), ctx);
|
expressionHandler.setReturnObject(mi.getResult(), ctx);
|
||||||
boolean granted = ExpressionUtils.evaluateAsBoolean(attribute.getExpression(), ctx);
|
boolean granted = ExpressionUtils.evaluateAsBoolean(postAuthorizeAttribute.getExpression(), ctx);
|
||||||
return new ExpressionAuthorizationDecision(granted, attribute.getExpression());
|
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.AnnotatedElement;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
||||||
import reactor.util.annotation.NonNull;
|
import reactor.util.annotation.NonNull;
|
||||||
|
|
||||||
import org.springframework.aop.support.AopUtils;
|
import org.springframework.aop.support.AopUtils;
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
import org.springframework.expression.Expression;
|
import org.springframework.expression.Expression;
|
||||||
import org.springframework.security.access.prepost.PostAuthorize;
|
import org.springframework.security.access.prepost.PostAuthorize;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* For internal use only, as this contract is likely to change.
|
* 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> {
|
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
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
ExpressionAttribute resolveAttribute(Method method, Class<?> targetClass) {
|
ExpressionAttribute resolveAttribute(Method method, Class<?> targetClass) {
|
||||||
@ -44,7 +55,9 @@ final class PostAuthorizeExpressionAttributeRegistry extends AbstractExpressionA
|
|||||||
return ExpressionAttribute.NULL_ATTRIBUTE;
|
return ExpressionAttribute.NULL_ATTRIBUTE;
|
||||||
}
|
}
|
||||||
Expression expression = getExpressionHandler().getExpressionParser().parseExpression(postAuthorize.value());
|
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) {
|
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));
|
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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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 org.aopalliance.intercept.MethodInvocation;
|
||||||
import reactor.core.publisher.Mono;
|
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.DefaultMethodSecurityExpressionHandler;
|
||||||
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
|
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
|
||||||
import org.springframework.security.access.prepost.PostAuthorize;
|
import org.springframework.security.access.prepost.PostAuthorize;
|
||||||
@ -61,6 +62,10 @@ public final class PostAuthorizeReactiveAuthorizationManager
|
|||||||
this.registry.setTemplateDefaults(defaults);
|
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
|
* Determines if an {@link Authentication} has access to the returned object from the
|
||||||
* {@link MethodInvocation} by evaluating an expression from the {@link PostAuthorize}
|
* {@link MethodInvocation} by evaluating an expression from the {@link PostAuthorize}
|
||||||
@ -77,13 +82,14 @@ public final class PostAuthorizeReactiveAuthorizationManager
|
|||||||
if (attribute == ExpressionAttribute.NULL_ATTRIBUTE) {
|
if (attribute == ExpressionAttribute.NULL_ATTRIBUTE) {
|
||||||
return Mono.empty();
|
return Mono.empty();
|
||||||
}
|
}
|
||||||
|
PostAuthorizeExpressionAttribute postAuthorizeAttribute = (PostAuthorizeExpressionAttribute) attribute;
|
||||||
MethodSecurityExpressionHandler expressionHandler = this.registry.getExpressionHandler();
|
MethodSecurityExpressionHandler expressionHandler = this.registry.getExpressionHandler();
|
||||||
// @formatter:off
|
// @formatter:off
|
||||||
return authentication
|
return authentication
|
||||||
.map((auth) -> expressionHandler.createEvaluationContext(auth, mi))
|
.map((auth) -> expressionHandler.createEvaluationContext(auth, mi))
|
||||||
.doOnNext((ctx) -> expressionHandler.setReturnObject(result.getResult(), ctx))
|
.doOnNext((ctx) -> expressionHandler.setReturnObject(result.getResult(), ctx))
|
||||||
.flatMap((ctx) -> ReactiveExpressionUtils.evaluateAsBoolean(attribute.getExpression(), 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
|
// @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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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.aopalliance.intercept.MethodInvocation;
|
||||||
|
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
import org.springframework.expression.EvaluationContext;
|
import org.springframework.expression.EvaluationContext;
|
||||||
import org.springframework.security.access.expression.ExpressionUtils;
|
import org.springframework.security.access.expression.ExpressionUtils;
|
||||||
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
|
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
import org.springframework.security.authorization.AuthorizationDecision;
|
import org.springframework.security.authorization.AuthorizationDecision;
|
||||||
import org.springframework.security.authorization.AuthorizationManager;
|
import org.springframework.security.authorization.AuthorizationManager;
|
||||||
import org.springframework.security.authorization.ExpressionAuthorizationDecision;
|
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -61,6 +61,10 @@ public final class PreAuthorizeAuthorizationManager implements AuthorizationMana
|
|||||||
this.registry.setTemplateDefaults(defaults);
|
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
|
* Determine if an {@link Authentication} has access to a method by evaluating an
|
||||||
* expression from the {@link PreAuthorize} annotation that the
|
* expression from the {@link PreAuthorize} annotation that the
|
||||||
@ -76,9 +80,11 @@ public final class PreAuthorizeAuthorizationManager implements AuthorizationMana
|
|||||||
if (attribute == ExpressionAttribute.NULL_ATTRIBUTE) {
|
if (attribute == ExpressionAttribute.NULL_ATTRIBUTE) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
PreAuthorizeExpressionAttribute preAuthorizeAttribute = (PreAuthorizeExpressionAttribute) attribute;
|
||||||
EvaluationContext ctx = this.registry.getExpressionHandler().createEvaluationContext(authentication, mi);
|
EvaluationContext ctx = this.registry.getExpressionHandler().createEvaluationContext(authentication, mi);
|
||||||
boolean granted = ExpressionUtils.evaluateAsBoolean(attribute.getExpression(), ctx);
|
boolean granted = ExpressionUtils.evaluateAsBoolean(preAuthorizeAttribute.getExpression(), ctx);
|
||||||
return new ExpressionAuthorizationDecision(granted, attribute.getExpression());
|
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.AnnotatedElement;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
||||||
import reactor.util.annotation.NonNull;
|
import reactor.util.annotation.NonNull;
|
||||||
|
|
||||||
import org.springframework.aop.support.AopUtils;
|
import org.springframework.aop.support.AopUtils;
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
import org.springframework.expression.Expression;
|
import org.springframework.expression.Expression;
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* For internal use only, as this contract is likely to change.
|
* 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> {
|
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
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
ExpressionAttribute resolveAttribute(Method method, Class<?> targetClass) {
|
ExpressionAttribute resolveAttribute(Method method, Class<?> targetClass) {
|
||||||
@ -44,7 +55,8 @@ final class PreAuthorizeExpressionAttributeRegistry extends AbstractExpressionAt
|
|||||||
return ExpressionAttribute.NULL_ATTRIBUTE;
|
return ExpressionAttribute.NULL_ATTRIBUTE;
|
||||||
}
|
}
|
||||||
Expression expression = getExpressionHandler().getExpressionParser().parseExpression(preAuthorize.value());
|
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) {
|
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));
|
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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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 org.aopalliance.intercept.MethodInvocation;
|
||||||
import reactor.core.publisher.Mono;
|
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.DefaultMethodSecurityExpressionHandler;
|
||||||
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
|
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
@ -60,6 +61,10 @@ public final class PreAuthorizeReactiveAuthorizationManager implements ReactiveA
|
|||||||
this.registry.setTemplateDefaults(defaults);
|
this.registry.setTemplateDefaults(defaults);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setApplicationContext(ApplicationContext context) {
|
||||||
|
this.registry.setApplicationContext(context);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determines if an {@link Authentication} has access to the {@link MethodInvocation}
|
* Determines if an {@link Authentication} has access to the {@link MethodInvocation}
|
||||||
* by evaluating an expression from the {@link PreAuthorize} annotation.
|
* by evaluating an expression from the {@link PreAuthorize} annotation.
|
||||||
@ -74,11 +79,12 @@ public final class PreAuthorizeReactiveAuthorizationManager implements ReactiveA
|
|||||||
if (attribute == ExpressionAttribute.NULL_ATTRIBUTE) {
|
if (attribute == ExpressionAttribute.NULL_ATTRIBUTE) {
|
||||||
return Mono.empty();
|
return Mono.empty();
|
||||||
}
|
}
|
||||||
|
PreAuthorizeExpressionAttribute preAuthorizeAttribute = (PreAuthorizeExpressionAttribute) attribute;
|
||||||
// @formatter:off
|
// @formatter:off
|
||||||
return authentication
|
return authentication
|
||||||
.map((auth) -> this.registry.getExpressionHandler().createEvaluationContext(auth, mi))
|
.map((auth) -> this.registry.getExpressionHandler().createEvaluationContext(auth, mi))
|
||||||
.flatMap((ctx) -> ReactiveExpressionUtils.evaluateAsBoolean(attribute.getExpression(), ctx))
|
.flatMap((ctx) -> ReactiveExpressionUtils.evaluateAsBoolean(attribute.getExpression(), ctx))
|
||||||
.map((granted) -> new ExpressionAttributeAuthorizationDecision(granted, attribute));
|
.map((granted) -> new PreAuthorizeAuthorizationDecision(granted, preAuthorizeAttribute.getExpression(), preAuthorizeAttribute.getHandler()));
|
||||||
// @formatter:on
|
// @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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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 reactor.core.publisher.Mono;
|
||||||
|
|
||||||
import org.springframework.aop.Pointcut;
|
import org.springframework.aop.Pointcut;
|
||||||
|
import org.springframework.expression.common.LiteralExpression;
|
||||||
import org.springframework.security.access.AccessDeniedException;
|
import org.springframework.security.access.AccessDeniedException;
|
||||||
import org.springframework.security.access.intercept.method.MockMethodInvocation;
|
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 org.springframework.security.authorization.ReactiveAuthorizationManager;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
@ -66,14 +70,15 @@ public class AuthorizationManagerAfterReactiveMethodInterceptorTests {
|
|||||||
given(mockMethodInvocation.proceed()).willReturn(Mono.just("john"));
|
given(mockMethodInvocation.proceed()).willReturn(Mono.just("john"));
|
||||||
ReactiveAuthorizationManager<MethodInvocationResult> mockReactiveAuthorizationManager = mock(
|
ReactiveAuthorizationManager<MethodInvocationResult> mockReactiveAuthorizationManager = mock(
|
||||||
ReactiveAuthorizationManager.class);
|
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(
|
AuthorizationManagerAfterReactiveMethodInterceptor interceptor = new AuthorizationManagerAfterReactiveMethodInterceptor(
|
||||||
Pointcut.TRUE, mockReactiveAuthorizationManager);
|
Pointcut.TRUE, mockReactiveAuthorizationManager);
|
||||||
Object result = interceptor.invoke(mockMethodInvocation);
|
Object result = interceptor.invoke(mockMethodInvocation);
|
||||||
assertThat(result).asInstanceOf(InstanceOfAssertFactories.type(Mono.class))
|
assertThat(result).asInstanceOf(InstanceOfAssertFactories.type(Mono.class))
|
||||||
.extracting(Mono::block)
|
.extracting(Mono::block)
|
||||||
.isEqualTo("john");
|
.isEqualTo("john");
|
||||||
verify(mockReactiveAuthorizationManager).verify(any(), any());
|
verify(mockReactiveAuthorizationManager).check(any(), any());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -83,7 +88,8 @@ public class AuthorizationManagerAfterReactiveMethodInterceptorTests {
|
|||||||
given(mockMethodInvocation.proceed()).willReturn(Flux.just("john", "bob"));
|
given(mockMethodInvocation.proceed()).willReturn(Flux.just("john", "bob"));
|
||||||
ReactiveAuthorizationManager<MethodInvocationResult> mockReactiveAuthorizationManager = mock(
|
ReactiveAuthorizationManager<MethodInvocationResult> mockReactiveAuthorizationManager = mock(
|
||||||
ReactiveAuthorizationManager.class);
|
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(
|
AuthorizationManagerAfterReactiveMethodInterceptor interceptor = new AuthorizationManagerAfterReactiveMethodInterceptor(
|
||||||
Pointcut.TRUE, mockReactiveAuthorizationManager);
|
Pointcut.TRUE, mockReactiveAuthorizationManager);
|
||||||
Object result = interceptor.invoke(mockMethodInvocation);
|
Object result = interceptor.invoke(mockMethodInvocation);
|
||||||
@ -91,7 +97,7 @@ public class AuthorizationManagerAfterReactiveMethodInterceptorTests {
|
|||||||
.extracting(Flux::collectList)
|
.extracting(Flux::collectList)
|
||||||
.extracting(Mono::block, InstanceOfAssertFactories.list(String.class))
|
.extracting(Mono::block, InstanceOfAssertFactories.list(String.class))
|
||||||
.containsExactly("john", "bob");
|
.containsExactly("john", "bob");
|
||||||
verify(mockReactiveAuthorizationManager, times(2)).verify(any(), any());
|
verify(mockReactiveAuthorizationManager, times(2)).check(any(), any());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -101,8 +107,8 @@ public class AuthorizationManagerAfterReactiveMethodInterceptorTests {
|
|||||||
given(mockMethodInvocation.proceed()).willReturn(Mono.just("john"));
|
given(mockMethodInvocation.proceed()).willReturn(Mono.just("john"));
|
||||||
ReactiveAuthorizationManager<MethodInvocationResult> mockReactiveAuthorizationManager = mock(
|
ReactiveAuthorizationManager<MethodInvocationResult> mockReactiveAuthorizationManager = mock(
|
||||||
ReactiveAuthorizationManager.class);
|
ReactiveAuthorizationManager.class);
|
||||||
given(mockReactiveAuthorizationManager.verify(any(), any()))
|
given(mockReactiveAuthorizationManager.check(any(), any()))
|
||||||
.willReturn(Mono.error(new AccessDeniedException("Access Denied")));
|
.willReturn(Mono.just(new AuthorizationDecision(false)));
|
||||||
AuthorizationManagerAfterReactiveMethodInterceptor interceptor = new AuthorizationManagerAfterReactiveMethodInterceptor(
|
AuthorizationManagerAfterReactiveMethodInterceptor interceptor = new AuthorizationManagerAfterReactiveMethodInterceptor(
|
||||||
Pointcut.TRUE, mockReactiveAuthorizationManager);
|
Pointcut.TRUE, mockReactiveAuthorizationManager);
|
||||||
Object result = interceptor.invoke(mockMethodInvocation);
|
Object result = interceptor.invoke(mockMethodInvocation);
|
||||||
@ -110,7 +116,157 @@ public class AuthorizationManagerAfterReactiveMethodInterceptorTests {
|
|||||||
.isThrownBy(() -> assertThat(result).asInstanceOf(InstanceOfAssertFactories.type(Mono.class))
|
.isThrownBy(() -> assertThat(result).asInstanceOf(InstanceOfAssertFactories.type(Mono.class))
|
||||||
.extracting(Mono::block))
|
.extracting(Mono::block))
|
||||||
.withMessage("Access Denied");
|
.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 {
|
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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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 reactor.core.publisher.Mono;
|
||||||
|
|
||||||
import org.springframework.aop.Pointcut;
|
import org.springframework.aop.Pointcut;
|
||||||
|
import org.springframework.expression.common.LiteralExpression;
|
||||||
import org.springframework.security.access.AccessDeniedException;
|
import org.springframework.security.access.AccessDeniedException;
|
||||||
import org.springframework.security.access.intercept.method.MockMethodInvocation;
|
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 org.springframework.security.authorization.ReactiveAuthorizationManager;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
@ -67,14 +71,15 @@ public class AuthorizationManagerBeforeReactiveMethodInterceptorTests {
|
|||||||
given(mockMethodInvocation.proceed()).willReturn(Mono.just("john"));
|
given(mockMethodInvocation.proceed()).willReturn(Mono.just("john"));
|
||||||
ReactiveAuthorizationManager<MethodInvocation> mockReactiveAuthorizationManager = mock(
|
ReactiveAuthorizationManager<MethodInvocation> mockReactiveAuthorizationManager = mock(
|
||||||
ReactiveAuthorizationManager.class);
|
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(
|
AuthorizationManagerBeforeReactiveMethodInterceptor interceptor = new AuthorizationManagerBeforeReactiveMethodInterceptor(
|
||||||
Pointcut.TRUE, mockReactiveAuthorizationManager);
|
Pointcut.TRUE, mockReactiveAuthorizationManager);
|
||||||
Object result = interceptor.invoke(mockMethodInvocation);
|
Object result = interceptor.invoke(mockMethodInvocation);
|
||||||
assertThat(result).asInstanceOf(InstanceOfAssertFactories.type(Mono.class))
|
assertThat(result).asInstanceOf(InstanceOfAssertFactories.type(Mono.class))
|
||||||
.extracting(Mono::block)
|
.extracting(Mono::block)
|
||||||
.isEqualTo("john");
|
.isEqualTo("john");
|
||||||
verify(mockReactiveAuthorizationManager).verify(any(), eq(mockMethodInvocation));
|
verify(mockReactiveAuthorizationManager).check(any(), eq(mockMethodInvocation));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -84,7 +89,8 @@ public class AuthorizationManagerBeforeReactiveMethodInterceptorTests {
|
|||||||
given(mockMethodInvocation.proceed()).willReturn(Flux.just("john", "bob"));
|
given(mockMethodInvocation.proceed()).willReturn(Flux.just("john", "bob"));
|
||||||
ReactiveAuthorizationManager<MethodInvocation> mockReactiveAuthorizationManager = mock(
|
ReactiveAuthorizationManager<MethodInvocation> mockReactiveAuthorizationManager = mock(
|
||||||
ReactiveAuthorizationManager.class);
|
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(
|
AuthorizationManagerBeforeReactiveMethodInterceptor interceptor = new AuthorizationManagerBeforeReactiveMethodInterceptor(
|
||||||
Pointcut.TRUE, mockReactiveAuthorizationManager);
|
Pointcut.TRUE, mockReactiveAuthorizationManager);
|
||||||
Object result = interceptor.invoke(mockMethodInvocation);
|
Object result = interceptor.invoke(mockMethodInvocation);
|
||||||
@ -92,7 +98,7 @@ public class AuthorizationManagerBeforeReactiveMethodInterceptorTests {
|
|||||||
.extracting(Flux::collectList)
|
.extracting(Flux::collectList)
|
||||||
.extracting(Mono::block, InstanceOfAssertFactories.list(String.class))
|
.extracting(Mono::block, InstanceOfAssertFactories.list(String.class))
|
||||||
.containsExactly("john", "bob");
|
.containsExactly("john", "bob");
|
||||||
verify(mockReactiveAuthorizationManager).verify(any(), eq(mockMethodInvocation));
|
verify(mockReactiveAuthorizationManager).check(any(), eq(mockMethodInvocation));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -102,8 +108,8 @@ public class AuthorizationManagerBeforeReactiveMethodInterceptorTests {
|
|||||||
given(mockMethodInvocation.proceed()).willReturn(Mono.just("john"));
|
given(mockMethodInvocation.proceed()).willReturn(Mono.just("john"));
|
||||||
ReactiveAuthorizationManager<MethodInvocation> mockReactiveAuthorizationManager = mock(
|
ReactiveAuthorizationManager<MethodInvocation> mockReactiveAuthorizationManager = mock(
|
||||||
ReactiveAuthorizationManager.class);
|
ReactiveAuthorizationManager.class);
|
||||||
given(mockReactiveAuthorizationManager.verify(any(), eq(mockMethodInvocation)))
|
given(mockReactiveAuthorizationManager.check(any(), eq(mockMethodInvocation)))
|
||||||
.willReturn(Mono.error(new AccessDeniedException("Access Denied")));
|
.willReturn(Mono.just(new AuthorizationDecision(false)));
|
||||||
AuthorizationManagerBeforeReactiveMethodInterceptor interceptor = new AuthorizationManagerBeforeReactiveMethodInterceptor(
|
AuthorizationManagerBeforeReactiveMethodInterceptor interceptor = new AuthorizationManagerBeforeReactiveMethodInterceptor(
|
||||||
Pointcut.TRUE, mockReactiveAuthorizationManager);
|
Pointcut.TRUE, mockReactiveAuthorizationManager);
|
||||||
Object result = interceptor.invoke(mockMethodInvocation);
|
Object result = interceptor.invoke(mockMethodInvocation);
|
||||||
@ -111,7 +117,119 @@ public class AuthorizationManagerBeforeReactiveMethodInterceptorTests {
|
|||||||
.isThrownBy(() -> assertThat(result).asInstanceOf(InstanceOfAssertFactories.type(Mono.class))
|
.isThrownBy(() -> assertThat(result).asInstanceOf(InstanceOfAssertFactories.type(Mono.class))
|
||||||
.extracting(Mono::block))
|
.extracting(Mono::block))
|
||||||
.withMessage("Access Denied");
|
.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 {
|
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
|
* Understanding <<method-security-architecture,how method security works>> and reasons to use it
|
||||||
* Comparing <<request-vs-method,request-level and method-level authorization>>
|
* Comparing <<request-vs-method,request-level and method-level authorization>>
|
||||||
* Authorizing methods with <<use-preauthorize,`@PreAuthorize`>> and <<use-postauthorize,`@PostAuthorize`>>
|
* 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`>>
|
* Filtering methods with <<use-prefilter,`@PreFilter`>> and <<use-postfilter,`@PostFilter`>>
|
||||||
* Authorizing methods with <<use-jsr250,JSR-250 annotations>>
|
* Authorizing methods with <<use-jsr250,JSR-250 annotations>>
|
||||||
* Authorizing methods with <<use-aspectj,AspectJ expressions>>
|
* 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.
|
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]]
|
[[migration-enableglobalmethodsecurity]]
|
||||||
== Migrating from `@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/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/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
|
== Configuration
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user