diff --git a/core/src/main/java/org/springframework/security/authorization/ExpressionAuthorizationDecision.java b/core/src/main/java/org/springframework/security/authorization/ExpressionAuthorizationDecision.java new file mode 100644 index 0000000000..3c1bd629f0 --- /dev/null +++ b/core/src/main/java/org/springframework/security/authorization/ExpressionAuthorizationDecision.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.authorization; + +import org.springframework.expression.Expression; + +/** + * Represents an {@link AuthorizationDecision} based on a {@link Expression} + * + * @author Marcus Da Coregio + * @since 5.6 + */ +public class ExpressionAuthorizationDecision extends AuthorizationDecision { + + private final Expression expression; + + public ExpressionAuthorizationDecision(boolean granted, Expression expressionAttribute) { + super(granted); + this.expression = expressionAttribute; + } + + public Expression getExpression() { + return this.expression; + } + + @Override + public String toString() { + return getClass().getSimpleName() + " [" + "granted=" + isGranted() + ", expressionAttribute=" + this.expression + + ']'; + } + +} diff --git a/core/src/main/java/org/springframework/security/authorization/method/MethodExpressionAuthorizationManager.java b/core/src/main/java/org/springframework/security/authorization/method/MethodExpressionAuthorizationManager.java index 41f5a9dbb2..01c0cf1ca7 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/MethodExpressionAuthorizationManager.java +++ b/core/src/main/java/org/springframework/security/authorization/method/MethodExpressionAuthorizationManager.java @@ -16,6 +16,72 @@ package org.springframework.security.authorization.method; -public class MethodExpressionAuthorizationManager { +import java.util.function.Supplier; + +import org.aopalliance.intercept.MethodInvocation; + +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.Expression; +import org.springframework.security.access.expression.ExpressionUtils; +import org.springframework.security.access.expression.SecurityExpressionHandler; +import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; +import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.authorization.ExpressionAuthorizationDecision; +import org.springframework.security.core.Authentication; +import org.springframework.util.Assert; + +/** + * An expression-based {@link AuthorizationManager} that determines the access by + * evaluating the provided expression against the {@link MethodInvocation}. + * + * @author Josh Cummings + * @since 5.8 + */ +public final class MethodExpressionAuthorizationManager implements AuthorizationManager { + + private SecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler(); + + private Expression expression; + + /** + * Creates an instance. + * @param expressionString the raw expression string to parse + */ + public MethodExpressionAuthorizationManager(String expressionString) { + Assert.hasText(expressionString, "expressionString cannot be empty"); + this.expression = this.expressionHandler.getExpressionParser().parseExpression(expressionString); + } + + /** + * Sets the {@link SecurityExpressionHandler} to be used. The default is + * {@link DefaultMethodSecurityExpressionHandler}. + * @param expressionHandler the {@link SecurityExpressionHandler} to use + */ + public void setExpressionHandler(SecurityExpressionHandler expressionHandler) { + Assert.notNull(expressionHandler, "expressionHandler cannot be null"); + this.expressionHandler = expressionHandler; + this.expression = expressionHandler.getExpressionParser() + .parseExpression(this.expression.getExpressionString()); + } + + /** + * Determines the access by evaluating the provided expression. + * @param authentication the {@link Supplier} of the {@link Authentication} to check + * @param context the {@link MethodInvocation} to check + * @return an {@link ExpressionAuthorizationDecision} based on the evaluated + * expression + */ + @Override + public AuthorizationDecision check(Supplier authentication, MethodInvocation context) { + EvaluationContext ctx = this.expressionHandler.createEvaluationContext(authentication, context); + boolean granted = ExpressionUtils.evaluateAsBoolean(this.expression, ctx); + return new ExpressionAuthorizationDecision(granted, this.expression); + } + + @Override + public String toString() { + return "WebExpressionAuthorizationManager[expression='" + this.expression + "']"; + } } diff --git a/core/src/test/java/org/springframework/security/authorization/method/MethodExpressionAuthorizationManagerTests.java b/core/src/test/java/org/springframework/security/authorization/method/MethodExpressionAuthorizationManagerTests.java new file mode 100644 index 0000000000..9c36f80387 --- /dev/null +++ b/core/src/test/java/org/springframework/security/authorization/method/MethodExpressionAuthorizationManagerTests.java @@ -0,0 +1,104 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.authorization.method; + +import org.junit.jupiter.api.Test; +import org.junit.platform.commons.util.ReflectionUtils; + +import org.springframework.expression.Expression; +import org.springframework.expression.ExpressionParser; +import org.springframework.security.access.annotation.BusinessService; +import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; +import org.springframework.security.authentication.TestAuthentication; +import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.util.SimpleMethodInvocation; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +class MethodExpressionAuthorizationManagerTests { + + @Test + void instantiateWhenExpressionStringNullThenIllegalArgumentException() { + assertThatIllegalArgumentException().isThrownBy(() -> new MethodExpressionAuthorizationManager(null)) + .withMessage("expressionString cannot be empty"); + } + + @Test + void instantiateWhenExpressionStringEmptyThenIllegalArgumentException() { + assertThatIllegalArgumentException().isThrownBy(() -> new MethodExpressionAuthorizationManager("")) + .withMessage("expressionString cannot be empty"); + } + + @Test + void instantiateWhenExpressionStringBlankThenIllegalArgumentException() { + assertThatIllegalArgumentException().isThrownBy(() -> new MethodExpressionAuthorizationManager(" ")) + .withMessage("expressionString cannot be empty"); + } + + @Test + void instantiateWhenExpressionHandlerNotSetThenDefaultUsed() { + MethodExpressionAuthorizationManager manager = new MethodExpressionAuthorizationManager("hasRole('ADMIN')"); + assertThat(manager).extracting("expressionHandler").isInstanceOf(DefaultMethodSecurityExpressionHandler.class); + } + + @Test + void setExpressionHandlerWhenNullThenIllegalArgumentException() { + MethodExpressionAuthorizationManager manager = new MethodExpressionAuthorizationManager("hasRole('ADMIN')"); + assertThatIllegalArgumentException().isThrownBy(() -> manager.setExpressionHandler(null)) + .withMessage("expressionHandler cannot be null"); + } + + @Test + void setExpressionHandlerWhenNotNullThenVerifyExpressionHandler() { + String expressionString = "hasRole('ADMIN')"; + MethodExpressionAuthorizationManager manager = new MethodExpressionAuthorizationManager(expressionString); + DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler(); + ExpressionParser mockExpressionParser = mock(ExpressionParser.class); + Expression mockExpression = mock(Expression.class); + given(mockExpressionParser.parseExpression(expressionString)).willReturn(mockExpression); + expressionHandler.setExpressionParser(mockExpressionParser); + manager.setExpressionHandler(expressionHandler); + assertThat(manager).extracting("expressionHandler").isEqualTo(expressionHandler); + assertThat(manager).extracting("expression").isEqualTo(mockExpression); + verify(mockExpressionParser).parseExpression(expressionString); + } + + @Test + void checkWhenExpressionHasRoleAdminConfiguredAndRoleAdminThenGrantedDecision() { + MethodExpressionAuthorizationManager manager = new MethodExpressionAuthorizationManager("hasRole('ADMIN')"); + AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedAdmin, + new SimpleMethodInvocation(new Object(), + ReflectionUtils.getRequiredMethod(BusinessService.class, "someAdminMethod"))); + assertThat(decision).isNotNull(); + assertThat(decision.isGranted()).isTrue(); + } + + @Test + void checkWhenExpressionHasRoleAdminConfiguredAndRoleUserThenDeniedDecision() { + MethodExpressionAuthorizationManager manager = new MethodExpressionAuthorizationManager("hasRole('ADMIN')"); + AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedUser, + new SimpleMethodInvocation(new Object(), + ReflectionUtils.getRequiredMethod(BusinessService.class, "someAdminMethod"))); + assertThat(decision).isNotNull(); + assertThat(decision.isGranted()).isFalse(); + } + +}