diff --git a/config/src/test/groovy/org/springframework/security/config/http/InterceptUrlConfigTests.groovy b/config/src/test/groovy/org/springframework/security/config/http/InterceptUrlConfigTests.groovy index cb1030d996..b0bd6f86db 100644 --- a/config/src/test/groovy/org/springframework/security/config/http/InterceptUrlConfigTests.groovy +++ b/config/src/test/groovy/org/springframework/security/config/http/InterceptUrlConfigTests.groovy @@ -15,67 +15,15 @@ */ package org.springframework.security.config.http -import org.springframework.security.crypto.codec.Base64; - -import java.security.Principal - 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.MockHttpServletRequest import org.springframework.mock.web.MockHttpServletResponse -import org.springframework.security.access.AccessDeniedException import org.springframework.security.access.SecurityConfig -import org.springframework.security.authentication.AnonymousAuthenticationProvider; -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.crypto.codec.Base64 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"() { when: - httpAutoConfig { - 'intercept-url'(pattern: '/anyurl', access: "ROLE_USER") - 'intercept-url'(pattern: '/anyurl', 'method':'GET',access: 'ROLE_ADMIN') - } - createAppContext() + httpAutoConfig { + 'intercept-url'(pattern: '/anyurl', access: "ROLE_USER") + 'intercept-url'(pattern: '/anyurl', 'method':'GET',access: 'ROLE_ADMIN') + } + createAppContext() - def fids = getFilter(FilterSecurityInterceptor).securityMetadataSource - def attrs = fids.getAttributes(createFilterinvocation("/anyurl", "GET")) - def attrsPost = fids.getAttributes(createFilterinvocation("/anyurl", "POST")) + def fids = getFilter(FilterSecurityInterceptor).securityMetadataSource + def attrs = fids.getAttributes(createFilterinvocation("/anyurl", "GET")) + def attrsPost = fids.getAttributes(createFilterinvocation("/anyurl", "POST")) then: - attrs.size() == 1 - attrs.contains(new SecurityConfig("ROLE_USER")) - attrsPost.size() == 1 - attrsPost.contains(new SecurityConfig("ROLE_USER")) + attrs.size() == 1 + attrs.contains(new SecurityConfig("ROLE_USER")) + attrsPost.size() == 1 + attrsPost.contains(new SecurityConfig("ROLE_USER")) } def "SEC-2355: intercept-url support patch"() { setup: - MockHttpServletRequest request = new MockHttpServletRequest(method:'GET') - MockHttpServletResponse response = new MockHttpServletResponse() - MockFilterChain chain = new MockFilterChain() - xml.http('use-expressions':false) { - 'http-basic'() - 'intercept-url'(pattern: '/**', 'method':'PATCH',access: 'ROLE_ADMIN') - csrf(disabled:true) - } - createAppContext() + MockHttpServletRequest request = new MockHttpServletRequest(method:'GET') + MockHttpServletResponse response = new MockHttpServletResponse() + MockFilterChain chain = new MockFilterChain() + xml.http('use-expressions':false) { + 'http-basic'() + 'intercept-url'(pattern: '/**', 'method':'PATCH',access: 'ROLE_ADMIN') + csrf(disabled:true) + } + createAppContext() when: 'Method other than PATCH is used' - springSecurityFilterChain.doFilter(request,response,chain) + springSecurityFilterChain.doFilter(request,response,chain) then: 'The response is OK' - response.status == HttpServletResponse.SC_OK + response.status == HttpServletResponse.SC_OK when: 'Method of PATCH is used' - request = new MockHttpServletRequest(method:'PATCH') - response = new MockHttpServletResponse() - chain = new MockFilterChain() - springSecurityFilterChain.doFilter(request, response, chain) - then: 'The response is unauthorized' - response.status == HttpServletResponse.SC_UNAUTHORIZED + request = new MockHttpServletRequest(method:'PATCH') + response = new MockHttpServletResponse() + chain = new MockFilterChain() + springSecurityFilterChain.doFilter(request, response, chain) + then: 'The response is unauthorized' + response.status == HttpServletResponse.SC_UNAUTHORIZED } def "intercept-url supports hasAnyRoles"() { setup: - MockHttpServletRequest request = new MockHttpServletRequest(method:'GET') - MockHttpServletResponse response = new MockHttpServletResponse() - MockFilterChain chain = new MockFilterChain() - xml.http('use-expressions':true) { - 'http-basic'() - 'intercept-url'(pattern: '/**', access: "hasAnyRole('ROLE_DEVELOPER','ROLE_USER')") - csrf(disabled:true) - } + MockHttpServletRequest request = new MockHttpServletRequest(method:'GET') + MockHttpServletResponse response = new MockHttpServletResponse() + MockFilterChain chain = new MockFilterChain() + xml.http('use-expressions':true) { + 'http-basic'() + 'intercept-url'(pattern: '/**', access: "hasAnyRole('ROLE_DEVELOPER','ROLE_USER')") + csrf(disabled:true) + } when: - createAppContext() + createAppContext() then: 'no error' - noExceptionThrown() + noExceptionThrown() when: 'ROLE_USER can access' - login(request, 'user', 'password') - springSecurityFilterChain.doFilter(request,response,chain) + login(request, 'user', 'password') + springSecurityFilterChain.doFilter(request,response,chain) then: 'The response is OK' - response.status == HttpServletResponse.SC_OK + response.status == HttpServletResponse.SC_OK when: 'ROLE_A cannot access' - request = new MockHttpServletRequest(method:'GET') - response = new MockHttpServletResponse() - chain = new MockFilterChain() - login(request, 'bob', 'bobspassword') - springSecurityFilterChain.doFilter(request,response,chain) + request = new MockHttpServletRequest(method:'GET') + response = new MockHttpServletResponse() + chain = new MockFilterChain() + login(request, 'bob', 'bobspassword') + springSecurityFilterChain.doFilter(request,response,chain) then: 'The response is Forbidden' - response.status == HttpServletResponse.SC_FORBIDDEN - + response.status == HttpServletResponse.SC_FORBIDDEN } def "SEC-2256: intercept-url supports path variables"() { setup: - MockHttpServletRequest request = new MockHttpServletRequest(method:'GET') - MockHttpServletResponse response = new MockHttpServletResponse() - MockFilterChain chain = new MockFilterChain() - xml.http('use-expressions':true) { - 'http-basic'() - 'intercept-url'(pattern: '/user/{un}/**', access: "#un == authentication.name") - 'intercept-url'(pattern: '/**', access: "denyAll") - } - createAppContext() - login(request, 'user', 'password') + MockHttpServletRequest request = new MockHttpServletRequest(method:'GET') + MockHttpServletResponse response = new MockHttpServletResponse() + MockFilterChain chain = new MockFilterChain() + xml.http('use-expressions':true) { + 'http-basic'() + 'intercept-url'(pattern: '/user/{un}/**', access: "#un == authentication.name") + 'intercept-url'(pattern: '/**', access: "denyAll") + } + createAppContext() + login(request, 'user', 'password') when: 'user can access' - request.servletPath = '/user/user/abc' - springSecurityFilterChain.doFilter(request,response,chain) + request.servletPath = '/user/user/abc' + springSecurityFilterChain.doFilter(request,response,chain) then: 'The response is OK' - response.status == HttpServletResponse.SC_OK + response.status == HttpServletResponse.SC_OK when: 'user cannot access otheruser' - request = new MockHttpServletRequest(method:'GET', servletPath : '/user/otheruser/abc') - login(request, 'user', 'password') - chain.reset() - springSecurityFilterChain.doFilter(request,response,chain) + request = new MockHttpServletRequest(method:'GET', servletPath : '/user/otheruser/abc') + login(request, 'user', 'password') + chain.reset() + springSecurityFilterChain.doFilter(request,response,chain) 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"() { setup: - MockHttpServletRequest request = new MockHttpServletRequest(method:'GET') - MockHttpServletResponse response = new MockHttpServletResponse() - MockFilterChain chain = new MockFilterChain() - xml.http('use-expressions':true) { - 'http-basic'() - 'intercept-url'(pattern: '/user/{un}/**', access: "@id.isOne(#un)") - 'intercept-url'(pattern: '/**', access: "denyAll") - } - bean('id', Id) - createAppContext() - login(request, 'user', 'password') + MockHttpServletRequest request = new MockHttpServletRequest(method:'GET') + MockHttpServletResponse response = new MockHttpServletResponse() + MockFilterChain chain = new MockFilterChain() + xml.http('use-expressions':true) { + 'http-basic'() + 'intercept-url'(pattern: '/user/{un}/**', access: "@id.isOne(#un)") + 'intercept-url'(pattern: '/**', access: "denyAll") + } + bean('id', Id) + createAppContext() + login(request, 'user', 'password') when: 'can access id == 1' - request.servletPath = '/user/1/abc' - springSecurityFilterChain.doFilter(request,response,chain) + request.servletPath = '/user/1/abc' + springSecurityFilterChain.doFilter(request,response,chain) then: 'The response is OK' - response.status == HttpServletResponse.SC_OK + response.status == HttpServletResponse.SC_OK when: 'user cannot access 2' - request = new MockHttpServletRequest(method:'GET', servletPath : '/user/2/abc') - login(request, 'user', 'password') - chain.reset() - springSecurityFilterChain.doFilter(request,response,chain) + request = new MockHttpServletRequest(method:'GET', servletPath : '/user/2/abc') + login(request, 'user', 'password') + chain.reset() + springSecurityFilterChain.doFilter(request,response,chain) then: 'The response is OK' - response.status == HttpServletResponse.SC_FORBIDDEN + response.status == HttpServletResponse.SC_FORBIDDEN } public static class Id { diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeRequestsTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeRequestsTests.java index 10b84935c8..b0c3341db6 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeRequestsTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeRequestsTests.java @@ -116,6 +116,25 @@ public class AuthorizeRequestsTests { 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 @Configuration static class AntPatchersPathVariables extends WebSecurityConfigurerAdapter { diff --git a/web/src/main/java/org/springframework/security/web/access/expression/AbstractVariableEvaluationContextPostProcessor.java b/web/src/main/java/org/springframework/security/web/access/expression/AbstractVariableEvaluationContextPostProcessor.java new file mode 100644 index 0000000000..188225aff0 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/access/expression/AbstractVariableEvaluationContextPostProcessor.java @@ -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. + * + *

