Browse Source

SpEL Expressions Support Returning AuthorizationManager

Closes gh-17936
Josh Cummings 1 week ago
parent
commit
765bdf1ed0
14 changed files with 84 additions and 4 deletions
  1. 11 0
      config/src/test/java/org/springframework/security/config/annotation/method/configuration/Authz.java
  2. 3 0
      config/src/test/java/org/springframework/security/config/annotation/method/configuration/MethodSecurityService.java
  3. 5 0
      config/src/test/java/org/springframework/security/config/annotation/method/configuration/MethodSecurityServiceImpl.java
  4. 9 0
      config/src/test/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfigurationTests.java
  5. 11 0
      config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityConfigurationTests.java
  6. 3 0
      config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityService.java
  7. 5 0
      config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityServiceImpl.java
  8. 15 0
      core/src/main/java/org/springframework/security/authorization/method/ExpressionUtils.java
  9. 1 1
      core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeAuthorizationManager.java
  10. 1 1
      core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeReactiveAuthorizationManager.java
  11. 1 1
      core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationManager.java
  12. 1 1
      core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeReactiveAuthorizationManager.java
  13. 12 0
      core/src/main/java/org/springframework/security/authorization/method/ReactiveExpressionUtils.java
  14. 6 0
      docs/modules/ROOT/pages/servlet/authorization/method-security.adoc

+ 11 - 0
config/src/test/java/org/springframework/security/config/annotation/method/configuration/Authz.java

@@ -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 - 0
config/src/test/java/org/springframework/security/config/annotation/method/configuration/MethodSecurityService.java

@@ -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 - 0
config/src/test/java/org/springframework/security/config/annotation/method/configuration/MethodSecurityServiceImpl.java

@@ -203,6 +203,11 @@ public class MethodSecurityServiceImpl implements MethodSecurityService {
 		return "ok";
 	}
 
+	@Override
+	public String checkCustomManager(long id) {
+		return "ok";
+	}
+
 	@Override
 	public void hasAllRolesUserAdmin() {
 	}

+ 9 - 0
config/src/test/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfigurationTests.java

@@ -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 - 0
config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityConfigurationTests.java

@@ -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 - 0
config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityService.java

@@ -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 - 0
config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityServiceImpl.java

@@ -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 - 0
core/src/main/java/org/springframework/security/authorization/method/ExpressionUtils.java

@@ -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
core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeAuthorizationManager.java

@@ -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
core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeReactiveAuthorizationManager.java

@@ -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
core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationManager.java

@@ -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
core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeReactiveAuthorizationManager.java

@@ -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 - 0
core/src/main/java/org/springframework/security/authorization/method/ReactiveExpressionUtils.java

@@ -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));
 			}

+ 6 - 0
docs/modules/ROOT/pages/servlet/authorization/method-security.adoc

@@ -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