Skip to content

Commit 624fdfa

Browse files
committed
Add AuthorizationManager for protect-pointcut
Closes gh-11323
1 parent db25a37 commit 624fdfa

File tree

8 files changed

+250
-4
lines changed

8 files changed

+250
-4
lines changed
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/*
2+
* Copyright 2002-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.config.method;
18+
19+
import java.lang.reflect.Method;
20+
import java.util.HashSet;
21+
import java.util.Set;
22+
23+
import org.aspectj.weaver.tools.PointcutExpression;
24+
import org.aspectj.weaver.tools.PointcutParser;
25+
import org.aspectj.weaver.tools.PointcutPrimitive;
26+
27+
import org.springframework.aop.ClassFilter;
28+
import org.springframework.aop.MethodMatcher;
29+
import org.springframework.aop.Pointcut;
30+
31+
class AspectJMethodMatcher implements MethodMatcher, ClassFilter, Pointcut {
32+
33+
private static final PointcutParser parser;
34+
35+
static {
36+
Set<PointcutPrimitive> supportedPrimitives = new HashSet<>(3);
37+
supportedPrimitives.add(PointcutPrimitive.EXECUTION);
38+
supportedPrimitives.add(PointcutPrimitive.ARGS);
39+
supportedPrimitives.add(PointcutPrimitive.REFERENCE);
40+
parser = PointcutParser.getPointcutParserSupportingSpecifiedPrimitivesAndUsingContextClassloaderForResolution(
41+
supportedPrimitives);
42+
}
43+
44+
private final PointcutExpression expression;
45+
46+
AspectJMethodMatcher(String expression) {
47+
this.expression = parser.parsePointcutExpression(expression);
48+
}
49+
50+
@Override
51+
public boolean matches(Class<?> clazz) {
52+
return this.expression.couldMatchJoinPointsInType(clazz);
53+
}
54+
55+
@Override
56+
public boolean matches(Method method, Class<?> targetClass) {
57+
return this.expression.matchesMethodExecution(method).alwaysMatches();
58+
}
59+
60+
@Override
61+
public boolean isRuntime() {
62+
return false;
63+
}
64+
65+
@Override
66+
public boolean matches(Method method, Class<?> targetClass, Object... args) {
67+
return matches(method, targetClass);
68+
}
69+
70+
@Override
71+
public ClassFilter getClassFilter() {
72+
return this;
73+
}
74+
75+
@Override
76+
public MethodMatcher getMethodMatcher() {
77+
return this;
78+
}
79+
80+
}

config/src/main/java/org/springframework/security/config/method/MethodSecurityBeanDefinitionParser.java

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,25 @@
1616

1717
package org.springframework.security.config.method;
1818

19+
import java.util.Collection;
20+
import java.util.List;
21+
import java.util.Map;
22+
1923
import org.apache.commons.logging.Log;
2024
import org.apache.commons.logging.LogFactory;
2125
import org.w3c.dom.Element;
2226

27+
import org.springframework.aop.Pointcut;
2328
import org.springframework.aop.config.AopNamespaceUtils;
29+
import org.springframework.aop.support.Pointcuts;
2430
import org.springframework.beans.BeanMetadataElement;
2531
import org.springframework.beans.BeansException;
2632
import org.springframework.beans.factory.FactoryBean;
2733
import org.springframework.beans.factory.config.BeanDefinition;
2834
import org.springframework.beans.factory.config.RuntimeBeanReference;
2935
import org.springframework.beans.factory.parsing.CompositeComponentDefinition;
3036
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
37+
import org.springframework.beans.factory.support.ManagedMap;
3138
import org.springframework.beans.factory.xml.BeanDefinitionParser;
3239
import org.springframework.beans.factory.xml.ParserContext;
3340
import org.springframework.context.ApplicationContext;
@@ -37,6 +44,7 @@
3744
import org.springframework.security.authorization.method.AuthorizationManagerAfterMethodInterceptor;
3845
import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor;
3946
import org.springframework.security.authorization.method.Jsr250AuthorizationManager;
47+
import org.springframework.security.authorization.method.MethodExpressionAuthorizationManager;
4048
import org.springframework.security.authorization.method.PostAuthorizeAuthorizationManager;
4149
import org.springframework.security.authorization.method.PostFilterAuthorizationMethodInterceptor;
4250
import org.springframework.security.authorization.method.PreAuthorizeAuthorizationManager;
@@ -64,7 +72,11 @@ public class MethodSecurityBeanDefinitionParser implements BeanDefinitionParser
6472

6573
private static final String ATT_USE_PREPOST = "pre-post-enabled";
6674

67-
private static final String ATT_REF = "ref";
75+
private static final String ATT_AUTHORIZATION_MGR = "authorization-manager-ref";
76+
77+
private static final String ATT_ACCESS = "access";
78+
79+
private static final String ATT_EXPRESSION = "expression";
6880

6981
private static final String ATT_SECURITY_CONTEXT_HOLDER_STRATEGY_REF = "security-context-holder-strategy-ref";
7082

@@ -95,7 +107,7 @@ public BeanDefinition parse(Element element, ParserContext pc) {
95107
.addPropertyValue("securityContextHolderStrategy", securityContextHolderStrategy);
96108
Element expressionHandlerElt = DomUtils.getChildElementByTagName(element, Elements.EXPRESSION_HANDLER);
97109
if (expressionHandlerElt != null) {
98-
String expressionHandlerRef = expressionHandlerElt.getAttribute(ATT_REF);
110+
String expressionHandlerRef = expressionHandlerElt.getAttribute("ref");
99111
preFilterInterceptor.addPropertyReference("expressionHandler", expressionHandlerRef);
100112
preAuthorizeInterceptor.addPropertyReference("expressionHandler", expressionHandlerRef);
101113
postAuthorizeInterceptor.addPropertyReference("expressionHandler", expressionHandlerRef);
@@ -137,6 +149,21 @@ public BeanDefinition parse(Element element, ParserContext pc) {
137149
pc.getRegistry().registerBeanDefinition("jsr250AuthorizationMethodInterceptor",
138150
jsr250Interceptor.getBeanDefinition());
139151
}
152+
Map<Pointcut, BeanMetadataElement> managers = new ManagedMap<>();
153+
List<Element> methods = DomUtils.getChildElementsByTagName(element, Elements.PROTECT_POINTCUT);
154+
if (!methods.isEmpty()) {
155+
for (Element protectElt : methods) {
156+
managers.put(pointcut(protectElt), authorizationManager(element, protectElt));
157+
}
158+
BeanDefinitionBuilder protectPointcutInterceptor = BeanDefinitionBuilder
159+
.rootBeanDefinition(AuthorizationManagerBeforeMethodInterceptor.class)
160+
.setRole(BeanDefinition.ROLE_INFRASTRUCTURE)
161+
.addPropertyValue("securityContextHolderStrategy", securityContextHolderStrategy)
162+
.addConstructorArgValue(pointcut(managers.keySet()))
163+
.addConstructorArgValue(authorizationManager(managers));
164+
pc.getRegistry().registerBeanDefinition("protectPointcutInterceptor",
165+
protectPointcutInterceptor.getBeanDefinition());
166+
}
140167
AopNamespaceUtils.registerAutoProxyCreatorIfNecessary(pc, element);
141168
pc.popAndRegisterContainingComponent();
142169
return null;
@@ -150,6 +177,47 @@ private BeanMetadataElement getSecurityContextHolderStrategy(Element methodSecur
150177
return BeanDefinitionBuilder.rootBeanDefinition(SecurityContextHolderStrategyFactory.class).getBeanDefinition();
151178
}
152179

180+
private Pointcut pointcut(Element protectElt) {
181+
String expression = protectElt.getAttribute(ATT_EXPRESSION);
182+
expression = replaceBooleanOperators(expression);
183+
return new AspectJMethodMatcher(expression);
184+
}
185+
186+
private Pointcut pointcut(Collection<Pointcut> pointcuts) {
187+
Pointcut result = null;
188+
for (Pointcut pointcut : pointcuts) {
189+
if (result == null) {
190+
result = pointcut;
191+
}
192+
else {
193+
result = Pointcuts.union(result, pointcut);
194+
}
195+
}
196+
return result;
197+
}
198+
199+
private String replaceBooleanOperators(String expression) {
200+
expression = StringUtils.replace(expression, " and ", " && ");
201+
expression = StringUtils.replace(expression, " or ", " || ");
202+
expression = StringUtils.replace(expression, " not ", " ! ");
203+
return expression;
204+
}
205+
206+
private BeanMetadataElement authorizationManager(Element element, Element protectElt) {
207+
String authorizationManager = element.getAttribute(ATT_AUTHORIZATION_MGR);
208+
if (StringUtils.hasText(authorizationManager)) {
209+
return new RuntimeBeanReference(authorizationManager);
210+
}
211+
String access = protectElt.getAttribute(ATT_ACCESS);
212+
return BeanDefinitionBuilder.rootBeanDefinition(MethodExpressionAuthorizationManager.class)
213+
.addConstructorArgValue(access).getBeanDefinition();
214+
}
215+
216+
private BeanMetadataElement authorizationManager(Map<Pointcut, BeanMetadataElement> managers) {
217+
return BeanDefinitionBuilder.rootBeanDefinition(PointcutDelegatingAuthorizationManager.class)
218+
.addConstructorArgValue(managers).getBeanDefinition();
219+
}
220+
153221
public static final class MethodSecurityExpressionHandlerBean
154222
implements FactoryBean<MethodSecurityExpressionHandler>, ApplicationContextAware {
155223

config/src/main/resources/org/springframework/security/config/spring-security-5.8.rnc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -202,8 +202,8 @@ msmds.attlist &= id?
202202
msmds.attlist &= use-expressions?
203203

204204
method-security =
205-
## 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.
206-
element method-security {method-security.attlist, expression-handler?}
205+
## 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.
206+
element method-security {method-security.attlist, expression-handler?, protect-pointcut*}
207207
method-security.attlist &=
208208
## 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".
209209
attribute pre-post-enabled {xsd:boolean}?

config/src/main/resources/org/springframework/security/config/spring-security-5.8.xsd

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -615,6 +615,8 @@
615615
there is a match, the beans will automatically be proxied and security authorization
616616
applied to the methods accordingly. Interceptors are invoked in the order specified in
617617
AuthorizationInterceptorsOrder. Use can create your own interceptors using Spring AOP.
618+
Also, annotation-based interception can be overridden by expressions listed in
619+
&lt;protect-pointcut&gt; elements.
618620
</xs:documentation>
619621
</xs:annotation>
620622
<xs:complexType>
@@ -630,6 +632,17 @@
630632
<xs:attributeGroup ref="security:ref"/>
631633
</xs:complexType>
632634
</xs:element>
635+
<xs:element minOccurs="0" maxOccurs="unbounded" name="protect-pointcut">
636+
<xs:annotation>
637+
<xs:documentation>Defines a protected pointcut and the access control configuration attributes that apply to
638+
it. Every bean registered in the Spring application context that provides a method that
639+
matches the pointcut will receive security authorization.
640+
</xs:documentation>
641+
</xs:annotation>
642+
<xs:complexType>
643+
<xs:attributeGroup ref="security:protect-pointcut.attlist"/>
644+
</xs:complexType>
645+
</xs:element>
633646
</xs:sequence>
634647
<xs:attributeGroup ref="security:method-security.attlist"/>
635648
</xs:complexType>

config/src/test/java/org/springframework/security/config/method/MethodSecurityBeanDefinitionParserTests.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import org.springframework.security.access.AccessDeniedException;
3535
import org.springframework.security.access.PermissionEvaluator;
3636
import org.springframework.security.access.annotation.BusinessService;
37+
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
3738
import org.springframework.security.authentication.TestingAuthenticationToken;
3839
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
3940
import org.springframework.security.authorization.AuthorizationDecision;
@@ -42,6 +43,7 @@
4243
import org.springframework.security.config.test.SpringTestContext;
4344
import org.springframework.security.config.test.SpringTestContextExtension;
4445
import org.springframework.security.core.Authentication;
46+
import org.springframework.security.core.authority.AuthorityUtils;
4547
import org.springframework.security.core.context.SecurityContext;
4648
import org.springframework.security.core.context.SecurityContextHolder;
4749
import org.springframework.security.core.context.SecurityContextHolderStrategy;
@@ -401,6 +403,28 @@ public void repeatedSecuredAnnotationsWhenPresentThenFails() {
401403
.isThrownBy(() -> this.businessService.repeatedAnnotations());
402404
}
403405

406+
@WithMockUser
407+
@Test
408+
public void supportsMethodArgumentsInPointcut() {
409+
this.spring.configLocations(xml("ProtectPointcut")).autowire();
410+
this.businessService.someOther(0);
411+
assertThatExceptionOfType(AccessDeniedException.class)
412+
.isThrownBy(() -> this.businessService.someOther("somestring"));
413+
}
414+
415+
@Test
416+
public void supportsBooleanPointcutExpressions() {
417+
this.spring.configLocations(xml("ProtectPointcutBoolean")).autowire();
418+
this.businessService.someOther("somestring");
419+
// All others should require ROLE_USER
420+
assertThatExceptionOfType(AuthenticationCredentialsNotFoundException.class)
421+
.isThrownBy(() -> this.businessService.someOther(0));
422+
SecurityContextHolder.getContext().setAuthentication(
423+
new TestingAuthenticationToken("user", "password", AuthorityUtils.createAuthorityList("ROLE_USER")));
424+
this.businessService.someOther(0);
425+
SecurityContextHolder.clearContext();
426+
}
427+
404428
private static String xml(String configName) {
405429
return CONFIG_LOCATION_PREFIX + "-" + configName + ".xml";
406430
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!--
3+
~ Copyright 2002-2021 the original author or authors.
4+
~
5+
~ Licensed under the Apache License, Version 2.0 (the "License");
6+
~ you may not use this file except in compliance with the License.
7+
~ You may obtain a copy of the License at
8+
~
9+
~ https://www.apache.org/licenses/LICENSE-2.0
10+
~
11+
~ Unless required by applicable law or agreed to in writing, software
12+
~ distributed under the License is distributed on an "AS IS" BASIS,
13+
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
~ See the License for the specific language governing permissions and
15+
~ limitations under the License.
16+
-->
17+
18+
<b:beans xmlns:b="http://www.springframework.org/schema/beans"
19+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
20+
xmlns="http://www.springframework.org/schema/security"
21+
xsi:schemaLocation="http://www.springframework.org/schema/security https://www.springframework.org/schema/security/spring-security.xsd
22+
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">
23+
24+
<b:bean class="org.springframework.security.access.annotation.ExpressionProtectedBusinessServiceImpl"/>
25+
26+
<method-security>
27+
<protect-pointcut expression="execution(* org.springframework.security.access.annotation.BusinessService.someOther(String))" access="hasRole('ADMIN')"/>
28+
<protect-pointcut expression="execution(* org.springframework.security.access.annotation.BusinessService.*(..))" access="hasRole('USER')"/>
29+
</method-security>
30+
</b:beans>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!--
3+
~ Copyright 2002-2021 the original author or authors.
4+
~
5+
~ Licensed under the Apache License, Version 2.0 (the "License");
6+
~ you may not use this file except in compliance with the License.
7+
~ You may obtain a copy of the License at
8+
~
9+
~ https://www.apache.org/licenses/LICENSE-2.0
10+
~
11+
~ Unless required by applicable law or agreed to in writing, software
12+
~ distributed under the License is distributed on an "AS IS" BASIS,
13+
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
~ See the License for the specific language governing permissions and
15+
~ limitations under the License.
16+
-->
17+
18+
<b:beans xmlns:b="http://www.springframework.org/schema/beans"
19+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
20+
xmlns="http://www.springframework.org/schema/security"
21+
xsi:schemaLocation="http://www.springframework.org/schema/security https://www.springframework.org/schema/security/spring-security.xsd
22+
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">
23+
24+
<b:bean class="org.springframework.security.access.annotation.ExpressionProtectedBusinessServiceImpl"/>
25+
26+
<method-security>
27+
<protect-pointcut expression="execution(* org.springframework.security.access.annotation.BusinessService.*(..)) and not execution(* org.springframework.security.access.annotation.BusinessService.someOther(String)))" access="hasRole('USER')"/>
28+
</method-security>
29+
</b:beans>

docs/modules/ROOT/pages/servlet/appendix/namespace/method-security.adoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ Defaults to the value returned by SecurityContextHolder.getContextHolderStrategy
3737
=== Child Elements of <method-security>
3838

3939
* xref:servlet/appendix/namespace/http.adoc#nsa-expression-handler[expression-handler]
40+
* <<nsa-protect-pointcut,protect-pointcut>>
4041

4142
[[nsa-global-method-security]]
4243
== <global-method-security>
@@ -244,6 +245,7 @@ You can find an example in the xref:servlet/authorization/method-security.adoc#n
244245

245246

246247
* <<nsa-global-method-security,global-method-security>>
248+
* <<nsa-method-security,method-security>>
247249

248250

249251

0 commit comments

Comments
 (0)