Browse Source

SpEL Expressions Support Returning AuthorizationManager

Closes gh-17936
pull/17943/head
Josh Cummings 3 months ago
parent
commit
765bdf1ed0
No known key found for this signature in database
GPG Key ID: 869B37A20E876129
  1. 11
      config/src/test/java/org/springframework/security/config/annotation/method/configuration/Authz.java
  2. 3
      config/src/test/java/org/springframework/security/config/annotation/method/configuration/MethodSecurityService.java
  3. 5
      config/src/test/java/org/springframework/security/config/annotation/method/configuration/MethodSecurityServiceImpl.java
  4. 9
      config/src/test/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfigurationTests.java
  5. 11
      config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityConfigurationTests.java
  6. 3
      config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityService.java
  7. 5
      config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityServiceImpl.java
  8. 15
      core/src/main/java/org/springframework/security/authorization/method/ExpressionUtils.java
  9. 2
      core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeAuthorizationManager.java
  10. 2
      core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeReactiveAuthorizationManager.java
  11. 2
      core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationManager.java
  12. 2
      core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeReactiveAuthorizationManager.java
  13. 12
      core/src/main/java/org/springframework/security/authorization/method/ReactiveExpressionUtils.java
  14. 6
      docs/modules/ROOT/pages/servlet/authorization/method-security.adoc

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

@ -16,10 +16,13 @@ @@ -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 { @@ -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
config/src/test/java/org/springframework/security/config/annotation/method/configuration/MethodSecurityService.java

@ -196,6 +196,9 @@ public interface MethodSecurityService { @@ -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
config/src/test/java/org/springframework/security/config/annotation/method/configuration/MethodSecurityServiceImpl.java

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

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

@ -92,6 +92,7 @@ import org.springframework.security.access.prepost.PostFilter; @@ -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 { @@ -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
config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityConfigurationTests.java

@ -51,6 +51,7 @@ import org.springframework.security.access.prepost.PostFilter; @@ -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; @@ -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 { @@ -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
config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityService.java

@ -110,6 +110,9 @@ public interface ReactiveMethodSecurityService { @@ -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
config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityServiceImpl.java

@ -100,6 +100,11 @@ public class ReactiveMethodSecurityServiceImpl implements ReactiveMethodSecurity @@ -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
core/src/main/java/org/springframework/security/authorization/method/ExpressionUtils.java

@ -16,14 +16,19 @@ @@ -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 { @@ -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;
}

2
core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeAuthorizationManager.java

@ -95,7 +95,7 @@ public final class PostAuthorizeAuthorizationManager @@ -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

2
core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeReactiveAuthorizationManager.java

@ -91,7 +91,7 @@ public final class PostAuthorizeReactiveAuthorizationManager @@ -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
}

2
core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationManager.java

@ -85,7 +85,7 @@ public final class PreAuthorizeAuthorizationManager @@ -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

2
core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeReactiveAuthorizationManager.java

@ -85,7 +85,7 @@ public final class PreAuthorizeReactiveAuthorizationManager @@ -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
core/src/main/java/org/springframework/security/authorization/method/ReactiveExpressionUtils.java

@ -24,6 +24,9 @@ import org.springframework.expression.EvaluationException; @@ -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 @@ -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 { @@ -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
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 @@ -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

Loading…
Cancel
Save