+ * 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 + *

+ * + * @author Rob Winch + * @since 4.1 + */ +abstract class AbstractVariableEvaluationContextPostProcessor + implements EvaluationContextPostProcessor { + + @Override + public final EvaluationContext postProcess(EvaluationContext context, + FilterInvocation invocation) { + HttpServletRequest request = invocation.getHttpRequest(); + Map variables = extractVariables(request); + for (Map.Entry entry : variables.entrySet()) { + context.setVariable(entry.getKey(), entry.getValue()); + } + return context; + } + + protected abstract Map extractVariables( + HttpServletRequest request); + +} diff --git a/web/src/main/java/org/springframework/security/web/access/expression/SecurityEvaluationContextPostProcessor.java b/web/src/main/java/org/springframework/security/web/access/expression/EvaluationContextPostProcessor.java similarity index 96% rename from web/src/main/java/org/springframework/security/web/access/expression/SecurityEvaluationContextPostProcessor.java rename to web/src/main/java/org/springframework/security/web/access/expression/EvaluationContextPostProcessor.java index c13acfebcb..b23c79cc6a 100644 --- a/web/src/main/java/org/springframework/security/web/access/expression/SecurityEvaluationContextPostProcessor.java +++ b/web/src/main/java/org/springframework/security/web/access/expression/EvaluationContextPostProcessor.java @@ -29,7 +29,7 @@ import org.springframework.expression.EvaluationContext; * @since 4.1 * @param the invocation to use for post processing */ -interface SecurityEvaluationContextPostProcessor { +interface EvaluationContextPostProcessor { /** * Allows post processing of the {@link EvaluationContext}. Implementations diff --git a/web/src/main/java/org/springframework/security/web/access/expression/ExpressionBasedFilterInvocationSecurityMetadataSource.java b/web/src/main/java/org/springframework/security/web/access/expression/ExpressionBasedFilterInvocationSecurityMetadataSource.java index 3196487cc8..3a9fb67f8e 100644 --- a/web/src/main/java/org/springframework/security/web/access/expression/ExpressionBasedFilterInvocationSecurityMetadataSource.java +++ b/web/src/main/java/org/springframework/security/web/access/expression/ExpressionBasedFilterInvocationSecurityMetadataSource.java @@ -20,8 +20,11 @@ import java.util.Collection; import java.util.LinkedHashMap; import java.util.Map; +import javax.servlet.http.HttpServletRequest; + import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; + import org.springframework.expression.ExpressionParser; import org.springframework.expression.ParseException; import org.springframework.security.access.ConfigAttribute; @@ -38,8 +41,8 @@ import org.springframework.util.Assert; * @author Luke Taylor * @since 3.0 */ -public final class ExpressionBasedFilterInvocationSecurityMetadataSource extends - DefaultFilterInvocationSecurityMetadataSource { +public final class ExpressionBasedFilterInvocationSecurityMetadataSource + extends DefaultFilterInvocationSecurityMetadataSource { private final static Log logger = LogFactory .getLog(ExpressionBasedFilterInvocationSecurityMetadataSource.class); @@ -67,20 +70,18 @@ public final class ExpressionBasedFilterInvocationSecurityMetadataSource extends ArrayList attributes = new ArrayList(1); String expression = entry.getValue().toArray(new ConfigAttribute[1])[0] .getAttribute(); - logger.debug("Adding web access control expression '" + expression - + "', for " + request); + logger.debug("Adding web access control expression '" + expression + "', for " + + request); - String pattern = null; - if(request instanceof AntPathRequestMatcher) { - pattern = ((AntPathRequestMatcher)request).getPattern(); - } + AbstractVariableEvaluationContextPostProcessor postProcessor = createPostProcessor( + request); try { - attributes.add(new WebExpressionConfigAttribute(parser - .parseExpression(expression), new PathVariableSecurityEvaluationContextPostProcessor(pattern))); + attributes.add(new WebExpressionConfigAttribute( + parser.parseExpression(expression), postProcessor)); } catch (ParseException e) { - throw new IllegalArgumentException("Failed to parse expression '" - + expression + "'"); + throw new IllegalArgumentException( + "Failed to parse expression '" + expression + "'"); } requestToExpressionAttributesMap.put(request, attributes); @@ -89,4 +90,29 @@ public final class ExpressionBasedFilterInvocationSecurityMetadataSource extends 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 extractVariables( + HttpServletRequest request) { + return this.matcher.extractUriTemplateVariables(request); + } + } + } diff --git a/web/src/main/java/org/springframework/security/web/access/expression/PathVariableSecurityEvaluationContextPostProcessor.java b/web/src/main/java/org/springframework/security/web/access/expression/PathVariableSecurityEvaluationContextPostProcessor.java deleted file mode 100644 index c153c9e724..0000000000 --- a/web/src/main/java/org/springframework/security/web/access/expression/PathVariableSecurityEvaluationContextPostProcessor.java +++ /dev/null @@ -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. - * - *

- * 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 - *

- * - * @author Rob Winch - * @since 4.1 - */ -class PathVariableSecurityEvaluationContextPostProcessor implements SecurityEvaluationContextPostProcessor { - 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 variables = matcher.extractUriTemplateVariables(antPattern, path); - for(Map.Entry 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; - } -} diff --git a/web/src/main/java/org/springframework/security/web/access/expression/WebExpressionConfigAttribute.java b/web/src/main/java/org/springframework/security/web/access/expression/WebExpressionConfigAttribute.java index 6c2b961b7e..aa098e1201 100644 --- a/web/src/main/java/org/springframework/security/web/access/expression/WebExpressionConfigAttribute.java +++ b/web/src/main/java/org/springframework/security/web/access/expression/WebExpressionConfigAttribute.java @@ -26,29 +26,34 @@ import org.springframework.security.web.FilterInvocation; * @author Luke Taylor * @since 3.0 */ -class WebExpressionConfigAttribute implements ConfigAttribute, SecurityEvaluationContextPostProcessor { +class WebExpressionConfigAttribute implements ConfigAttribute, + EvaluationContextPostProcessor { private final Expression authorizeExpression; - private final SecurityEvaluationContextPostProcessor postProcessor; + private final EvaluationContextPostProcessor postProcessor; - public WebExpressionConfigAttribute(Expression authorizeExpression, SecurityEvaluationContextPostProcessor postProcessor) { + public WebExpressionConfigAttribute(Expression authorizeExpression, + EvaluationContextPostProcessor postProcessor) { this.authorizeExpression = authorizeExpression; this.postProcessor = postProcessor; } Expression getAuthorizeExpression() { - return authorizeExpression; + return this.authorizeExpression; } + @Override 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() { return null; } @Override public String toString() { - return authorizeExpression.getExpressionString(); + return this.authorizeExpression.getExpressionString(); } } diff --git a/web/src/main/java/org/springframework/security/web/util/matcher/AntPathRequestMatcher.java b/web/src/main/java/org/springframework/security/web/util/matcher/AntPathRequestMatcher.java index 16d669586a..cc6b44c21f 100644 --- a/web/src/main/java/org/springframework/security/web/util/matcher/AntPathRequestMatcher.java +++ b/web/src/main/java/org/springframework/security/web/util/matcher/AntPathRequestMatcher.java @@ -15,12 +15,15 @@ */ package org.springframework.security.web.util.matcher; +import java.util.Collections; +import java.util.Map; + import javax.servlet.http.HttpServletRequest; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; + import org.springframework.http.HttpMethod; -import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.util.AntPathMatcher; import org.springframework.util.Assert; 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. * @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"); this.caseSensitive = caseSensitive; if (pattern.equals(MATCH_ALL) || pattern.equals("**")) { pattern = MATCH_ALL; - matcher = null; + this.matcher = null; } else { 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 // variables, then optimize to a sub-path match if (pattern.endsWith(MATCH_ALL) - && (pattern.indexOf('?') == -1 && pattern.indexOf('{') == -1 && pattern - .indexOf('}') == -1) + && (pattern.indexOf('?') == -1 && pattern.indexOf('{') == -1 + && pattern.indexOf('}') == -1) && 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 { - matcher = new SpringAntMatcher(pattern); + this.matcher = new SpringAntMatcher(pattern); } } this.pattern = pattern; - this.httpMethod = StringUtils.hasText(httpMethod) ? HttpMethod - .valueOf(httpMethod) : null; + this.httpMethod = StringUtils.hasText(httpMethod) ? HttpMethod.valueOf(httpMethod) + : null; } /** @@ -127,19 +132,20 @@ public final class AntPathRequestMatcher implements RequestMatcher { * @param request the request to match against. The ant pattern will be matched * against the {@code servletPath} + {@code pathInfo} of the request. */ + @Override public boolean matches(HttpServletRequest request) { - if (httpMethod != null && StringUtils.hasText(request.getMethod()) - && httpMethod != valueOf(request.getMethod())) { + if (this.httpMethod != null && StringUtils.hasText(request.getMethod()) + && this.httpMethod != valueOf(request.getMethod())) { if (logger.isDebugEnabled()) { logger.debug("Request '" + request.getMethod() + " " - + getRequestPath(request) + "'" + " doesn't match '" + httpMethod - + " " + pattern); + + getRequestPath(request) + "'" + " doesn't match '" + + this.httpMethod + " " + this.pattern); } return false; } - if (pattern.equals(MATCH_ALL)) { + if (this.pattern.equals(MATCH_ALL)) { if (logger.isDebugEnabled()) { logger.debug("Request '" + getRequestPath(request) + "' matched by universal pattern '/**'"); @@ -151,11 +157,19 @@ public final class AntPathRequestMatcher implements RequestMatcher { String url = getRequestPath(request); 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 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) { @@ -165,7 +179,7 @@ public final class AntPathRequestMatcher implements RequestMatcher { url += request.getPathInfo(); } - if (!caseSensitive) { + if (!this.caseSensitive) { url = url.toLowerCase(); } @@ -173,7 +187,7 @@ public final class AntPathRequestMatcher implements RequestMatcher { } public String getPattern() { - return pattern; + return this.pattern; } @Override @@ -189,9 +203,9 @@ public final class AntPathRequestMatcher implements RequestMatcher { @Override public int hashCode() { - int code = 31 ^ pattern.hashCode(); - if (httpMethod != null) { - code ^= httpMethod.hashCode(); + int code = 31 ^ this.pattern.hashCode(); + if (this.httpMethod != null) { + code ^= this.httpMethod.hashCode(); } return code; } @@ -199,10 +213,10 @@ public final class AntPathRequestMatcher implements RequestMatcher { @Override public String toString() { StringBuilder sb = new StringBuilder(); - sb.append("Ant [pattern='").append(pattern).append("'"); + sb.append("Ant [pattern='").append(this.pattern).append("'"); - if (httpMethod != null) { - sb.append(", ").append(httpMethod); + if (this.httpMethod != null) { + sb.append(", ").append(this.httpMethod); } sb.append("]"); @@ -230,6 +244,8 @@ public final class AntPathRequestMatcher implements RequestMatcher { private static interface Matcher { boolean matches(String path); + + Map extractUriTemplateVariables(String path); } private static class SpringAntMatcher implements Matcher { @@ -241,8 +257,14 @@ public final class AntPathRequestMatcher implements RequestMatcher { this.pattern = pattern; } + @Override public boolean matches(String path) { - return antMatcher.match(pattern, path); + return antMatcher.match(this.pattern, path); + } + + @Override + public Map extractUriTemplateVariables(String path) { + return antMatcher.extractUriTemplateVariables(this.pattern, path); } } @@ -254,14 +276,20 @@ public final class AntPathRequestMatcher implements RequestMatcher { private final int length; private SubpathMatcher(String subpath) { - assert !subpath.contains("*"); + assert!subpath.contains("*"); this.subpath = subpath; this.length = subpath.length(); } + @Override public boolean matches(String path) { - return path.startsWith(subpath) - && (path.length() == length || path.charAt(length) == '/'); + return path.startsWith(this.subpath) + && (path.length() == this.length || path.charAt(this.length) == '/'); + } + + @Override + public Map extractUriTemplateVariables(String path) { + return Collections.emptyMap(); } } } diff --git a/web/src/test/java/org/springframework/security/web/access/expression/PathVariableSecurityEvaluationContextPostProcessorTests.java b/web/src/test/java/org/springframework/security/web/access/expression/AbstractVariableEvaluationContextPostProcessorTests.java similarity index 50% rename from web/src/test/java/org/springframework/security/web/access/expression/PathVariableSecurityEvaluationContextPostProcessorTests.java rename to web/src/test/java/org/springframework/security/web/access/expression/AbstractVariableEvaluationContextPostProcessorTests.java index 1d654287e6..3bdb301f71 100644 --- a/web/src/test/java/org/springframework/security/web/access/expression/PathVariableSecurityEvaluationContextPostProcessorTests.java +++ b/web/src/test/java/org/springframework/security/web/access/expression/AbstractVariableEvaluationContextPostProcessorTests.java @@ -15,20 +15,28 @@ */ 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.Test; + import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.mock.web.MockFilterChain; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.security.web.FilterInvocation; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Rob Winch * */ -public class PathVariableSecurityEvaluationContextPostProcessorTests { - PathVariableSecurityEvaluationContextPostProcessor processor; +public class AbstractVariableEvaluationContextPostProcessorTests { + AbstractVariableEvaluationContextPostProcessor processor; FilterInvocation invocation; @@ -38,19 +46,34 @@ public class PathVariableSecurityEvaluationContextPostProcessorTests { @Before public void setup() { - processor = new PathVariableSecurityEvaluationContextPostProcessor("/"); + this.processor = new VariableEvaluationContextPostProcessor(); - request = new MockHttpServletRequest(); - request.setServletPath("/"); - response = new MockHttpServletResponse(); - invocation = new FilterInvocation(request,response, new MockFilterChain()); - context = new StandardEvaluationContext(); + this.request = new MockHttpServletRequest(); + this.request.setServletPath("/"); + this.response = new MockHttpServletResponse(); + this.invocation = new FilterInvocation(this.request, this.response, + new MockFilterChain()); + this.context = new StandardEvaluationContext(); } @Test - public void queryIgnored() { - request.setQueryString("logout"); - processor.postProcess(context, invocation); + public void postProcess() { + this.processor.postProcess(this.context, this.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 RESULTS = Collections.singletonMap("a", "b"); + + @Override + protected Map extractVariables(HttpServletRequest request) { + return RESULTS; + } + + } } diff --git a/web/src/test/java/org/springframework/security/web/access/expression/WebExpressionVoterTests.java b/web/src/test/java/org/springframework/security/web/access/expression/WebExpressionVoterTests.java index 31f131eef3..fa866dd6a4 100644 --- a/web/src/test/java/org/springframework/security/web/access/expression/WebExpressionVoterTests.java +++ b/web/src/test/java/org/springframework/security/web/access/expression/WebExpressionVoterTests.java @@ -51,7 +51,7 @@ public class WebExpressionVoterTests { public void supportsWebConfigAttributeAndFilterInvocation() throws Exception { WebExpressionVoter voter = new WebExpressionVoter(); 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(MethodInvocation.class)).isFalse(); @@ -69,8 +69,8 @@ public class WebExpressionVoterTests { public void grantsAccessIfExpressionIsTrueDeniesIfFalse() { WebExpressionVoter voter = new WebExpressionVoter(); Expression ex = mock(Expression.class); - SecurityEvaluationContextPostProcessor postProcessor = mock( - SecurityEvaluationContextPostProcessor.class); + EvaluationContextPostProcessor postProcessor = mock( + EvaluationContextPostProcessor.class); when(postProcessor.postProcess(any(EvaluationContext.class), any(FilterInvocation.class))).thenAnswer(new Answer() {