Explorar el Código

Add WebExpressionAuthorizationManager.Builder

Closes gh-17504
Josh Cummings hace 1 mes
padre
commit
dadf10899c

+ 49 - 1
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

+ 86 - 0
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<RequestAuthorizationContext> 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<RequestAuthorizationContext> 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.
+	 *
+	 * <p>
+	 * 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<RequestAuthorizationContext> expressionHandler) {
+		return new Builder(expressionHandler);
+	}
+
+	/**
+	 * A {@link Builder} for constructing {@link WebExpressionAuthorizationManager}
+	 * instances.
+	 *
+	 * <p>
+	 * May be reused to create multiple instances.
+	 *
+	 * @author Josh Cummings
+	 * @since 7.0
+	 */
+	public static final class Builder implements ApplicationContextAware {
+
+		private final SecurityExpressionHandler<RequestAuthorizationContext> expressionHandler;
+
+		private final boolean defaultExpressionHandler;
+
+		private Builder() {
+			this.expressionHandler = new DefaultHttpSecurityExpressionHandler();
+			this.defaultExpressionHandler = true;
+		}
+
+		private Builder(SecurityExpressionHandler<RequestAuthorizationContext> 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);
+			}
+		}
+
+	}
+
 }

+ 46 - 0
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();
+	}
+
 }