diff --git a/docs/modules/ROOT/pages/servlet/authorization/authorize-http-requests.adoc b/docs/modules/ROOT/pages/servlet/authorization/authorize-http-requests.adoc index 86425fe9ba..363ec5c4e1 100644 --- a/docs/modules/ROOT/pages/servlet/authorization/authorize-http-requests.adoc +++ b/docs/modules/ROOT/pages/servlet/authorization/authorize-http-requests.adoc @@ -996,6 +996,29 @@ Kotlin:: ---- ====== +To migrate several, you can use `WebExpressionAuthorizationManager#withDefaults`: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +WebExpressionAuthorizationManager.Builder authz = WebExpressionAuthorizationManager.withDefaults(); +.requestMatchers("/test/**").access(authz.expression("hasRole('ADMIN') && hasRole('USER')")) +.requestMatchers("/test/**").access(authz.expression("permitAll")) +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +var authz = WebExpressionAuthorizationManager.withDefaults() +.requestMatchers("/test/**").access(authz.expression("hasRole('ADMIN') && hasRole('USER')")) +.requestMatchers("/test/**").access(authz.expression("permitAll")) +---- +====== + If you are referring to a bean in your expression like so: `@webSecurity.check(authentication, request)`, it's recommended that you instead call the bean directly, which will look something like the following: [tabs] @@ -1019,7 +1042,32 @@ Kotlin:: For complex instructions that include bean references as well as other expressions, it is recommended that you change those to implement `AuthorizationManager` and refer to them by calling `.access(AuthorizationManager)`. -If you are not able to do that, you can configure a javadoc:org.springframework.security.web.access.expression.DefaultHttpSecurityExpressionHandler[] with a bean resolver and supply that to `WebExpressionAuthorizationManager#setExpressionhandler`. +If you are not able to do that, you can publish javadoc:org.springframework.security.web.access.expression.WebExpressionAuthorizationManager$Builder[] as a bean: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +@Bean +WebExpressionAuthorizationManager.Builder authz() { + return WebExpressionAuthorizationManager.withDefaults(); +} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@Bean +fun authz(): WebExpressionAuthorizationManager.Builder { + return WebExpressionAuthorizationManager.withDefaults() +} +---- +====== + +Then, expressions passed to that builder will be able to refer to beans. [[security-matchers]] == Security Matchers diff --git a/web/src/main/java/org/springframework/security/web/access/expression/WebExpressionAuthorizationManager.java b/web/src/main/java/org/springframework/security/web/access/expression/WebExpressionAuthorizationManager.java index 83b6fd7e1f..f82ea5c731 100644 --- a/web/src/main/java/org/springframework/security/web/access/expression/WebExpressionAuthorizationManager.java +++ b/web/src/main/java/org/springframework/security/web/access/expression/WebExpressionAuthorizationManager.java @@ -18,6 +18,9 @@ package org.springframework.security.web.access.expression; import java.util.function.Supplier; +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; import org.springframework.expression.EvaluationContext; import org.springframework.expression.Expression; import org.springframework.security.access.expression.ExpressionUtils; @@ -51,11 +54,20 @@ public final class WebExpressionAuthorizationManager implements AuthorizationMan this.expression = this.expressionHandler.getExpressionParser().parseExpression(expressionString); } + private WebExpressionAuthorizationManager(String expressionString, + SecurityExpressionHandler expressionHandler) { + Assert.hasText(expressionString, "expressionString cannot be empty"); + this.expressionHandler = expressionHandler; + this.expression = expressionHandler.getExpressionParser().parseExpression(expressionString); + } + /** * Sets the {@link SecurityExpressionHandler} to be used. The default is * {@link DefaultHttpSecurityExpressionHandler}. * @param expressionHandler the {@link SecurityExpressionHandler} to use + * @deprecated Please use {@link #withDefaults()} or {@link #withExpressionHandler} */ + @Deprecated public void setExpressionHandler(SecurityExpressionHandler expressionHandler) { Assert.notNull(expressionHandler, "expressionHandler cannot be null"); this.expressionHandler = expressionHandler; @@ -82,4 +94,78 @@ public final class WebExpressionAuthorizationManager implements AuthorizationMan return "WebExpressionAuthorizationManager[expression='" + this.expression + "']"; } + /** + * Use a {@link DefaultHttpSecurityExpressionHandler} to create + * {@link WebExpressionAuthorizationManager} instances. + * + *

