Path Variables fail with different case

Fixes gh-3329
This commit is contained in:
Rob Winch 2016-03-21 10:09:50 -05:00
parent cf66487d3a
commit 7bf014f678
10 changed files with 312 additions and 276 deletions

View File

@ -15,67 +15,15 @@
*/ */
package org.springframework.security.config.http package org.springframework.security.config.http
import org.springframework.security.crypto.codec.Base64;
import java.security.Principal
import javax.servlet.Filter import javax.servlet.Filter
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse
import org.springframework.beans.BeansException
import org.springframework.beans.factory.BeanCreationException
import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer
import org.springframework.beans.factory.parsing.BeanDefinitionParsingException
import org.springframework.mock.web.MockFilterChain import org.springframework.mock.web.MockFilterChain
import org.springframework.mock.web.MockHttpServletRequest import org.springframework.mock.web.MockHttpServletRequest
import org.springframework.mock.web.MockHttpServletResponse import org.springframework.mock.web.MockHttpServletResponse
import org.springframework.security.access.AccessDeniedException
import org.springframework.security.access.SecurityConfig import org.springframework.security.access.SecurityConfig
import org.springframework.security.authentication.AnonymousAuthenticationProvider; import org.springframework.security.crypto.codec.Base64
import org.springframework.security.authentication.TestingAuthenticationToken
import org.springframework.security.config.BeanIds
import org.springframework.security.config.MockUserServiceBeanPostProcessor
import org.springframework.security.config.PostProcessedMockUserDetailsService
import org.springframework.security.config.util.InMemoryXmlApplicationContext
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.openid.OpenIDAuthenticationFilter
import org.springframework.security.util.FieldUtils
import org.springframework.security.web.FilterChainProxy
import org.springframework.security.web.PortMapperImpl
import org.springframework.security.web.access.ExceptionTranslationFilter
import org.springframework.security.web.access.channel.ChannelProcessingFilter
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor import org.springframework.security.web.access.intercept.FilterSecurityInterceptor
import org.springframework.security.web.authentication.AnonymousAuthenticationFilter
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
import org.springframework.security.web.authentication.logout.LogoutFilter
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler
import org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter
import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter
import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter
import org.springframework.security.web.context.HttpSessionSecurityContextRepository
import org.springframework.security.web.context.SecurityContextPersistenceFilter
import org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter;
import org.springframework.security.web.jaasapi.JaasApiIntegrationFilter
import org.springframework.security.web.savedrequest.HttpSessionRequestCache
import org.springframework.security.web.savedrequest.RequestCacheAwareFilter
import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter
import org.springframework.security.web.session.SessionManagementFilter
import org.springframework.security.web.authentication.logout.CookieClearingLogoutHandler
import org.springframework.security.web.firewall.DefaultHttpFirewall
import org.springframework.security.BeanNameCollectingPostProcessor
import org.springframework.security.authentication.dao.DaoAuthenticationProvider
import org.springframework.security.access.vote.RoleVoter
import org.springframework.security.web.access.expression.WebExpressionVoter
import org.springframework.security.access.vote.AffirmativeBased
import org.springframework.security.access.PermissionEvaluator
import org.springframework.security.core.Authentication
import org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler
import org.springframework.security.web.util.matcher.AntPathRequestMatcher
import org.springframework.security.authentication.AuthenticationManager
/** /**
@ -86,128 +34,134 @@ class InterceptUrlConfigTests extends AbstractHttpConfigTests {
def "SEC-2256: intercept-url method is not given priority"() { def "SEC-2256: intercept-url method is not given priority"() {
when: when:
httpAutoConfig { httpAutoConfig {
'intercept-url'(pattern: '/anyurl', access: "ROLE_USER") 'intercept-url'(pattern: '/anyurl', access: "ROLE_USER")
'intercept-url'(pattern: '/anyurl', 'method':'GET',access: 'ROLE_ADMIN') 'intercept-url'(pattern: '/anyurl', 'method':'GET',access: 'ROLE_ADMIN')
} }
createAppContext() createAppContext()
def fids = getFilter(FilterSecurityInterceptor).securityMetadataSource def fids = getFilter(FilterSecurityInterceptor).securityMetadataSource
def attrs = fids.getAttributes(createFilterinvocation("/anyurl", "GET")) def attrs = fids.getAttributes(createFilterinvocation("/anyurl", "GET"))
def attrsPost = fids.getAttributes(createFilterinvocation("/anyurl", "POST")) def attrsPost = fids.getAttributes(createFilterinvocation("/anyurl", "POST"))
then: then:
attrs.size() == 1 attrs.size() == 1
attrs.contains(new SecurityConfig("ROLE_USER")) attrs.contains(new SecurityConfig("ROLE_USER"))
attrsPost.size() == 1 attrsPost.size() == 1
attrsPost.contains(new SecurityConfig("ROLE_USER")) attrsPost.contains(new SecurityConfig("ROLE_USER"))
} }
def "SEC-2355: intercept-url support patch"() { def "SEC-2355: intercept-url support patch"() {
setup: setup:
MockHttpServletRequest request = new MockHttpServletRequest(method:'GET') MockHttpServletRequest request = new MockHttpServletRequest(method:'GET')
MockHttpServletResponse response = new MockHttpServletResponse() MockHttpServletResponse response = new MockHttpServletResponse()
MockFilterChain chain = new MockFilterChain() MockFilterChain chain = new MockFilterChain()
xml.http('use-expressions':false) { xml.http('use-expressions':false) {
'http-basic'() 'http-basic'()
'intercept-url'(pattern: '/**', 'method':'PATCH',access: 'ROLE_ADMIN') 'intercept-url'(pattern: '/**', 'method':'PATCH',access: 'ROLE_ADMIN')
csrf(disabled:true) csrf(disabled:true)
} }
createAppContext() createAppContext()
when: 'Method other than PATCH is used' when: 'Method other than PATCH is used'
springSecurityFilterChain.doFilter(request,response,chain) springSecurityFilterChain.doFilter(request,response,chain)
then: 'The response is OK' then: 'The response is OK'
response.status == HttpServletResponse.SC_OK response.status == HttpServletResponse.SC_OK
when: 'Method of PATCH is used' when: 'Method of PATCH is used'
request = new MockHttpServletRequest(method:'PATCH') request = new MockHttpServletRequest(method:'PATCH')
response = new MockHttpServletResponse() response = new MockHttpServletResponse()
chain = new MockFilterChain() chain = new MockFilterChain()
springSecurityFilterChain.doFilter(request, response, chain) springSecurityFilterChain.doFilter(request, response, chain)
then: 'The response is unauthorized' then: 'The response is unauthorized'
response.status == HttpServletResponse.SC_UNAUTHORIZED response.status == HttpServletResponse.SC_UNAUTHORIZED
} }
def "intercept-url supports hasAnyRoles"() { def "intercept-url supports hasAnyRoles"() {
setup: setup:
MockHttpServletRequest request = new MockHttpServletRequest(method:'GET') MockHttpServletRequest request = new MockHttpServletRequest(method:'GET')
MockHttpServletResponse response = new MockHttpServletResponse() MockHttpServletResponse response = new MockHttpServletResponse()
MockFilterChain chain = new MockFilterChain() MockFilterChain chain = new MockFilterChain()
xml.http('use-expressions':true) { xml.http('use-expressions':true) {
'http-basic'() 'http-basic'()
'intercept-url'(pattern: '/**', access: "hasAnyRole('ROLE_DEVELOPER','ROLE_USER')") 'intercept-url'(pattern: '/**', access: "hasAnyRole('ROLE_DEVELOPER','ROLE_USER')")
csrf(disabled:true) csrf(disabled:true)
} }
when: when:
createAppContext() createAppContext()
then: 'no error' then: 'no error'
noExceptionThrown() noExceptionThrown()
when: 'ROLE_USER can access' when: 'ROLE_USER can access'
login(request, 'user', 'password') login(request, 'user', 'password')
springSecurityFilterChain.doFilter(request,response,chain) springSecurityFilterChain.doFilter(request,response,chain)
then: 'The response is OK' then: 'The response is OK'
response.status == HttpServletResponse.SC_OK response.status == HttpServletResponse.SC_OK
when: 'ROLE_A cannot access' when: 'ROLE_A cannot access'
request = new MockHttpServletRequest(method:'GET') request = new MockHttpServletRequest(method:'GET')
response = new MockHttpServletResponse() response = new MockHttpServletResponse()
chain = new MockFilterChain() chain = new MockFilterChain()
login(request, 'bob', 'bobspassword') login(request, 'bob', 'bobspassword')
springSecurityFilterChain.doFilter(request,response,chain) springSecurityFilterChain.doFilter(request,response,chain)
then: 'The response is Forbidden' then: 'The response is Forbidden'
response.status == HttpServletResponse.SC_FORBIDDEN response.status == HttpServletResponse.SC_FORBIDDEN
} }
def "SEC-2256: intercept-url supports path variables"() { def "SEC-2256: intercept-url supports path variables"() {
setup: setup:
MockHttpServletRequest request = new MockHttpServletRequest(method:'GET') MockHttpServletRequest request = new MockHttpServletRequest(method:'GET')
MockHttpServletResponse response = new MockHttpServletResponse() MockHttpServletResponse response = new MockHttpServletResponse()
MockFilterChain chain = new MockFilterChain() MockFilterChain chain = new MockFilterChain()
xml.http('use-expressions':true) { xml.http('use-expressions':true) {
'http-basic'() 'http-basic'()
'intercept-url'(pattern: '/user/{un}/**', access: "#un == authentication.name") 'intercept-url'(pattern: '/user/{un}/**', access: "#un == authentication.name")
'intercept-url'(pattern: '/**', access: "denyAll") 'intercept-url'(pattern: '/**', access: "denyAll")
} }
createAppContext() createAppContext()
login(request, 'user', 'password') login(request, 'user', 'password')
when: 'user can access' when: 'user can access'
request.servletPath = '/user/user/abc' request.servletPath = '/user/user/abc'
springSecurityFilterChain.doFilter(request,response,chain) springSecurityFilterChain.doFilter(request,response,chain)
then: 'The response is OK' then: 'The response is OK'
response.status == HttpServletResponse.SC_OK response.status == HttpServletResponse.SC_OK
when: 'user cannot access otheruser' when: 'user cannot access otheruser'
request = new MockHttpServletRequest(method:'GET', servletPath : '/user/otheruser/abc') request = new MockHttpServletRequest(method:'GET', servletPath : '/user/otheruser/abc')
login(request, 'user', 'password') login(request, 'user', 'password')
chain.reset() chain.reset()
springSecurityFilterChain.doFilter(request,response,chain) springSecurityFilterChain.doFilter(request,response,chain)
then: 'The response is OK' then: 'The response is OK'
response.status == HttpServletResponse.SC_FORBIDDEN response.status == HttpServletResponse.SC_FORBIDDEN
when: 'user can access case insensitive URL'
request = new MockHttpServletRequest(method:'GET', servletPath : '/USER/user/abc')
login(request, 'user', 'password')
chain.reset()
springSecurityFilterChain.doFilter(request,response,chain)
then: 'The response is OK'
response.status == HttpServletResponse.SC_FORBIDDEN
} }
def "SEC-2256: intercept-url supports path variable type conversion"() { def "SEC-2256: intercept-url supports path variable type conversion"() {
setup: setup:
MockHttpServletRequest request = new MockHttpServletRequest(method:'GET') MockHttpServletRequest request = new MockHttpServletRequest(method:'GET')
MockHttpServletResponse response = new MockHttpServletResponse() MockHttpServletResponse response = new MockHttpServletResponse()
MockFilterChain chain = new MockFilterChain() MockFilterChain chain = new MockFilterChain()
xml.http('use-expressions':true) { xml.http('use-expressions':true) {
'http-basic'() 'http-basic'()
'intercept-url'(pattern: '/user/{un}/**', access: "@id.isOne(#un)") 'intercept-url'(pattern: '/user/{un}/**', access: "@id.isOne(#un)")
'intercept-url'(pattern: '/**', access: "denyAll") 'intercept-url'(pattern: '/**', access: "denyAll")
} }
bean('id', Id) bean('id', Id)
createAppContext() createAppContext()
login(request, 'user', 'password') login(request, 'user', 'password')
when: 'can access id == 1' when: 'can access id == 1'
request.servletPath = '/user/1/abc' request.servletPath = '/user/1/abc'
springSecurityFilterChain.doFilter(request,response,chain) springSecurityFilterChain.doFilter(request,response,chain)
then: 'The response is OK' then: 'The response is OK'
response.status == HttpServletResponse.SC_OK response.status == HttpServletResponse.SC_OK
when: 'user cannot access 2' when: 'user cannot access 2'
request = new MockHttpServletRequest(method:'GET', servletPath : '/user/2/abc') request = new MockHttpServletRequest(method:'GET', servletPath : '/user/2/abc')
login(request, 'user', 'password') login(request, 'user', 'password')
chain.reset() chain.reset()
springSecurityFilterChain.doFilter(request,response,chain) springSecurityFilterChain.doFilter(request,response,chain)
then: 'The response is OK' then: 'The response is OK'
response.status == HttpServletResponse.SC_FORBIDDEN response.status == HttpServletResponse.SC_FORBIDDEN
} }
public static class Id { public static class Id {

View File

@ -116,6 +116,25 @@ public class AuthorizeRequestsTests {
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_FORBIDDEN); assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_FORBIDDEN);
} }
// SEC-2256
@Test
public void antMatchersPathVariablesCaseInsensitive() throws Exception {
loadConfig(AntPatchersPathVariables.class);
this.request.setServletPath("/USER/user");
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
this.setup();
this.request.setServletPath("/USER/deny");
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_FORBIDDEN);
}
@EnableWebSecurity @EnableWebSecurity
@Configuration @Configuration
static class AntPatchersPathVariables extends WebSecurityConfigurerAdapter { static class AntPatchersPathVariables extends WebSecurityConfigurerAdapter {

View File

@ -0,0 +1,55 @@
/*
* Copyright 2002-2015 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
*
* http://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.web.access.expression;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.springframework.expression.EvaluationContext;
import org.springframework.security.web.FilterInvocation;
/**
* Exposes URI template variables as variables on the {@link EvaluationContext}. For
* example, the pattern "/user/{username}/**" would expose a variable named username based
* on the current URI.
*
* <p>
* NOTE: This API is intentionally kept package scope as it may change in the future. It
* may be nice to allow users to augment expressions and queries
* </p>
*
* @author Rob Winch
* @since 4.1
*/
abstract class AbstractVariableEvaluationContextPostProcessor
implements EvaluationContextPostProcessor<FilterInvocation> {
@Override
public final EvaluationContext postProcess(EvaluationContext context,
FilterInvocation invocation) {
HttpServletRequest request = invocation.getHttpRequest();
Map<String, String> variables = extractVariables(request);
for (Map.Entry<String, String> entry : variables.entrySet()) {
context.setVariable(entry.getKey(), entry.getValue());
}
return context;
}
protected abstract Map<String, String> extractVariables(
HttpServletRequest request);
}

