Skip to content

Commit 2afa931

Browse files
committed
Use AuthorizationManager in <http>
Closes gh-11305
1 parent e125a76 commit 2afa931

File tree

24 files changed

+1013
-1
lines changed

24 files changed

+1013
-1
lines changed
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
/*
2+
* Copyright 2002-2016 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.http;
18+
19+
import java.util.List;
20+
import java.util.Map;
21+
22+
import jakarta.servlet.http.HttpServletRequest;
23+
import org.w3c.dom.Element;
24+
25+
import org.springframework.beans.BeanMetadataElement;
26+
import org.springframework.beans.factory.config.BeanDefinition;
27+
import org.springframework.beans.factory.config.RuntimeBeanReference;
28+
import org.springframework.beans.factory.parsing.BeanComponentDefinition;
29+
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
30+
import org.springframework.beans.factory.support.ManagedMap;
31+
import org.springframework.beans.factory.xml.AbstractBeanDefinitionParser;
32+
import org.springframework.beans.factory.xml.BeanDefinitionParser;
33+
import org.springframework.beans.factory.xml.ParserContext;
34+
import org.springframework.beans.factory.xml.XmlReaderContext;
35+
import org.springframework.security.authorization.AuthenticatedAuthorizationManager;
36+
import org.springframework.security.authorization.AuthorizationManager;
37+
import org.springframework.security.config.Elements;
38+
import org.springframework.security.web.access.expression.DefaultHttpSecurityExpressionHandler;
39+
import org.springframework.security.web.access.expression.WebExpressionAuthorizationManager;
40+
import org.springframework.security.web.access.intercept.AuthorizationFilter;
41+
import org.springframework.security.web.access.intercept.RequestAuthorizationContext;
42+
import org.springframework.security.web.access.intercept.RequestMatcherDelegatingAuthorizationManager;
43+
import org.springframework.security.web.util.matcher.AnyRequestMatcher;
44+
import org.springframework.security.web.util.matcher.RequestMatcher;
45+
import org.springframework.util.StringUtils;
46+
import org.springframework.util.xml.DomUtils;
47+
48+
class AuthorizationFilterParser implements BeanDefinitionParser {
49+
50+
private static final String ATT_USE_EXPRESSIONS = "use-expressions";
51+
52+
private static final String ATT_HTTP_METHOD = "method";
53+
54+
private static final String ATT_PATTERN = "pattern";
55+
56+
private static final String ATT_ACCESS = "access";
57+
58+
private static final String ATT_SERVLET_PATH = "servlet-path";
59+
60+
private String authorizationManagerRef;
61+
62+
@Override
63+
public BeanDefinition parse(Element element, ParserContext parserContext) {
64+
if (!isUseExpressions(element)) {
65+
parserContext.getReaderContext().error("AuthorizationManager must be used with `use-expressions=\"true\"",
66+
element);
67+
return null;
68+
}
69+
this.authorizationManagerRef = createAuthorizationManager(element, parserContext);
70+
BeanDefinitionBuilder filterBuilder = BeanDefinitionBuilder.rootBeanDefinition(AuthorizationFilter.class);
71+
filterBuilder.getRawBeanDefinition().setSource(parserContext.extractSource(element));
72+
BeanDefinition filter = filterBuilder.addConstructorArgReference(this.authorizationManagerRef)
73+
.getBeanDefinition();
74+
String id = element.getAttribute(AbstractBeanDefinitionParser.ID_ATTRIBUTE);
75+
if (StringUtils.hasText(id)) {
76+
parserContext.registerComponent(new BeanComponentDefinition(filter, id));
77+
parserContext.getRegistry().registerBeanDefinition(id, filter);
78+
}
79+
return filter;
80+
}
81+
82+
String getAuthorizationManagerRef() {
83+
return this.authorizationManagerRef;
84+
}
85+
86+
private String createAuthorizationManager(Element element, ParserContext parserContext) {
87+
XmlReaderContext context = parserContext.getReaderContext();
88+
String authorizationManagerRef = element.getAttribute("authorization-manager-ref");
89+
if (StringUtils.hasText(authorizationManagerRef)) {
90+
return authorizationManagerRef;
91+
}
92+
Element expressionHandlerElt = DomUtils.getChildElementByTagName(element, Elements.EXPRESSION_HANDLER);
93+
String expressionHandlerRef = (expressionHandlerElt != null) ? expressionHandlerElt.getAttribute("ref") : null;
94+
if (expressionHandlerRef == null) {
95+
expressionHandlerRef = registerDefaultExpressionHandler(parserContext);
96+
}
97+
MatcherType matcherType = MatcherType.fromElement(element);
98+
ManagedMap<BeanMetadataElement, BeanDefinition> matcherToExpression = new ManagedMap<>();
99+
List<Element> interceptMessages = DomUtils.getChildElementsByTagName(element, Elements.INTERCEPT_URL);
100+
for (Element interceptMessage : interceptMessages) {
101+
String accessExpression = interceptMessage.getAttribute(ATT_ACCESS);
102+
BeanDefinitionBuilder authorizationManager = BeanDefinitionBuilder
103+
.rootBeanDefinition(WebExpressionAuthorizationManager.class);
104+
authorizationManager.addPropertyReference("expressionHandler", expressionHandlerRef);
105+
authorizationManager.addConstructorArgValue(accessExpression);
106+
BeanMetadataElement matcher = createMatcher(matcherType, interceptMessage, parserContext);
107+
matcherToExpression.put(matcher, authorizationManager.getBeanDefinition());
108+
}
109+
BeanDefinitionBuilder mds = BeanDefinitionBuilder
110+
.rootBeanDefinition(RequestMatcherDelegatingAuthorizationManagerFactory.class);
111+
mds.setFactoryMethod("createRequestMatcherDelegatingAuthorizationManager");
112+
mds.addConstructorArgValue(matcherToExpression);
113+
return context.registerWithGeneratedName(mds.getBeanDefinition());
114+
}
115+
116+
private BeanMetadataElement createMatcher(MatcherType matcherType, Element urlElt, ParserContext parserContext) {
117+
String path = urlElt.getAttribute(ATT_PATTERN);
118+
String matcherRef = urlElt.getAttribute(HttpSecurityBeanDefinitionParser.ATT_REQUEST_MATCHER_REF);
119+
boolean hasMatcherRef = StringUtils.hasText(matcherRef);
120+
if (!hasMatcherRef && !StringUtils.hasText(path)) {
121+
parserContext.getReaderContext().error("path attribute cannot be empty or null", urlElt);
122+
}
123+
String method = urlElt.getAttribute(ATT_HTTP_METHOD);
124+
if (!StringUtils.hasText(method)) {
125+
method = null;
126+
}
127+
String servletPath = urlElt.getAttribute(ATT_SERVLET_PATH);
128+
if (!StringUtils.hasText(servletPath)) {
129+
servletPath = null;
130+
}
131+
else if (!MatcherType.mvc.equals(matcherType)) {
132+
parserContext.getReaderContext().error(
133+
ATT_SERVLET_PATH + " is not applicable for request-matcher: '" + matcherType.name() + "'", urlElt);
134+
}
135+
return hasMatcherRef ? new RuntimeBeanReference(matcherRef)
136+
: matcherType.createMatcher(parserContext, path, method, servletPath);
137+
}
138+
139+
String registerDefaultExpressionHandler(ParserContext pc) {
140+
BeanDefinition expressionHandler = GrantedAuthorityDefaultsParserUtils.registerWithDefaultRolePrefix(pc,
141+
DefaultWebSecurityExpressionHandlerBeanFactory.class);
142+
String expressionHandlerRef = pc.getReaderContext().generateBeanName(expressionHandler);
143+
pc.registerBeanComponent(new BeanComponentDefinition(expressionHandler, expressionHandlerRef));
144+
return expressionHandlerRef;
145+
}
146+
147+
boolean isUseExpressions(Element elt) {
148+
String useExpressions = elt.getAttribute(ATT_USE_EXPRESSIONS);
149+
return !StringUtils.hasText(useExpressions) || "true".equals(useExpressions);
150+
}
151+
152+
private static class RequestMatcherDelegatingAuthorizationManagerFactory {
153+
154+
private static AuthorizationManager<HttpServletRequest> createRequestMatcherDelegatingAuthorizationManager(
155+
Map<RequestMatcher, AuthorizationManager<RequestAuthorizationContext>> beans) {
156+
RequestMatcherDelegatingAuthorizationManager.Builder builder = RequestMatcherDelegatingAuthorizationManager
157+
.builder();
158+
for (Map.Entry<RequestMatcher, AuthorizationManager<RequestAuthorizationContext>> entry : beans
159+
.entrySet()) {
160+
builder.add(entry.getKey(), entry.getValue());
161+
}
162+
return builder.add(AnyRequestMatcher.INSTANCE, AuthenticatedAuthorizationManager.authenticated()).build();
163+
}
164+
165+
}
166+
167+
static class DefaultWebSecurityExpressionHandlerBeanFactory
168+
extends GrantedAuthorityDefaultsParserUtils.AbstractGrantedAuthorityDefaultsBeanFactory {
169+
170+
private DefaultHttpSecurityExpressionHandler handler = new DefaultHttpSecurityExpressionHandler();
171+
172+
@Override
173+
public DefaultHttpSecurityExpressionHandler getBean() {
174+
this.handler.setDefaultRolePrefix(this.rolePrefix);
175+
return this.handler;
176+
}
177+
178+
}
179+
180+
}

config/src/main/java/org/springframework/security/config/http/HttpConfigurationBuilder.java

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import org.springframework.security.config.Elements;
4141
import org.springframework.security.config.http.GrantedAuthorityDefaultsParserUtils.AbstractGrantedAuthorityDefaultsBeanFactory;
4242
import org.springframework.security.core.session.SessionRegistryImpl;
43+
import org.springframework.security.web.access.AuthorizationManagerWebInvocationPrivilegeEvaluator;
4344
import org.springframework.security.web.access.DefaultWebInvocationPrivilegeEvaluator;
4445
import org.springframework.security.web.access.channel.ChannelDecisionManagerImpl;
4546
import org.springframework.security.web.access.channel.ChannelProcessingFilter;
@@ -112,6 +113,10 @@ class HttpConfigurationBuilder {
112113

113114
private static final String ATT_DISABLE_URL_REWRITING = "disable-url-rewriting";
114115

116+
private static final String ATT_USE_AUTHORIZATION_MGR = "use-authorization-manager";
117+
118+
private static final String ATT_AUTHORIZATION_MGR = "authorization-manager-ref";
119+
115120
private static final String ATT_ACCESS_MGR = "access-decision-manager-ref";
116121

117122
private static final String ATT_ONCE_PER_REQUEST = "once-per-request";
@@ -219,7 +224,7 @@ class HttpConfigurationBuilder {
219224
createServletApiFilter(authenticationManager);
220225
createJaasApiFilter();
221226
createChannelProcessingFilter();
222-
createFilterSecurityInterceptor(authenticationManager);
227+
createFilterSecurity(authenticationManager);
223228
createAddHeadersFilter();
224229
createCorsFilter();
225230
createWellKnownChangePasswordRedirectFilter();
@@ -674,6 +679,35 @@ private void createRequestCacheFilter() {
674679
this.requestCacheAwareFilter.getConstructorArgumentValues().addGenericArgumentValue(this.requestCache);
675680
}
676681

682+
private void createFilterSecurity(BeanReference authManager) {
683+
boolean useAuthorizationManager = Boolean.parseBoolean(this.httpElt.getAttribute(ATT_USE_AUTHORIZATION_MGR));
684+
if (useAuthorizationManager) {
685+
createAuthorizationFilter();
686+
return;
687+
}
688+
if (StringUtils.hasText(this.httpElt.getAttribute(ATT_AUTHORIZATION_MGR))) {
689+
createAuthorizationFilter();
690+
return;
691+
}
692+
createFilterSecurityInterceptor(authManager);
693+
}
694+
695+
private void createAuthorizationFilter() {
696+
AuthorizationFilterParser authorizationFilterParser = new AuthorizationFilterParser();
697+
BeanDefinition fsiBean = authorizationFilterParser.parse(this.httpElt, this.pc);
698+
String fsiId = this.pc.getReaderContext().generateBeanName(fsiBean);
699+
this.pc.registerBeanComponent(new BeanComponentDefinition(fsiBean, fsiId));
700+
// Create and register a AuthorizationManagerWebInvocationPrivilegeEvaluator for
701+
// use with
702+
// taglibs etc.
703+
BeanDefinition wipe = BeanDefinitionBuilder
704+
.rootBeanDefinition(AuthorizationManagerWebInvocationPrivilegeEvaluator.class)
705+
.addConstructorArgReference(authorizationFilterParser.getAuthorizationManagerRef()).getBeanDefinition();
706+
this.pc.registerBeanComponent(
707+
new BeanComponentDefinition(wipe, this.pc.getReaderContext().generateBeanName(wipe)));
708+
this.fsi = new RuntimeBeanReference(fsiId);
709+
}
710+
677711
private void createFilterSecurityInterceptor(BeanReference authManager) {
678712
boolean useExpressions = FilterInvocationSecurityMetadataSourceParser.isUseExpressions(this.httpElt);
679713
RootBeanDefinition securityMds = FilterInvocationSecurityMetadataSourceParser

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,12 @@ http.attlist &=
350350
http.attlist &=
351351
## If available, runs the request as the Subject acquired from the JaasAuthenticationToken. Defaults to "false".
352352
attribute jaas-api-provision {xsd:boolean}?
353+
http.attlist &=
354+
## Use AuthorizationManager API instead of SecurityMetadataSource
355+
attribute use-authorization-manager {xsd:boolean}?
356+
http.attlist &=
357+
## Use this AuthorizationManager instead of deriving one from <intercept-url> elements
358+
attribute authorization-manager-ref {xsd:token}?
353359
http.attlist &=
354360
## Optional attribute specifying the ID of the AccessDecisionManager implementation which should be used for authorizing HTTP requests.
355361
attribute access-decision-manager-ref {xsd:token}?

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1287,6 +1287,18 @@
12871287
</xs:documentation>
12881288
</xs:annotation>
12891289
</xs:attribute>
1290+
<xs:attribute name="use-authorization-manager" type="xs:boolean">
1291+
<xs:annotation>
1292+
<xs:documentation>Use AuthorizationManager API instead of SecurityMetadataSource
1293+
</xs:documentation>
1294+
</xs:annotation>
1295+
</xs:attribute>
1296+
<xs:attribute name="authorization-manager-ref" type="xs:token">
1297+
<xs:annotation>
1298+
<xs:documentation>Use this AuthorizationManager instead of deriving one from &lt;intercept-url&gt; elements
1299+
</xs:documentation>
1300+
</xs:annotation>
1301+
</xs:attribute>
12901302
<xs:attribute name="access-decision-manager-ref" type="xs:token">
12911303
<xs:annotation>
12921304
<xs:documentation>Optional attribute specifying the ID of the AccessDecisionManager implementation which

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,12 @@ http.attlist &=
350350
http.attlist &=
351351
## If available, runs the request as the Subject acquired from the JaasAuthenticationToken. Defaults to "false".
352352
attribute jaas-api-provision {xsd:boolean}?
353+
http.attlist &=
354+
## Use AuthorizationManager API instead of SecurityMetadataSource
355+
attribute use-authorization-manager {xsd:boolean}?
356+
http.attlist &=
357+
## Use this AuthorizationManager instead of deriving one from <intercept-url> elements
358+
attribute authorization-manager-ref {xsd:token}?
353359
http.attlist &=
354360
## Optional attribute specifying the ID of the AccessDecisionManager implementation which should be used for authorizing HTTP requests.
355361
attribute access-decision-manager-ref {xsd:token}?

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1265,6 +1265,18 @@
12651265
</xs:documentation>
12661266
</xs:annotation>
12671267
</xs:attribute>
1268+
<xs:attribute name="use-authorization-manager" type="xs:boolean">
1269+
<xs:annotation>
1270+
<xs:documentation>Use AuthorizationManager API instead of SecurityMetadataSource
1271+
</xs:documentation>
1272+
</xs:annotation>
1273+
</xs:attribute>
1274+
<xs:attribute name="authorization-manager-ref" type="xs:token">
1275+
<xs:annotation>
1276+
<xs:documentation>Use this AuthorizationManager instead of deriving one from &lt;intercept-url&gt; elements
1277+
</xs:documentation>
1278+
</xs:annotation>
1279+
</xs:attribute>
12681280
<xs:attribute name="access-decision-manager-ref" type="xs:token">
12691281
<xs:annotation>
12701282
<xs:documentation>Optional attribute specifying the ID of the AccessDecisionManager implementation which

config/src/test/java/org/springframework/security/config/http/HttpConfigTests.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

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

19+
import jakarta.servlet.http.HttpServletRequest;
1920
import jakarta.servlet.http.HttpServletResponse;
2021
import jakarta.servlet.http.HttpServletResponseWrapper;
2122
import org.apache.http.HttpStatus;
@@ -25,12 +26,17 @@
2526
import org.springframework.beans.factory.annotation.Autowired;
2627
import org.springframework.mock.web.MockHttpServletRequest;
2728
import org.springframework.mock.web.MockHttpServletResponse;
29+
import org.springframework.security.authorization.AuthorizationDecision;
30+
import org.springframework.security.authorization.AuthorizationManager;
2831
import org.springframework.security.config.test.SpringTestContext;
2932
import org.springframework.security.config.test.SpringTestContextExtension;
3033
import org.springframework.security.web.FilterChainProxy;
3134
import org.springframework.test.web.servlet.MockMvc;
3235

3336
import static org.assertj.core.api.Assertions.assertThat;
37+
import static org.mockito.ArgumentMatchers.any;
38+
import static org.mockito.BDDMockito.given;
39+
import static org.mockito.Mockito.verify;
3440
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
3541
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl;
3642
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@@ -59,6 +65,30 @@ public void getWhenUsingMinimalConfigurationThenRedirectsToLogin() throws Except
5965
// @formatter:on
6066
}
6167

68+
@Test
69+
public void getWhenUsingMinimalAuthorizationManagerThenRedirectsToLogin() throws Exception {
70+
this.spring.configLocations(this.xml("MinimalAuthorizationManager")).autowire();
71+
// @formatter:off
72+
this.mvc.perform(get("/"))
73+
.andExpect(status().isFound())
74+
.andExpect(redirectedUrl("http://localhost/login"));
75+
// @formatter:on
76+
}
77+
78+
@Test
79+
public void getWhenUsingAuthorizationManagerThenRedirectsToLogin() throws Exception {
80+
this.spring.configLocations(this.xml("AuthorizationManager")).autowire();
81+
AuthorizationManager<HttpServletRequest> authorizationManager = this.spring.getContext()
82+
.getBean(AuthorizationManager.class);
83+
given(authorizationManager.check(any(), any())).willReturn(new AuthorizationDecision(false));
84+
// @formatter:off
85+
this.mvc.perform(get("/"))
86+
.andExpect(status().isFound())
87+
.andExpect(redirectedUrl("http://localhost/login"));
88+
// @formatter:on
89+
verify(authorizationManager).check(any(), any());
90+
}
91+
6292
@Test
6393
public void getWhenUsingMinimalConfigurationThenPreventsSessionAsUrlParameter() throws Exception {
6494
this.spring.configLocations(this.xml("Minimal")).autowire();

0 commit comments

Comments
 (0)