Add MvcRequestMatcher

Fixes gh-3964
This commit is contained in:
Rob Winch
2016-07-01 16:30:51 -05:00
parent 13bc70f693
commit e4c13e3c0e
26 changed files with 918 additions and 54 deletions

View File

@@ -33,6 +33,7 @@ import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.DefaultFilterInvocationSecurityMetadataSource;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.security.web.util.matcher.RequestVariablesExtractor;
import org.springframework.util.Assert;
/**
@@ -96,6 +97,10 @@ public final class ExpressionBasedFilterInvocationSecurityMetadataSource
return new AntPathMatcherEvaluationContextPostProcessor(
(AntPathRequestMatcher) request);
}
if (request instanceof RequestVariablesExtractor) {
return new RequestVariablesExtractorEvaluationContextPostProcessor(
(RequestVariablesExtractor) request);
}
return null;
}
@@ -119,4 +124,24 @@ public final class ExpressionBasedFilterInvocationSecurityMetadataSource
}
}
static class RequestVariablesExtractorEvaluationContextPostProcessor
extends AbstractVariableEvaluationContextPostProcessor {
private final RequestVariablesExtractor matcher;
public RequestVariablesExtractorEvaluationContextPostProcessor(
RequestVariablesExtractor matcher) {
this.matcher = matcher;
}
@Override
Map<String, String> extractVariables(HttpServletRequest request) {
return this.matcher.extractUriTemplateVariables(request);
}
@Override
String postProcessVariableName(String variableName) {
return variableName;
}
}
}

View File

@@ -0,0 +1,127 @@
/*
* Copyright 2012-2016 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.servlet.util.matcher;
import java.util.Collections;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.springframework.http.HttpMethod;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.security.web.util.matcher.RequestVariablesExtractor;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
import org.springframework.web.servlet.handler.MatchableHandlerMapping;
import org.springframework.web.servlet.handler.RequestMatchResult;
import org.springframework.web.util.UrlPathHelper;
/**
* A {@link RequestMatcher} that uses Spring MVC's {@link HandlerMappingIntrospector} to
* match the path and extract variables.
*
* @author Rob Winch
* @since 4.1.1
*/
public final class MvcRequestMatcher
implements RequestMatcher, RequestVariablesExtractor {
private final DefaultMatcher defaultMatcher = new DefaultMatcher();
private final HandlerMappingIntrospector introspector;
private final String pattern;
private HttpMethod method;
public MvcRequestMatcher(HandlerMappingIntrospector introspector, String pattern) {
this.introspector = introspector;
this.pattern = pattern;
}
@Override
public boolean matches(HttpServletRequest request) {
if (this.method != null && !this.method.name().equals(request.getMethod())) {
return false;
}
MatchableHandlerMapping mapping = getMapping(request);
if (mapping == null) {
return this.defaultMatcher.matches(request);
}
RequestMatchResult matchResult = mapping.match(request, this.pattern);
return matchResult != null;
}
private MatchableHandlerMapping getMapping(HttpServletRequest request) {
try {
return this.introspector.getMatchableHandlerMapping(request);
}
catch (Throwable t) {
return null;
}
}
/*
* (non-Javadoc)
*
* @see org.springframework.security.web.util.matcher.RequestVariablesExtractor#
* extractUriTemplateVariables(javax.servlet.http.HttpServletRequest)
*/
@Override
public Map<String, String> extractUriTemplateVariables(HttpServletRequest request) {
MatchableHandlerMapping mapping = getMapping(request);
if (mapping == null) {
return this.defaultMatcher.extractUriTemplateVariables(request);
}
RequestMatchResult result = mapping.match(request, this.pattern);
return result == null ? Collections.<String, String>emptyMap()
: result.extractUriTemplateVariables();
}
/**
* @param method the method to set
*/
public void setMethod(HttpMethod method) {
this.method = method;
}
private class DefaultMatcher implements RequestMatcher, RequestVariablesExtractor {
private final UrlPathHelper pathHelper = new UrlPathHelper();
private final PathMatcher pathMatcher = new AntPathMatcher();
@Override
public boolean matches(HttpServletRequest request) {
String lookupPath = this.pathHelper.getLookupPathForRequest(request);
return matches(lookupPath);
}
private boolean matches(String lookupPath) {
return this.pathMatcher.match(MvcRequestMatcher.this.pattern, lookupPath);
}
@Override
public Map<String, String> extractUriTemplateVariables(
HttpServletRequest request) {
String lookupPath = this.pathHelper.getLookupPathForRequest(request);
if (matches(lookupPath)) {
return this.pathMatcher.extractUriTemplateVariables(
MvcRequestMatcher.this.pattern, lookupPath);
}
return Collections.emptyMap();
}
}
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2012-2016 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.util.matcher;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
/**
* An interface for extracting URI variables from the {@link HttpServletRequest}.
*
* @author Rob Winch
* @since 4.1.1
*/
public interface RequestVariablesExtractor {
/**
* Extract URL template variables from the request.
*
* @param request the HttpServletRequest to obtain a URL to extract the variables from
* @return the URL variables or empty if no variables are found
*/
Map<String, String> extractUriTemplateVariables(HttpServletRequest request);
}

View File

@@ -0,0 +1,202 @@
/*
* Copyright 2012-2016 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.servlet.util.matcher;
import java.util.Collections;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.http.HttpMethod;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
import org.springframework.web.servlet.handler.MatchableHandlerMapping;
import org.springframework.web.servlet.handler.RequestMatchResult;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
/**
* @author Rob Winch
*/
@RunWith(MockitoJUnitRunner.class)
public class MvcRequestMatcherTests {
@Mock
HandlerMappingIntrospector introspector;
@Mock
MatchableHandlerMapping mapping;
@Mock
RequestMatchResult result;
@Captor
ArgumentCaptor<String> pattern;
MockHttpServletRequest request;
MvcRequestMatcher matcher;
@Before
public void setup() throws Exception {
this.request = new MockHttpServletRequest();
this.request.setMethod("GET");
this.request.setServletPath("/path");
this.matcher = new MvcRequestMatcher(this.introspector, "/path");
}
@Test
public void extractUriTemplateVariablesSuccess() throws Exception {
when(this.result.extractUriTemplateVariables())
.thenReturn(Collections.singletonMap("p", "path"));
when(this.introspector.getMatchableHandlerMapping(this.request))
.thenReturn(this.mapping);
when(this.mapping.match(eq(this.request), this.pattern.capture()))
.thenReturn(this.result);
this.matcher = new MvcRequestMatcher(this.introspector, "/{p}");
when(this.introspector.getMatchableHandlerMapping(this.request)).thenReturn(null);
assertThat(this.matcher.extractUriTemplateVariables(this.request))
.containsEntry("p", "path");
}
@Test
public void extractUriTemplateVariablesFail() throws Exception {
when(this.result.extractUriTemplateVariables())
.thenReturn(Collections.<String, String>emptyMap());
when(this.introspector.getMatchableHandlerMapping(this.request))
.thenReturn(this.mapping);
when(this.mapping.match(eq(this.request), this.pattern.capture()))
.thenReturn(this.result);
assertThat(this.matcher.extractUriTemplateVariables(this.request)).isEmpty();
}
@Test
public void extractUriTemplateVariablesDefaultSuccess() throws Exception {
this.matcher = new MvcRequestMatcher(this.introspector, "/{p}");
when(this.introspector.getMatchableHandlerMapping(this.request)).thenReturn(null);
assertThat(this.matcher.extractUriTemplateVariables(this.request))
.containsEntry("p", "path");
}
@Test
public void extractUriTemplateVariablesDefaultFail() throws Exception {
this.matcher = new MvcRequestMatcher(this.introspector, "/nomatch/{p}");
when(this.introspector.getMatchableHandlerMapping(this.request)).thenReturn(null);
assertThat(this.matcher.extractUriTemplateVariables(this.request)).isEmpty();
}
@Test
public void matchesPathOnlyTrue() throws Exception {
when(this.introspector.getMatchableHandlerMapping(this.request))
.thenReturn(this.mapping);
when(this.mapping.match(eq(this.request), this.pattern.capture()))
.thenReturn(this.result);
assertThat(this.matcher.matches(this.request)).isTrue();
assertThat(this.pattern.getValue()).isEqualTo("/path");
}
@Test
public void matchesDefaultMatches() throws Exception {
when(this.introspector.getMatchableHandlerMapping(this.request)).thenReturn(null);
assertThat(this.matcher.matches(this.request)).isTrue();
}
@Test
public void matchesDefaultDoesNotMatch() throws Exception {
this.request.setServletPath("/other");
when(this.introspector.getMatchableHandlerMapping(this.request)).thenReturn(null);
assertThat(this.matcher.matches(this.request)).isFalse();
}
@Test
public void matchesPathOnlyFalse() throws Exception {
when(this.introspector.getMatchableHandlerMapping(this.request))
.thenReturn(this.mapping);
assertThat(this.matcher.matches(this.request)).isFalse();
}
@Test
public void matchesMethodAndPathTrue() throws Exception {
this.matcher.setMethod(HttpMethod.GET);
when(this.introspector.getMatchableHandlerMapping(this.request))
.thenReturn(this.mapping);
when(this.mapping.match(eq(this.request), this.pattern.capture()))
.thenReturn(this.result);
assertThat(this.matcher.matches(this.request)).isTrue();
assertThat(this.pattern.getValue()).isEqualTo("/path");
}
@Test
public void matchesMethodAndPathFalseMethod() throws Exception {
this.matcher.setMethod(HttpMethod.POST);
assertThat(this.matcher.matches(this.request)).isFalse();
// method compare should be done first since faster
verifyZeroInteractions(this.introspector);
}
/**
* Malicious users can specify any HTTP Method to create a stacktrace and try to
* expose useful information about the system. We should ensure we ignore invalid HTTP
* methods.
* @throws Exception if an error occurs
*/
@Test
public void matchesInvalidMethodOnRequest() throws Exception {
this.matcher.setMethod(HttpMethod.GET);
this.request.setMethod("invalid");
assertThat(this.matcher.matches(this.request)).isFalse();
// method compare should be done first since faster
verifyZeroInteractions(this.introspector);
}
@Test
public void matchesMethodAndPathFalsePath() throws Exception {
this.matcher.setMethod(HttpMethod.GET);
when(this.introspector.getMatchableHandlerMapping(this.request))
.thenReturn(this.mapping);
assertThat(this.matcher.matches(this.request)).isFalse();
}
@Test
public void matchesGetMatchableHandlerMappingNull() throws Exception {
assertThat(this.matcher.matches(this.request)).isTrue();
}
@Test
public void matchesGetMatchableHandlerMappingThrows() throws Exception {
when(this.introspector.getMatchableHandlerMapping(this.request)).thenThrow(
new HttpRequestMethodNotSupportedException(this.request.getMethod()));
assertThat(this.matcher.matches(this.request)).isTrue();
}
}