View File

@ -29,7 +29,7 @@ import org.springframework.expression.EvaluationContext;
* @since 4.1 * @since 4.1
* @param <I> the invocation to use for post processing * @param <I> the invocation to use for post processing
*/ */
interface SecurityEvaluationContextPostProcessor<I> { interface EvaluationContextPostProcessor<I> {
/** /**
* Allows post processing of the {@link EvaluationContext}. Implementations * Allows post processing of the {@link EvaluationContext}. Implementations

View File

@ -20,8 +20,11 @@ import java.util.Collection;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import org.springframework.expression.ExpressionParser; import org.springframework.expression.ExpressionParser;
import org.springframework.expression.ParseException; import org.springframework.expression.ParseException;
import org.springframework.security.access.ConfigAttribute; import org.springframework.security.access.ConfigAttribute;
@ -38,8 +41,8 @@ import org.springframework.util.Assert;
* @author Luke Taylor * @author Luke Taylor
* @since 3.0 * @since 3.0
*/ */
public final class ExpressionBasedFilterInvocationSecurityMetadataSource extends public final class ExpressionBasedFilterInvocationSecurityMetadataSource
DefaultFilterInvocationSecurityMetadataSource { extends DefaultFilterInvocationSecurityMetadataSource {
private final static Log logger = LogFactory private final static Log logger = LogFactory
.getLog(ExpressionBasedFilterInvocationSecurityMetadataSource.class); .getLog(ExpressionBasedFilterInvocationSecurityMetadataSource.class);
@ -67,20 +70,18 @@ public final class ExpressionBasedFilterInvocationSecurityMetadataSource extends
ArrayList<ConfigAttribute> attributes = new ArrayList<ConfigAttribute>(1); ArrayList<ConfigAttribute> attributes = new ArrayList<ConfigAttribute>(1);
String expression = entry.getValue().toArray(new ConfigAttribute[1])[0] String expression = entry.getValue().toArray(new ConfigAttribute[1])[0]
.getAttribute(); .getAttribute();
logger.debug("Adding web access control expression '" + expression logger.debug("Adding web access control expression '" + expression + "', for "
+ "', for " + request); + request);
String pattern = null; AbstractVariableEvaluationContextPostProcessor postProcessor = createPostProcessor(
if(request instanceof AntPathRequestMatcher) { request);
pattern = ((AntPathRequestMatcher)request).getPattern();
}
try { try {
attributes.add(new WebExpressionConfigAttribute(parser attributes.add(new WebExpressionConfigAttribute(
.parseExpression(expression), new PathVariableSecurityEvaluationContextPostProcessor(pattern))); parser.parseExpression(expression), postProcessor));
} }
catch (ParseException e) { catch (ParseException e) {
throw new IllegalArgumentException("Failed to parse expression '" throw new IllegalArgumentException(
+ expression + "'"); "Failed to parse expression '" + expression + "'");
} }
requestToExpressionAttributesMap.put(request, attributes); requestToExpressionAttributesMap.put(request, attributes);
@ -89,4 +90,29 @@ public final class ExpressionBasedFilterInvocationSecurityMetadataSource extends
return requestToExpressionAttributesMap; return requestToExpressionAttributesMap;
} }
private static AbstractVariableEvaluationContextPostProcessor createPostProcessor(
Object request) {
if (request instanceof AntPathRequestMatcher) {
return new AntPathMatcherEvaluationContextPostProcessor(
(AntPathRequestMatcher) request);
}
return null;
}
static class AntPathMatcherEvaluationContextPostProcessor
extends AbstractVariableEvaluationContextPostProcessor {
private final AntPathRequestMatcher matcher;
public AntPathMatcherEvaluationContextPostProcessor(
AntPathRequestMatcher matcher) {
this.matcher = matcher;
}
@Override
protected Map<String, String> extractVariables(
HttpServletRequest request) {
return this.matcher.extractUriTemplateVariables(request);
}
}
} }

