diff --git a/config/src/main/java/org/springframework/security/config/method/AspectJMethodMatcher.java b/config/src/main/java/org/springframework/security/config/method/AspectJMethodMatcher.java new file mode 100644 index 0000000000..b3c10a9b03 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/method/AspectJMethodMatcher.java @@ -0,0 +1,80 @@ +/* + * 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.config.method; + +import java.lang.reflect.Method; +import java.util.HashSet; +import java.util.Set; + +import org.aspectj.weaver.tools.PointcutExpression; +import org.aspectj.weaver.tools.PointcutParser; +import org.aspectj.weaver.tools.PointcutPrimitive; + +import org.springframework.aop.ClassFilter; +import org.springframework.aop.MethodMatcher; +import org.springframework.aop.Pointcut; + +class AspectJMethodMatcher implements MethodMatcher, ClassFilter, Pointcut { + + private static final PointcutParser parser; + + static { + Set supportedPrimitives = new HashSet<>(3); + supportedPrimitives.add(PointcutPrimitive.EXECUTION); + supportedPrimitives.add(PointcutPrimitive.ARGS); + supportedPrimitives.add(PointcutPrimitive.REFERENCE); + parser = PointcutParser.getPointcutParserSupportingSpecifiedPrimitivesAndUsingContextClassloaderForResolution( + supportedPrimitives); + } + + private final PointcutExpression expression; + + AspectJMethodMatcher(String expression) { + this.expression = parser.parsePointcutExpression(expression); + } + + @Override + public boolean matches(Class clazz) { + return this.expression.couldMatchJoinPointsInType(clazz); + } + + @Override + public boolean matches(Method method, Class targetClass) { + return this.expression.matchesMethodExecution(method).alwaysMatches(); + } + + @Override + public boolean isRuntime() { + return false; + } + + @Override + public boolean matches(Method method, Class targetClass, Object... args) { + return matches(method, targetClass); + } + + @Override + public ClassFilter getClassFilter() { + return this; + } + + @Override + public MethodMatcher getMethodMatcher() { + return this; + } + +} diff --git a/config/src/main/java/org/springframework/security/config/method/MethodSecurityBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/method/MethodSecurityBeanDefinitionParser.java index 76c4fa82df..2869e719e0 100644 --- a/config/src/main/java/org/springframework/security/config/method/MethodSecurityBeanDefinitionParser.java +++ b/config/src/main/java/org/springframework/security/config/method/MethodSecurityBeanDefinitionParser.java @@ -16,11 +16,17 @@ package org.springframework.security.config.method; +import java.util.Collection; +import java.util.List; +import java.util.Map; + import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.w3c.dom.Element; +import org.springframework.aop.Pointcut; import org.springframework.aop.config.AopNamespaceUtils; +import org.springframework.aop.support.Pointcuts; import org.springframework.beans.BeanMetadataElement; import org.springframework.beans.BeansException; import org.springframework.beans.factory.FactoryBean; @@ -28,6 +34,7 @@ import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.RuntimeBeanReference; import org.springframework.beans.factory.parsing.CompositeComponentDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.ManagedMap; import org.springframework.beans.factory.xml.BeanDefinitionParser; import org.springframework.beans.factory.xml.ParserContext; import org.springframework.context.ApplicationContext; @@ -37,6 +44,7 @@ import org.springframework.security.access.expression.method.MethodSecurityExpre import org.springframework.security.authorization.method.AuthorizationManagerAfterMethodInterceptor; import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor; import org.springframework.security.authorization.method.Jsr250AuthorizationManager; +import org.springframework.security.authorization.method.MethodExpressionAuthorizationManager; import org.springframework.security.authorization.method.PostAuthorizeAuthorizationManager; import org.springframework.security.authorization.method.PostFilterAuthorizationMethodInterceptor; import org.springframework.security.authorization.method.PreAuthorizeAuthorizationManager; @@ -64,7 +72,11 @@ public class MethodSecurityBeanDefinitionParser implements BeanDefinitionParser private static final String ATT_USE_PREPOST = "pre-post-enabled"; - private static final String ATT_REF = "ref"; + private static final String ATT_AUTHORIZATION_MGR = "authorization-manager-ref"; + + private static final String ATT_ACCESS = "access"; + + private static final String ATT_EXPRESSION = "expression"; private static final String ATT_SECURITY_CONTEXT_HOLDER_STRATEGY_REF = "security-context-holder-strategy-ref"; @@ -95,7 +107,7 @@ public class MethodSecurityBeanDefinitionParser implements BeanDefinitionParser .addPropertyValue("securityContextHolderStrategy", securityContextHolderStrategy); Element expressionHandlerElt = DomUtils.getChildElementByTagName(element, Elements.EXPRESSION_HANDLER); if (expressionHandlerElt != null) { - String expressionHandlerRef = expressionHandlerElt.getAttribute(ATT_REF); + String expressionHandlerRef = expressionHandlerElt.getAttribute("ref"); preFilterInterceptor.addPropertyReference("expressionHandler", expressionHandlerRef); preAuthorizeInterceptor.addPropertyReference("expressionHandler", expressionHandlerRef); postAuthorizeInterceptor.addPropertyReference("expressionHandler", expressionHandlerRef); @@ -137,6 +149,21 @@ public class MethodSecurityBeanDefinitionParser implements BeanDefinitionParser pc.getRegistry().registerBeanDefinition("jsr250AuthorizationMethodInterceptor", jsr250Interceptor.getBeanDefinition()); } + Map managers = new ManagedMap<>(); + List methods = DomUtils.getChildElementsByTagName(element, Elements.PROTECT_POINTCUT); + if (!methods.isEmpty()) { + for (Element protectElt : methods) { + managers.put(pointcut(protectElt), authorizationManager(element, protectElt)); + } + BeanDefinitionBuilder protectPointcutInterceptor = BeanDefinitionBuilder + .rootBeanDefinition(AuthorizationManagerBeforeMethodInterceptor.class) + .setRole(BeanDefinition.ROLE_INFRASTRUCTURE) + .addPropertyValue("securityContextHolderStrategy", securityContextHolderStrategy) + .addConstructorArgValue(pointcut(managers.keySet())) + .addConstructorArgValue(authorizationManager(managers)); + pc.getRegistry().registerBeanDefinition("protectPointcutInterceptor", + protectPointcutInterceptor.getBeanDefinition()); + } AopNamespaceUtils.registerAutoProxyCreatorIfNecessary(pc, element); pc.popAndRegisterContainingComponent(); return null; @@ -150,6 +177,47 @@ public class MethodSecurityBeanDefinitionParser implements BeanDefinitionParser return BeanDefinitionBuilder.rootBeanDefinition(SecurityContextHolderStrategyFactory.class).getBeanDefinition(); } + private Pointcut pointcut(Element protectElt) { + String expression = protectElt.getAttribute(ATT_EXPRESSION); + expression = replaceBooleanOperators(expression); + return new AspectJMethodMatcher(expression); + } + + private Pointcut pointcut(Collection pointcuts) { + Pointcut result = null; + for (Pointcut pointcut : pointcuts) { + if (result == null) { + result = pointcut; + } + else { + result = Pointcuts.union(result, pointcut); + } + } + return result; + } + + private String replaceBooleanOperators(String expression) { + expression = StringUtils.replace(expression, " and ", " && "); + expression = StringUtils.replace(expression, " or ", " || "); + expression = StringUtils.replace(expression, " not ", " ! "); + return expression; + } + + private BeanMetadataElement authorizationManager(Element element, Element protectElt) { + String authorizationManager = element.getAttribute(ATT_AUTHORIZATION_MGR); + if (StringUtils.hasText(authorizationManager)) { + return new RuntimeBeanReference(authorizationManager); + } + String access = protectElt.getAttribute(ATT_ACCESS); + return BeanDefinitionBuilder.rootBeanDefinition(MethodExpressionAuthorizationManager.class) + .addConstructorArgValue(access).getBeanDefinition(); + } + + private BeanMetadataElement authorizationManager(Map managers) { + return BeanDefinitionBuilder.rootBeanDefinition(PointcutDelegatingAuthorizationManager.class) + .addConstructorArgValue(managers).getBeanDefinition(); + } + public static final class MethodSecurityExpressionHandlerBean implements FactoryBean, ApplicationContextAware { diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-5.8.rnc b/config/src/main/resources/org/springframework/security/config/spring-security-5.8.rnc index af8b10425a..a6b80f163e 100644 --- a/config/src/main/resources/org/springframework/security/config/spring-security-5.8.rnc +++ b/config/src/main/resources/org/springframework/security/config/spring-security-5.8.rnc @@ -202,8 +202,8 @@ msmds.attlist &= id? msmds.attlist &= use-expressions? method-security = - ## Provides method security for all beans registered in the Spring application context. Specifically, beans will be scanned for matches with Spring Security annotations. Where there is a match, the beans will automatically be proxied and security authorization applied to the methods accordingly. Interceptors are invoked in the order specified in AuthorizationInterceptorsOrder. Use can create your own interceptors using Spring AOP. - element method-security {method-security.attlist, expression-handler?} + ## Provides method security for all beans registered in the Spring application context. Specifically, beans will be scanned for matches with Spring Security annotations. Where there is a match, the beans will automatically be proxied and security authorization applied to the methods accordingly. Interceptors are invoked in the order specified in AuthorizationInterceptorsOrder. Use can create your own interceptors using Spring AOP. Also, annotation-based interception can be overridden by expressions listed in elements. + element method-security {method-security.attlist, expression-handler?, protect-pointcut*} method-security.attlist &= ## Specifies whether the use of Spring Security's pre and post invocation annotations (@PreFilter, @PreAuthorize, @PostFilter, @PostAuthorize) should be enabled for this application context. Defaults to "true". attribute pre-post-enabled {xsd:boolean}? diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-5.8.xsd b/config/src/main/resources/org/springframework/security/config/spring-security-5.8.xsd index 29e56d18b0..4d5e16a304 100644 --- a/config/src/main/resources/org/springframework/security/config/spring-security-5.8.xsd +++ b/config/src/main/resources/org/springframework/security/config/spring-security-5.8.xsd @@ -615,6 +615,8 @@ there is a match, the beans will automatically be proxied and security authorization applied to the methods accordingly. Interceptors are invoked in the order specified in AuthorizationInterceptorsOrder. Use can create your own interceptors using Spring AOP. + Also, annotation-based interception can be overridden by expressions listed in + <protect-pointcut> elements. @@ -630,6 +632,17 @@ + + + Defines a protected pointcut and the access control configuration attributes that apply to + it. Every bean registered in the Spring application context that provides a method that + matches the pointcut will receive security authorization. + + + + + + diff --git a/config/src/test/java/org/springframework/security/config/method/MethodSecurityBeanDefinitionParserTests.java b/config/src/test/java/org/springframework/security/config/method/MethodSecurityBeanDefinitionParserTests.java index ea4b60c5ad..e1fd044a50 100644 --- a/config/src/test/java/org/springframework/security/config/method/MethodSecurityBeanDefinitionParserTests.java +++ b/config/src/test/java/org/springframework/security/config/method/MethodSecurityBeanDefinitionParserTests.java @@ -34,6 +34,7 @@ import org.springframework.lang.Nullable; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.PermissionEvaluator; import org.springframework.security.access.annotation.BusinessService; +import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authorization.AuthorizationDecision; @@ -42,6 +43,7 @@ import org.springframework.security.config.annotation.method.configuration.Metho import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.config.test.SpringTestContextExtension; import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolderStrategy; @@ -401,6 +403,28 @@ public class MethodSecurityBeanDefinitionParserTests { .isThrownBy(() -> this.businessService.repeatedAnnotations()); } + @WithMockUser + @Test + public void supportsMethodArgumentsInPointcut() { + this.spring.configLocations(xml("ProtectPointcut")).autowire(); + this.businessService.someOther(0); + assertThatExceptionOfType(AccessDeniedException.class) + .isThrownBy(() -> this.businessService.someOther("somestring")); + } + + @Test + public void supportsBooleanPointcutExpressions() { + this.spring.configLocations(xml("ProtectPointcutBoolean")).autowire(); + this.businessService.someOther("somestring"); + // All others should require ROLE_USER + assertThatExceptionOfType(AuthenticationCredentialsNotFoundException.class) + .isThrownBy(() -> this.businessService.someOther(0)); + SecurityContextHolder.getContext().setAuthentication( + new TestingAuthenticationToken("user", "password", AuthorityUtils.createAuthorityList("ROLE_USER"))); + this.businessService.someOther(0); + SecurityContextHolder.clearContext(); + } + private static String xml(String configName) { return CONFIG_LOCATION_PREFIX + "-" + configName + ".xml"; } diff --git a/config/src/test/resources/org/springframework/security/config/method/MethodSecurityBeanDefinitionParserTests-ProtectPointcut.xml b/config/src/test/resources/org/springframework/security/config/method/MethodSecurityBeanDefinitionParserTests-ProtectPointcut.xml new file mode 100644 index 0000000000..529e4c62ea --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/method/MethodSecurityBeanDefinitionParserTests-ProtectPointcut.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/method/MethodSecurityBeanDefinitionParserTests-ProtectPointcutBoolean.xml b/config/src/test/resources/org/springframework/security/config/method/MethodSecurityBeanDefinitionParserTests-ProtectPointcutBoolean.xml new file mode 100644 index 0000000000..b2a8595879 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/method/MethodSecurityBeanDefinitionParserTests-ProtectPointcutBoolean.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + diff --git a/docs/modules/ROOT/pages/servlet/appendix/namespace/method-security.adoc b/docs/modules/ROOT/pages/servlet/appendix/namespace/method-security.adoc index 995007a40d..0298175c05 100644 --- a/docs/modules/ROOT/pages/servlet/appendix/namespace/method-security.adoc +++ b/docs/modules/ROOT/pages/servlet/appendix/namespace/method-security.adoc @@ -37,6 +37,7 @@ Defaults to the value returned by SecurityContextHolder.getContextHolderStrategy === Child Elements of * xref:servlet/appendix/namespace/http.adoc#nsa-expression-handler[expression-handler] +* <> [[nsa-global-method-security]] == @@ -244,6 +245,7 @@ You can find an example in the xref:servlet/authorization/method-security.adoc#n * <> +* <>