mirror of
https://github.com/spring-projects/spring-security.git
synced 2026-05-02 19:30:50 +01:00
SpEL Expressions Support Returning AuthorizationManager
Closes gh-17936
This commit is contained in:
+11
@@ -16,10 +16,13 @@
|
||||
|
||||
package org.springframework.security.config.annotation.method.configuration;
|
||||
|
||||
import org.aopalliance.intercept.MethodInvocation;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.security.authorization.AuthorizationDecision;
|
||||
import org.springframework.security.authorization.AuthorizationManager;
|
||||
import org.springframework.security.authorization.AuthorizationResult;
|
||||
import org.springframework.security.authorization.ReactiveAuthorizationManager;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@@ -55,6 +58,14 @@ public class Authz {
|
||||
return Mono.just(checkResult(result));
|
||||
}
|
||||
|
||||
public AuthorizationManager<MethodInvocation> checkManager(long id) {
|
||||
return (authentication, context) -> new AuthorizationDecision(check(id));
|
||||
}
|
||||
|
||||
public ReactiveAuthorizationManager<MethodInvocation> checkReactiveManager(long id) {
|
||||
return (authentication, context) -> checkReactive(id).map(AuthorizationDecision::new);
|
||||
}
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
public static class AuthzResult extends AuthorizationDecision {
|
||||
|
||||
|
||||
+3
@@ -196,6 +196,9 @@ public interface MethodSecurityService {
|
||||
@HandleAuthorizationDenied(handlerClass = MethodAuthorizationDeniedHandler.class)
|
||||
String checkCustomResult(boolean result);
|
||||
|
||||
@PreAuthorize("@authz.checkManager(#id)")
|
||||
String checkCustomManager(long id);
|
||||
|
||||
class StarMaskingHandler implements MethodAuthorizationDeniedHandler {
|
||||
|
||||
@Override
|
||||
|
||||
+5
@@ -203,6 +203,11 @@ public class MethodSecurityServiceImpl implements MethodSecurityService {
|
||||
return "ok";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String checkCustomManager(long id) {
|
||||
return "ok";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hasAllRolesUserAdmin() {
|
||||
}
|
||||
|
||||
+9
@@ -92,6 +92,7 @@ import org.springframework.security.access.prepost.PostFilter;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.access.prepost.PreFilter;
|
||||
import org.springframework.security.authorization.AuthorizationDecision;
|
||||
import org.springframework.security.authorization.AuthorizationDeniedException;
|
||||
import org.springframework.security.authorization.AuthorizationEventPublisher;
|
||||
import org.springframework.security.authorization.AuthorizationManager;
|
||||
import org.springframework.security.authorization.SpringAuthorizationEventPublisher;
|
||||
@@ -1410,6 +1411,14 @@ public class PrePostMethodSecurityConfigurationTests {
|
||||
this.mvc.perform(requestWithUser).andExpect(status().isForbidden());
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkCustomManagerWhenInvokedThenUsesBeanToAuthorize() {
|
||||
this.spring.register(MethodSecurityServiceConfig.class).autowire();
|
||||
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
|
||||
service.checkCustomManager(2);
|
||||
assertThatExceptionOfType(AuthorizationDeniedException.class).isThrownBy(() -> service.checkCustomManager(1));
|
||||
}
|
||||
|
||||
private static Consumer<ConfigurableWebApplicationContext> disallowBeanOverriding() {
|
||||
return (context) -> ((AnnotationConfigWebApplicationContext) context).setAllowBeanDefinitionOverriding(false);
|
||||
}
|
||||
|
||||
+11
@@ -51,6 +51,7 @@ import org.springframework.security.access.prepost.PostFilter;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.access.prepost.PreFilter;
|
||||
import org.springframework.security.authentication.TestAuthentication;
|
||||
import org.springframework.security.authorization.AuthorizationDeniedException;
|
||||
import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory;
|
||||
import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory.TargetVisitor;
|
||||
import org.springframework.security.authorization.method.AuthorizeReturnObject;
|
||||
@@ -66,6 +67,7 @@ import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.test.context.support.WithMockUser;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.clearInvocations;
|
||||
import static org.mockito.Mockito.mock;
|
||||
@@ -285,6 +287,15 @@ public class ReactiveMethodSecurityConfigurationTests {
|
||||
verifyNoInteractions(handler);
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkCustomManagerWhenInvokedThenUsesBeanToAuthorize() {
|
||||
this.spring.register(WithRolePrefixConfiguration.class, MethodSecurityServiceConfig.class).autowire();
|
||||
ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class);
|
||||
service.checkCustomManager(2).block();
|
||||
assertThatExceptionOfType(AuthorizationDeniedException.class)
|
||||
.isThrownBy(() -> service.checkCustomManager(1).block());
|
||||
}
|
||||
|
||||
private static Consumer<User.UserBuilder> authorities(String... authorities) {
|
||||
return (builder) -> builder.authorities(authorities);
|
||||
}
|
||||
|
||||
+3
@@ -110,6 +110,9 @@ public interface ReactiveMethodSecurityService {
|
||||
@HandleAuthorizationDenied(handlerClass = MethodAuthorizationDeniedHandler.class)
|
||||
Mono<String> checkCustomResult(boolean result);
|
||||
|
||||
@PreAuthorize("@authz.checkReactiveManager(#id)")
|
||||
Mono<String> checkCustomManager(long id);
|
||||
|
||||
@PreAuthorize("hasPermission(#kgName, 'read')")
|
||||
Mono<String> preAuthorizeHasPermission(String kgName);
|
||||
|
||||
|
||||
+5
@@ -100,6 +100,11 @@ public class ReactiveMethodSecurityServiceImpl implements ReactiveMethodSecurity
|
||||
return Mono.just("ok");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<String> checkCustomManager(long id) {
|
||||
return Mono.just("ok");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<String> preAuthorizeHasPermission(String kgName) {
|
||||
return Mono.just("ok");
|
||||
|
||||
+15
@@ -16,14 +16,19 @@
|
||||
|
||||
package org.springframework.security.authorization.method;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.expression.EvaluationContext;
|
||||
import org.springframework.expression.EvaluationException;
|
||||
import org.springframework.expression.Expression;
|
||||
import org.springframework.security.authorization.AuthorizationDeniedException;
|
||||
import org.springframework.security.authorization.AuthorizationManager;
|
||||
import org.springframework.security.authorization.AuthorizationResult;
|
||||
import org.springframework.security.authorization.ExpressionAuthorizationDecision;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
final class ExpressionUtils {
|
||||
|
||||
@@ -31,8 +36,18 @@ final class ExpressionUtils {
|
||||
}
|
||||
|
||||
static @Nullable AuthorizationResult evaluate(Expression expr, EvaluationContext ctx) {
|
||||
return evaluate(expr, ctx, () -> null, null);
|
||||
}
|
||||
|
||||
static <T> @Nullable AuthorizationResult evaluate(Expression expr, EvaluationContext ctx,
|
||||
Supplier<? extends @Nullable Authentication> authentication, @Nullable T context) {
|
||||
try {
|
||||
Object result = expr.getValue(ctx);
|
||||
if (result instanceof AuthorizationManager<?> manager) {
|
||||
Assert.notNull(authentication, "authentication supplier cannot be null");
|
||||
Assert.notNull(context, "context cannot be null");
|
||||
return ((AuthorizationManager<T>) manager).authorize(authentication, context);
|
||||
}
|
||||
if (result instanceof AuthorizationResult decision) {
|
||||
return decision;
|
||||
}
|
||||
|
||||
+1
-1
@@ -95,7 +95,7 @@ public final class PostAuthorizeAuthorizationManager
|
||||
MethodSecurityExpressionHandler expressionHandler = this.registry.getExpressionHandler();
|
||||
EvaluationContext ctx = expressionHandler.createEvaluationContext(authentication, mi.getMethodInvocation());
|
||||
expressionHandler.setReturnObject(mi.getResult(), ctx);
|
||||
return ExpressionUtils.evaluate(attribute.getExpression(), ctx);
|
||||
return ExpressionUtils.evaluate(attribute.getExpression(), ctx, authentication, mi);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+1
-1
@@ -91,7 +91,7 @@ public final class PostAuthorizeReactiveAuthorizationManager
|
||||
return authentication
|
||||
.map((auth) -> expressionHandler.createEvaluationContext(auth, mi))
|
||||
.doOnNext((ctx) -> expressionHandler.setReturnObject(result.getResult(), ctx))
|
||||
.flatMap((ctx) -> ReactiveExpressionUtils.evaluate(attribute.getExpression(), ctx))
|
||||
.flatMap((ctx) -> ReactiveExpressionUtils.evaluate(attribute.getExpression(), ctx, authentication, result))
|
||||
.cast(AuthorizationResult.class);
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
+1
-1
@@ -85,7 +85,7 @@ public final class PreAuthorizeAuthorizationManager
|
||||
return null;
|
||||
}
|
||||
EvaluationContext ctx = this.registry.getExpressionHandler().createEvaluationContext(authentication, mi);
|
||||
return ExpressionUtils.evaluate(attribute.getExpression(), ctx);
|
||||
return ExpressionUtils.evaluate(attribute.getExpression(), ctx, authentication, mi);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+1
-1
@@ -85,7 +85,7 @@ public final class PreAuthorizeReactiveAuthorizationManager
|
||||
// @formatter:off
|
||||
return authentication
|
||||
.map((auth) -> this.registry.getExpressionHandler().createEvaluationContext(auth, mi))
|
||||
.flatMap((ctx) -> ReactiveExpressionUtils.evaluate(attribute.getExpression(), ctx))
|
||||
.flatMap((ctx) -> ReactiveExpressionUtils.evaluate(attribute.getExpression(), ctx, authentication, mi))
|
||||
.cast(AuthorizationResult.class);
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
+12
@@ -24,6 +24,9 @@ import org.springframework.expression.EvaluationException;
|
||||
import org.springframework.expression.Expression;
|
||||
import org.springframework.security.authorization.AuthorizationResult;
|
||||
import org.springframework.security.authorization.ExpressionAuthorizationDecision;
|
||||
import org.springframework.security.authorization.ReactiveAuthorizationManager;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* For internal use only, as this contract is likely to change.
|
||||
@@ -34,6 +37,11 @@ import org.springframework.security.authorization.ExpressionAuthorizationDecisio
|
||||
final class ReactiveExpressionUtils {
|
||||
|
||||
static Mono<AuthorizationResult> evaluate(Expression expr, EvaluationContext ctx) {
|
||||
return evaluate(expr, ctx, Mono.empty(), null);
|
||||
}
|
||||
|
||||
static <T> Mono<AuthorizationResult> evaluate(Expression expr, EvaluationContext ctx,
|
||||
Mono<Authentication> authentication, @Nullable T context) {
|
||||
return Mono.defer(() -> {
|
||||
Object value;
|
||||
try {
|
||||
@@ -43,6 +51,10 @@ final class ReactiveExpressionUtils {
|
||||
return Mono.error(() -> new IllegalArgumentException(
|
||||
"Failed to evaluate expression '" + expr.getExpressionString() + "'", ex));
|
||||
}
|
||||
if (value instanceof ReactiveAuthorizationManager<?> manager) {
|
||||
Assert.notNull(context, "context cannot be null");
|
||||
return ((ReactiveAuthorizationManager<T>) manager).authorize(authentication, context);
|
||||
}
|
||||
if (value instanceof Mono<?> mono) {
|
||||
return mono.flatMap((data) -> adapt(expr, data));
|
||||
}
|
||||
|
||||
@@ -1362,6 +1362,12 @@ Note, though, that returning an object is preferred as this doesn't incur the ex
|
||||
|
||||
Then, you can access the custom details when you <<fallback-values-authorization-denied, customize how the authorization result is handled>>.
|
||||
|
||||
[TIP]
|
||||
====
|
||||
Further, you can return an `AuthorizationManager` itself.
|
||||
This is helpful when unifying custom web authorization rules with method security ones since web security by default requires specifying an `AuthorizationManager` instance.
|
||||
====
|
||||
|
||||
[[custom-authorization-managers]]
|
||||
=== Using a Custom Authorization Manager
|
||||
|
||||
|
||||
Reference in New Issue
Block a user