View File

@ -1,74 +0,0 @@
/*
* Copyright 2002-2015 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
*
* http://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.web.access.expression;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.springframework.expression.EvaluationContext;
import org.springframework.security.web.FilterInvocation;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
/**
* Exposes URI template variables as variables on the {@link EvaluationContext}.
* For example, the pattern "/user/{username}/**" would expose a variable named
* username based on the current URI.
*
* <p>
* NOTE: This API is intentionally kept package scope as it may change in the future. It may be nice to allow users to augment expressions and queries
* </p>
*
* @author Rob Winch
* @since 4.1
*/
class PathVariableSecurityEvaluationContextPostProcessor implements SecurityEvaluationContextPostProcessor<FilterInvocation> {
private final PathMatcher matcher = new AntPathMatcher();
private final String antPattern;
/**
* Creates a new instance.
*
* @param antPattern the ant pattern that may have template variables (i.e. "/user/{username}/**)
*/
public PathVariableSecurityEvaluationContextPostProcessor(String antPattern) {
this.antPattern = antPattern;
}
public EvaluationContext postProcess(EvaluationContext context, FilterInvocation invocation) {
if(antPattern == null) {
return context;
}
String path = getRequestPath(invocation.getHttpRequest());
Map<String, String> variables = matcher.extractUriTemplateVariables(antPattern, path);
for(Map.Entry<String, String> entry : variables.entrySet()) {
context.setVariable(entry.getKey(), entry.getValue());
}
return context;
}
private String getRequestPath(HttpServletRequest request) {
String url = request.getServletPath();
if (request.getPathInfo() != null) {
url += request.getPathInfo();
}
return url;
}
}

