Add Authorization Denied Handlers for Method Security

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.authorization.method;
import org.springframework.lang.Nullable;
import org.springframework.security.authorization.AuthorizationResult;
/**
* An interface to define a strategy to handle denied method invocation results
*
* @author Marcus da Coregio
* @since 6.3
* @see org.springframework.security.access.prepost.PostAuthorize
*/
public interface MethodAuthorizationDeniedPostProcessor {
/**
* Post-process the denied result produced by a method invocation, implementations
* might either throw an
* {@link org.springframework.security.access.AccessDeniedException} or return a
* replacement result instead of the denied result, e.g. a masked value.
* @param methodInvocationResult the object containing the method invocation and the
* result produced
* @param authorizationResult the {@link AuthorizationResult} containing the
* authorization denied details
* @return a replacement result for the denied result, or null, or a
* {@link reactor.core.publisher.Mono} for reactive applications
*/
@Nullable
Object postProcessResult(MethodInvocationResult methodInvocationResult, AuthorizationResult authorizationResult);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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