+ * Note that publishing the {@link Builder} as a bean will allow the default + * expression handler to be configured with a bean provider so that expressions can + * reference beans + * @return a {@link Builder} for constructing + * {@link WebExpressionAuthorizationManager} instances + * @since 7.0 + */ + public static Builder withDefaults() { + return new Builder(); + } + + /** + * Use this {@link SecurityExpressionHandler} to create + * {@link WebExpressionAuthorizationManager} instances + * @param expressionHandler + * @return a {@link Builder} for constructing + * {@link WebExpressionAuthorizationManager} instances + * @since 7.0 + */ + public static Builder withExpressionHandler( + SecurityExpressionHandler expressionHandler) { + return new Builder(expressionHandler); + } + + /** + * A {@link Builder} for constructing {@link WebExpressionAuthorizationManager} + * instances. + * + *

+ * May be reused to create multiple instances. + * + * @author Josh Cummings + * @since 7.0 + */ + public static final class Builder implements ApplicationContextAware { + + private final SecurityExpressionHandler expressionHandler; + + private final boolean defaultExpressionHandler; + + private Builder() { + this.expressionHandler = new DefaultHttpSecurityExpressionHandler(); + this.defaultExpressionHandler = true; + } + + private Builder(SecurityExpressionHandler expressionHandler) { + this.expressionHandler = expressionHandler; + this.defaultExpressionHandler = false; + } + + /** + * Create a {@link WebExpressionAuthorizationManager} using this + * {@code expression} + * @param expression the expression to evaluate + * @return the resulting {@link AuthorizationManager} + */ + public WebExpressionAuthorizationManager expression(String expression) { + return new WebExpressionAuthorizationManager(expression, this.expressionHandler); + } + + @Override + public void setApplicationContext(ApplicationContext context) throws BeansException { + if (this.defaultExpressionHandler) { + ((DefaultHttpSecurityExpressionHandler) this.expressionHandler).setApplicationContext(context); + } + } + + } + } diff --git a/web/src/test/java/org/springframework/security/web/access/expression/WebExpressionAuthorizationManagerTests.java b/web/src/test/java/org/springframework/security/web/access/expression/WebExpressionAuthorizationManagerTests.java index 131dba7845..5c344cccf3 100644 --- a/web/src/test/java/org/springframework/security/web/access/expression/WebExpressionAuthorizationManagerTests.java +++ b/web/src/test/java/org/springframework/security/web/access/expression/WebExpressionAuthorizationManagerTests.java @@ -18,6 +18,7 @@ package org.springframework.security.web.access.expression; import org.junit.jupiter.api.Test; +import org.springframework.context.support.GenericApplicationContext; import org.springframework.expression.Expression; import org.springframework.expression.ExpressionParser; import org.springframework.mock.web.MockHttpServletRequest; @@ -102,4 +103,49 @@ class WebExpressionAuthorizationManagerTests { assertThat(decision.isGranted()).isFalse(); } + @Test + void authorizeWhenDefaultsThenEvaluatesExpressionsReferencingBeans() { + GenericApplicationContext context = new GenericApplicationContext(); + context.registerBean("bean", WebExpressionAuthorizationManagerTests.class, () -> this); + context.refresh(); + WebExpressionAuthorizationManager.Builder builder = WebExpressionAuthorizationManager.withDefaults(); + builder.setApplicationContext(context); + WebExpressionAuthorizationManager manager = builder + .expression("@bean.class.simpleName.startsWith('WebExpression')"); + AuthorizationResult result = manager.authorize(TestAuthentication::authenticatedUser, + new RequestAuthorizationContext(new MockHttpServletRequest())); + assertThat(result.isGranted()).isTrue(); + } + + @Test + void authorizeWhenDefaultsAsBeanThenEvaluatesExpressionsReferencingBeans() { + GenericApplicationContext context = new GenericApplicationContext(); + context.registerBean("bean", WebExpressionAuthorizationManagerTests.class, () -> this); + context.registerBean("builder", WebExpressionAuthorizationManager.Builder.class, + WebExpressionAuthorizationManager::withDefaults); + context.refresh(); + WebExpressionAuthorizationManager.Builder builder = context + .getBean(WebExpressionAuthorizationManager.Builder.class); + WebExpressionAuthorizationManager manager = builder + .expression("@bean.class.simpleName.startsWith('WebExpression')"); + AuthorizationResult result = manager.authorize(TestAuthentication::authenticatedUser, + new RequestAuthorizationContext(new MockHttpServletRequest())); + assertThat(result.isGranted()).isTrue(); + } + + @Test + void authorizeWhenExpressionHandlerHasBeanProviderThenEvaluatesExpressionsReferencingBeans() { + GenericApplicationContext context = new GenericApplicationContext(); + context.registerBean("bean", WebExpressionAuthorizationManagerTests.class, () -> this); + context.refresh(); + DefaultHttpSecurityExpressionHandler expressionHandler = new DefaultHttpSecurityExpressionHandler(); + expressionHandler.setApplicationContext(context); + WebExpressionAuthorizationManager manager = WebExpressionAuthorizationManager + .withExpressionHandler(expressionHandler) + .expression("@bean.class.simpleName.startsWith('WebExpression')"); + AuthorizationResult result = manager.authorize(TestAuthentication::authenticatedUser, + new RequestAuthorizationContext(new MockHttpServletRequest())); + assertThat(result.isGranted()).isTrue(); + } + }