View File

@ -26,29 +26,34 @@ import org.springframework.security.web.FilterInvocation;
* @author Luke Taylor * @author Luke Taylor
* @since 3.0 * @since 3.0
*/ */
class WebExpressionConfigAttribute implements ConfigAttribute, SecurityEvaluationContextPostProcessor<FilterInvocation> { class WebExpressionConfigAttribute implements ConfigAttribute,
EvaluationContextPostProcessor<FilterInvocation> {
private final Expression authorizeExpression; private final Expression authorizeExpression;
private final SecurityEvaluationContextPostProcessor<FilterInvocation> postProcessor; private final EvaluationContextPostProcessor<FilterInvocation> postProcessor;
public WebExpressionConfigAttribute(Expression authorizeExpression, SecurityEvaluationContextPostProcessor<FilterInvocation> postProcessor) { public WebExpressionConfigAttribute(Expression authorizeExpression,
EvaluationContextPostProcessor<FilterInvocation> postProcessor) {
this.authorizeExpression = authorizeExpression; this.authorizeExpression = authorizeExpression;
this.postProcessor = postProcessor; this.postProcessor = postProcessor;
} }
Expression getAuthorizeExpression() { Expression getAuthorizeExpression() {
return authorizeExpression; return this.authorizeExpression;
} }
@Override
public EvaluationContext postProcess(EvaluationContext context, FilterInvocation fi) { public EvaluationContext postProcess(EvaluationContext context, FilterInvocation fi) {
return postProcessor.postProcess(context, fi); return this.postProcessor == null ? context
: this.postProcessor.postProcess(context, fi);
} }
@Override
public String getAttribute() { public String getAttribute() {
return null; return null;
} }
@Override @Override
public String toString() { public String toString() {
return authorizeExpression.getExpressionString(); return this.authorizeExpression.getExpressionString();
} }
} }

