Add AuthorizationManager for protect-pointcut

Closes gh-11323
This commit is contained in:
Josh Cummings 2022-07-12 17:10:17 -06:00
parent db25a37320
commit 624fdfa731
No known key found for this signature in database
GPG Key ID: A306A51F43B8E5A5
8 changed files with 250 additions and 4 deletions

View File

@ -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<PointcutPrimitive> 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;
}
}

View File

@ -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<Pointcut, BeanMetadataElement> managers = new ManagedMap<>();
List<Element> 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<Pointcut> 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<Pointcut, BeanMetadataElement> managers) {
return BeanDefinitionBuilder.rootBeanDefinition(PointcutDelegatingAuthorizationManager.class)
.addConstructorArgValue(managers).getBeanDefinition();
}
public static final class MethodSecurityExpressionHandlerBean
implements FactoryBean<MethodSecurityExpressionHandler>, ApplicationContextAware {

View File

@ -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 <protect-pointcut> 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}?

View File

@ -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
&lt;protect-pointcut&gt; elements.
</xs:documentation>
</xs:annotation>
<xs:complexType>
@ -630,6 +632,17 @@
<xs:attributeGroup ref="security:ref"/>
</xs:complexType>
</xs:element>
<xs:element minOccurs="0" maxOccurs="unbounded" name="protect-pointcut">
<xs:annotation>
<xs:documentation>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.
</xs:documentation>
</xs:annotation>
<xs:complexType>
<xs:attributeGroup ref="security:protect-pointcut.attlist"/>
</xs:complexType>
</xs:element>
</xs:sequence>
<xs:attributeGroup ref="security:method-security.attlist"/>
</xs:complexType>

View File

@ -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";
}

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ 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.
-->
<b:beans xmlns:b="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.springframework.org/schema/security"
xsi:schemaLocation="http://www.springframework.org/schema/security https://www.springframework.org/schema/security/spring-security.xsd
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">
<b:bean class="org.springframework.security.access.annotation.ExpressionProtectedBusinessServiceImpl"/>
<method-security>
<protect-pointcut expression="execution(* org.springframework.security.access.annotation.BusinessService.someOther(String))" access="hasRole('ADMIN')"/>
<protect-pointcut expression="execution(* org.springframework.security.access.annotation.BusinessService.*(..))" access="hasRole('USER')"/>
</method-security>
</b:beans>

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ 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.
-->
<b:beans xmlns:b="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.springframework.org/schema/security"
xsi:schemaLocation="http://www.springframework.org/schema/security https://www.springframework.org/schema/security/spring-security.xsd
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">
<b:bean class="org.springframework.security.access.annotation.ExpressionProtectedBusinessServiceImpl"/>
<method-security>
<protect-pointcut expression="execution(* org.springframework.security.access.annotation.BusinessService.*(..)) and not execution(* org.springframework.security.access.annotation.BusinessService.someOther(String)))" access="hasRole('USER')"/>
</method-security>
</b:beans>

View File

@ -37,6 +37,7 @@ Defaults to the value returned by SecurityContextHolder.getContextHolderStrategy
=== Child Elements of <method-security>
* xref:servlet/appendix/namespace/http.adoc#nsa-expression-handler[expression-handler]
* <<nsa-protect-pointcut,protect-pointcut>>
[[nsa-global-method-security]]
== <global-method-security>
@ -244,6 +245,7 @@ You can find an example in the xref:servlet/authorization/method-security.adoc#n
* <<nsa-global-method-security,global-method-security>>
* <<nsa-method-security,method-security>>