View File

@ -15,12 +15,15 @@
*/ */
package org.springframework.security.web.util.matcher; package org.springframework.security.web.util.matcher;
import java.util.Collections;
import java.util.Map;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.AntPathMatcher; import org.springframework.util.AntPathMatcher;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
@ -89,13 +92,14 @@ public final class AntPathRequestMatcher implements RequestMatcher {
* the incoming request doesn't doesn't have the same method. * the incoming request doesn't doesn't have the same method.
* @param caseSensitive true if the matcher should consider case, else false * @param caseSensitive true if the matcher should consider case, else false
*/ */
public AntPathRequestMatcher(String pattern, String httpMethod, boolean caseSensitive) { public AntPathRequestMatcher(String pattern, String httpMethod,
boolean caseSensitive) {
Assert.hasText(pattern, "Pattern cannot be null or empty"); Assert.hasText(pattern, "Pattern cannot be null or empty");
this.caseSensitive = caseSensitive; this.caseSensitive = caseSensitive;
if (pattern.equals(MATCH_ALL) || pattern.equals("**")) { if (pattern.equals(MATCH_ALL) || pattern.equals("**")) {
pattern = MATCH_ALL; pattern = MATCH_ALL;
matcher = null; this.matcher = null;
} }
else { else {
if (!caseSensitive) { if (!caseSensitive) {
@ -105,19 +109,20 @@ public final class AntPathRequestMatcher implements RequestMatcher {
// If the pattern ends with {@code /**} and has no other wildcards or path // If the pattern ends with {@code /**} and has no other wildcards or path
// variables, then optimize to a sub-path match // variables, then optimize to a sub-path match
if (pattern.endsWith(MATCH_ALL) if (pattern.endsWith(MATCH_ALL)
&& (pattern.indexOf('?') == -1 && pattern.indexOf('{') == -1 && pattern && (pattern.indexOf('?') == -1 && pattern.indexOf('{') == -1
.indexOf('}') == -1) && pattern.indexOf('}') == -1)
&& pattern.indexOf("*") == pattern.length() - 2) { && pattern.indexOf("*") == pattern.length() - 2) {
matcher = new SubpathMatcher(pattern.substring(0, pattern.length() - 3)); this.matcher = new SubpathMatcher(
pattern.substring(0, pattern.length() - 3));
} }
else { else {
matcher = new SpringAntMatcher(pattern); this.matcher = new SpringAntMatcher(pattern);
} }
} }
this.pattern = pattern; this.pattern = pattern;
this.httpMethod = StringUtils.hasText(httpMethod) ? HttpMethod this.httpMethod = StringUtils.hasText(httpMethod) ? HttpMethod.valueOf(httpMethod)
.valueOf(httpMethod) : null; : null;
} }
/** /**
@ -127,19 +132,20 @@ public final class AntPathRequestMatcher implements RequestMatcher {
* @param request the request to match against. The ant pattern will be matched * @param request the request to match against. The ant pattern will be matched
* against the {@code servletPath} + {@code pathInfo} of the request. * against the {@code servletPath} + {@code pathInfo} of the request.
*/ */
@Override
public boolean matches(HttpServletRequest request) { public boolean matches(HttpServletRequest request) {
if (httpMethod != null && StringUtils.hasText(request.getMethod()) if (this.httpMethod != null && StringUtils.hasText(request.getMethod())
&& httpMethod != valueOf(request.getMethod())) { && this.httpMethod != valueOf(request.getMethod())) {
if (logger.isDebugEnabled()) { if (logger.isDebugEnabled()) {
logger.debug("Request '" + request.getMethod() + " " logger.debug("Request '" + request.getMethod() + " "
+ getRequestPath(request) + "'" + " doesn't match '" + httpMethod + getRequestPath(request) + "'" + " doesn't match '"
+ " " + pattern); + this.httpMethod + " " + this.pattern);
} }
return false; return false;
} }
if (pattern.equals(MATCH_ALL)) { if (this.pattern.equals(MATCH_ALL)) {
if (logger.isDebugEnabled()) { if (logger.isDebugEnabled()) {
logger.debug("Request '" + getRequestPath(request) logger.debug("Request '" + getRequestPath(request)
+ "' matched by universal pattern '/**'"); + "' matched by universal pattern '/**'");
@ -151,11 +157,19 @@ public final class AntPathRequestMatcher implements RequestMatcher {
String url = getRequestPath(request); String url = getRequestPath(request);
if (logger.isDebugEnabled()) { if (logger.isDebugEnabled()) {
logger.debug("Checking match of request : '" + url + "'; against '" + pattern logger.debug("Checking match of request : '" + url + "'; against '"
+ "'"); + this.pattern + "'");
} }
return matcher.matches(url); return this.matcher.matches(url);
}
public Map<String, String> extractUriTemplateVariables(HttpServletRequest request) {
if (this.matcher == null || !matches(request)) {
return Collections.emptyMap();
}
String url = getRequestPath(request);
return this.matcher.extractUriTemplateVariables(url);
} }
private String getRequestPath(HttpServletRequest request) { private String getRequestPath(HttpServletRequest request) {
@ -165,7 +179,7 @@ public final class AntPathRequestMatcher implements RequestMatcher {
url += request.getPathInfo(); url += request.getPathInfo();
} }
if (!caseSensitive) { if (!this.caseSensitive) {
url = url.toLowerCase(); url = url.toLowerCase();
} }
@ -173,7 +187,7 @@ public final class AntPathRequestMatcher implements RequestMatcher {
} }
public String getPattern() { public String getPattern() {
return pattern; return this.pattern;
} }
@Override @Override
@ -189,9 +203,9 @@ public final class AntPathRequestMatcher implements RequestMatcher {
@Override @Override
public int hashCode() { public int hashCode() {
int code = 31 ^ pattern.hashCode(); int code = 31 ^ this.pattern.hashCode();
if (httpMethod != null) { if (this.httpMethod != null) {
code ^= httpMethod.hashCode(); code ^= this.httpMethod.hashCode();
} }
return code; return code;
} }
@ -199,10 +213,10 @@ public final class AntPathRequestMatcher implements RequestMatcher {
@Override @Override
public String toString() { public String toString() {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append("Ant [pattern='").append(pattern).append("'"); sb.append("Ant [pattern='").append(this.pattern).append("'");
if (httpMethod != null) { if (this.httpMethod != null) {
sb.append(", ").append(httpMethod); sb.append(", ").append(this.httpMethod);
} }
sb.append("]"); sb.append("]");
@ -230,6 +244,8 @@ public final class AntPathRequestMatcher implements RequestMatcher {
private static interface Matcher { private static interface Matcher {
boolean matches(String path); boolean matches(String path);
Map<String, String> extractUriTemplateVariables(String path);
} }
private static class SpringAntMatcher implements Matcher { private static class SpringAntMatcher implements Matcher {
@ -241,8 +257,14 @@ public final class AntPathRequestMatcher implements RequestMatcher {
this.pattern = pattern; this.pattern = pattern;
} }
@Override
public boolean matches(String path) { public boolean matches(String path) {
return antMatcher.match(pattern, path); return antMatcher.match(this.pattern, path);
}
@Override
public Map<String, String> extractUriTemplateVariables(String path) {
return antMatcher.extractUriTemplateVariables(this.pattern, path);
} }
} }
@ -254,14 +276,20 @@ public final class AntPathRequestMatcher implements RequestMatcher {
private final int length; private final int length;
private SubpathMatcher(String subpath) { private SubpathMatcher(String subpath) {
assert !subpath.contains("*"); assert!subpath.contains("*");
this.subpath = subpath; this.subpath = subpath;
this.length = subpath.length(); this.length = subpath.length();
} }
@Override
public boolean matches(String path) { public boolean matches(String path) {
return path.startsWith(subpath) return path.startsWith(this.subpath)
&& (path.length() == length || path.charAt(length) == '/'); && (path.length() == this.length || path.charAt(this.length) == '/');
}
@Override
public Map<String, String> extractUriTemplateVariables(String path) {
return Collections.emptyMap();
} }
} }
} }

View File

@ -15,20 +15,28 @@
*/ */
package org.springframework.security.web.access.expression; package org.springframework.security.web.access.expression;
import java.util.Collections;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.mock.web.MockFilterChain; import org.springframework.mock.web.MockFilterChain;
import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.web.FilterInvocation; import org.springframework.security.web.FilterInvocation;
import static org.assertj.core.api.Assertions.assertThat;
/** /**
* @author Rob Winch * @author Rob Winch
* *
*/ */
public class PathVariableSecurityEvaluationContextPostProcessorTests { public class AbstractVariableEvaluationContextPostProcessorTests {
PathVariableSecurityEvaluationContextPostProcessor processor; AbstractVariableEvaluationContextPostProcessor processor;
FilterInvocation invocation; FilterInvocation invocation;
@ -38,19 +46,34 @@ public class PathVariableSecurityEvaluationContextPostProcessorTests {
@Before @Before
public void setup() { public void setup() {
processor = new PathVariableSecurityEvaluationContextPostProcessor("/"); this.processor = new VariableEvaluationContextPostProcessor();
request = new MockHttpServletRequest(); this.request = new MockHttpServletRequest();
request.setServletPath("/"); this.request.setServletPath("/");
response = new MockHttpServletResponse(); this.response = new MockHttpServletResponse();
invocation = new FilterInvocation(request,response, new MockFilterChain()); this.invocation = new FilterInvocation(this.request, this.response,
context = new StandardEvaluationContext(); new MockFilterChain());
this.context = new StandardEvaluationContext();
} }
@Test @Test
public void queryIgnored() { public void postProcess() {
request.setQueryString("logout"); this.processor.postProcess(this.context, this.invocation);
processor.postProcess(context, invocation);
for (String key : VariableEvaluationContextPostProcessor.RESULTS.keySet()) {
assertThat(this.context.lookupVariable(key))
.isEqualTo(VariableEvaluationContextPostProcessor.RESULTS.get(key));
}
} }
static class VariableEvaluationContextPostProcessor
extends AbstractVariableEvaluationContextPostProcessor {
static final Map<String, String> RESULTS = Collections.singletonMap("a", "b");
@Override
protected Map<String, String> extractVariables(HttpServletRequest request) {
return RESULTS;
}
}
} }

View File

@ -51,7 +51,7 @@ public class WebExpressionVoterTests {
public void supportsWebConfigAttributeAndFilterInvocation() throws Exception { public void supportsWebConfigAttributeAndFilterInvocation() throws Exception {
WebExpressionVoter voter = new WebExpressionVoter(); WebExpressionVoter voter = new WebExpressionVoter();
assertThat(voter.supports(new WebExpressionConfigAttribute(mock(Expression.class), assertThat(voter.supports(new WebExpressionConfigAttribute(mock(Expression.class),
mock(SecurityEvaluationContextPostProcessor.class)))).isTrue(); mock(EvaluationContextPostProcessor.class)))).isTrue();
assertThat(voter.supports(FilterInvocation.class)).isTrue(); assertThat(voter.supports(FilterInvocation.class)).isTrue();
assertThat(voter.supports(MethodInvocation.class)).isFalse(); assertThat(voter.supports(MethodInvocation.class)).isFalse();
@ -69,8 +69,8 @@ public class WebExpressionVoterTests {
public void grantsAccessIfExpressionIsTrueDeniesIfFalse() { public void grantsAccessIfExpressionIsTrueDeniesIfFalse() {
WebExpressionVoter voter = new WebExpressionVoter(); WebExpressionVoter voter = new WebExpressionVoter();
Expression ex = mock(Expression.class); Expression ex = mock(Expression.class);
SecurityEvaluationContextPostProcessor postProcessor = mock( EvaluationContextPostProcessor postProcessor = mock(
SecurityEvaluationContextPostProcessor.class); EvaluationContextPostProcessor.class);
when(postProcessor.postProcess(any(EvaluationContext.class), when(postProcessor.postProcess(any(EvaluationContext.class),
any(FilterInvocation.class))).thenAnswer(new Answer<EvaluationContext>() { any(FilterInvocation.class))).thenAnswer(new Answer<EvaluationContext>() {