Răsfoiți Sursa

SEC-531: Provide support for HTTP methods in FilterInvocationDefinitionSource. Path/Regex versions of FIDS are now deprecated and in favour of using their (no longer abstract) parent class with a UrlPathMatcher strategy.

Luke Taylor 17 ani în urmă
părinte
comite
d10450cfb7
16 a modificat fișierele cu 515 adăugiri și 299 ștergeri
  1. 44 31
      core/src/main/java/org/springframework/security/config/HttpSecurityBeanDefinitionParser.java
  2. 0 152
      core/src/main/java/org/springframework/security/intercept/web/AbstractFilterInvocationDefinitionSource.java
  3. 263 0
      core/src/main/java/org/springframework/security/intercept/web/DefaultFilterInvocationDefinitionSource.java
  4. 2 4
      core/src/main/java/org/springframework/security/intercept/web/FIDSToFilterChainMapConverter.java
  5. 46 25
      core/src/main/java/org/springframework/security/intercept/web/FilterInvocationDefinitionSourceEditor.java
  6. 6 22
      core/src/main/java/org/springframework/security/intercept/web/PathBasedFilterInvocationDefinitionMap.java
  7. 3 3
      core/src/main/java/org/springframework/security/intercept/web/RegExpBasedFilterInvocationDefinitionMap.java
  8. 1 1
      core/src/main/java/org/springframework/security/util/FilterInvocationUtils.java
  9. 4 0
      core/src/main/resources/org/springframework/security/config/spring-security-2.0.rnc
  10. 13 0
      core/src/main/resources/org/springframework/security/config/spring-security-2.0.xsd
  11. 21 3
      core/src/test/java/org/springframework/security/config/HttpSecurityBeanDefinitionParserTests.java
  12. 1 1
      core/src/test/java/org/springframework/security/intercept/web/AbstractFilterInvocationDefinitionSourceTests.java
  13. 15 14
      core/src/test/java/org/springframework/security/intercept/web/FilterInvocationDefinitionSourceEditorTests.java
  14. 14 13
      core/src/test/java/org/springframework/security/intercept/web/FilterInvocationDefinitionSourceEditorWithPathsTests.java
  15. 2 2
      core/src/test/java/org/springframework/security/intercept/web/MockFilterInvocationDefinitionSource.java
  16. 80 28
      core/src/test/java/org/springframework/security/intercept/web/PathBasedFilterInvocationDefinitionMapTests.java

+ 44 - 31
core/src/main/java/org/springframework/security/config/HttpSecurityBeanDefinitionParser.java

@@ -18,11 +18,8 @@ import org.springframework.security.ConfigAttributeDefinition;
 import org.springframework.security.ConfigAttributeEditor;
 import org.springframework.security.wrapper.SecurityContextHolderAwareRequestFilter;
 import org.springframework.security.context.HttpSessionContextIntegrationFilter;
-import org.springframework.security.intercept.web.AbstractFilterInvocationDefinitionSource;
-import org.springframework.security.intercept.web.FilterInvocationDefinitionMap;
+import org.springframework.security.intercept.web.DefaultFilterInvocationDefinitionSource;
 import org.springframework.security.intercept.web.FilterSecurityInterceptor;
-import org.springframework.security.intercept.web.PathBasedFilterInvocationDefinitionMap;
-import org.springframework.security.intercept.web.RegExpBasedFilterInvocationDefinitionMap;
 import org.springframework.security.securechannel.ChannelDecisionManagerImpl;
 import org.springframework.security.securechannel.ChannelProcessingFilter;
 import org.springframework.security.securechannel.InsecureChannelProcessor;
@@ -33,6 +30,7 @@ import org.springframework.security.ui.ExceptionTranslationFilter;
 import org.springframework.security.util.FilterChainProxy;
 import org.springframework.security.util.RegexUrlPathMatcher;
 import org.springframework.security.util.AntUrlPathMatcher;
+import org.springframework.security.util.UrlMatcher;
 import org.springframework.util.Assert;
 import org.springframework.util.StringUtils;
 import org.springframework.util.xml.DomUtils;
@@ -65,6 +63,8 @@ public class HttpSecurityBeanDefinitionParser implements BeanDefinitionParser {
     static final String OPT_REQUIRES_HTTPS = "https";
     static final String OPT_ANY_CHANNEL = "any";
 
+    static final String ATT_HTTP_METHOD = "method";
+
     static final String ATT_CREATE_SESSION = "create-session";
     static final String DEF_CREATE_SESSION_IF_REQUIRED = "ifRequired";
     static final String OPT_CREATE_SESSION_ALWAYS = "always";
@@ -118,8 +118,13 @@ public class HttpSecurityBeanDefinitionParser implements BeanDefinitionParser {
         	patternType = DEF_PATH_TYPE_ANT;
         }
 
-        FilterInvocationDefinitionMap interceptorFilterInvDefSource = new PathBasedFilterInvocationDefinitionMap();
-        FilterInvocationDefinitionMap channelFilterInvDefSource = new PathBasedFilterInvocationDefinitionMap();
+        boolean useRegex = patternType.equals(OPT_PATH_TYPE_REGEX);
+
+        UrlMatcher matcher = new AntUrlPathMatcher();
+
+        if (useRegex) {
+            matcher = new RegexUrlPathMatcher();
+        }        
 
         // Deal with lowercase conversion requests
         String lowercaseComparisons = element.getAttribute(ATT_LOWERCASE_COMPARISONS);
@@ -127,32 +132,26 @@ public class HttpSecurityBeanDefinitionParser implements BeanDefinitionParser {
         	lowercaseComparisons = null;
         }
 
+
         // Only change from the defaults if the attribute has been set
         if ("true".equals(lowercaseComparisons)) {
-        	interceptorFilterInvDefSource.setConvertUrlToLowercaseBeforeComparison(true);
-        	channelFilterInvDefSource.setConvertUrlToLowercaseBeforeComparison(true);
+            if (useRegex) {
+                ((RegexUrlPathMatcher)matcher).setRequiresLowerCaseUrl(true);
+            }
+            // Default for ant is already to force lower case
         } else if ("false".equals(lowercaseComparisons)) {
-        	interceptorFilterInvDefSource.setConvertUrlToLowercaseBeforeComparison(false);
-        	channelFilterInvDefSource.setConvertUrlToLowercaseBeforeComparison(false);
-        }
-
-        if (patternType.equals(OPT_PATH_TYPE_REGEX)) {
-            RegexUrlPathMatcher matcher = new RegexUrlPathMatcher();
-
-            if (lowercaseComparisons != null) {
-                matcher.setRequiresLowerCaseUrl("true".equals(lowercaseComparisons));
+            if (!useRegex) {
+                ((AntUrlPathMatcher)matcher).setRequiresLowerCaseUrl(false);
             }
+            // Default for regex is no change
+        }
 
-            filterChainProxy.getPropertyValues().addPropertyValue("matcher", matcher);
-
-            interceptorFilterInvDefSource = new RegExpBasedFilterInvocationDefinitionMap();
-            channelFilterInvDefSource = new RegExpBasedFilterInvocationDefinitionMap();
-        } else if (lowercaseComparisons != null) {
-            AntUrlPathMatcher matcher = new AntUrlPathMatcher();
-            matcher.setRequiresLowerCaseUrl("true".equals(lowercaseComparisons));
+        DefaultFilterInvocationDefinitionSource interceptorFilterInvDefSource =
+                new DefaultFilterInvocationDefinitionSource(matcher);
+        DefaultFilterInvocationDefinitionSource channelFilterInvDefSource =
+                new DefaultFilterInvocationDefinitionSource(matcher);
 
-            filterChainProxy.getPropertyValues().addPropertyValue("matcher", matcher);
-        }
+        filterChainProxy.getPropertyValues().addPropertyValue("matcher", matcher);
 
         // Add servlet-api integration filter if required
         String provideServletApi = element.getAttribute(ATT_SERVLET_API_PROVISION);
@@ -181,11 +180,16 @@ public class HttpSecurityBeanDefinitionParser implements BeanDefinitionParser {
         filterSecurityInterceptorBuilder.addPropertyValue("authenticationManager",
                 ConfigUtils.registerProviderManagerIfNecessary(parserContext));
 
+        // SEC-501 - should paths stored in request maps be converted to lower case
+        // true if Ant path and using lower case
+        boolean convertPathsToLowerCase = (matcher instanceof AntUrlPathMatcher) && matcher.requiresLowerCaseUrl();
+
         parseInterceptUrls(DomUtils.getChildElementsByTagName(element, "intercept-url"),
-                filterChainMap, interceptorFilterInvDefSource, channelFilterInvDefSource, parserContext);
+                filterChainMap, interceptorFilterInvDefSource, channelFilterInvDefSource,
+                convertPathsToLowerCase, parserContext);
 
         // Check if we need to register the channel processing beans
-        if (((AbstractFilterInvocationDefinitionSource)channelFilterInvDefSource).getMapSize() > 0) {
+        if (((DefaultFilterInvocationDefinitionSource)channelFilterInvDefSource).getMapSize() > 0) {
             // At least one channel requirement has been specified
             RootBeanDefinition channelFilter = new RootBeanDefinition(ChannelProcessingFilter.class);
             channelFilter.getPropertyValues().addPropertyValue("channelDecisionManager",
@@ -268,8 +272,9 @@ public class HttpSecurityBeanDefinitionParser implements BeanDefinitionParser {
      * FilterInvocationDefinitionSource used in FilterSecurityInterceptor.
      */
     private void parseInterceptUrls(List urlElts, Map filterChainMap,
-            FilterInvocationDefinitionMap interceptorFilterInvDefSource,
-            FilterInvocationDefinitionMap channelFilterInvDefSource, ParserContext parserContext) {
+            DefaultFilterInvocationDefinitionSource interceptorFilterInvDefSource,
+            DefaultFilterInvocationDefinitionSource channelFilterInvDefSource,
+            boolean useLowerCasePaths, ParserContext parserContext) {
 
         Iterator urlEltsIterator = urlElts.iterator();
 
@@ -279,6 +284,14 @@ public class HttpSecurityBeanDefinitionParser implements BeanDefinitionParser {
             Element urlElt = (Element) urlEltsIterator.next();
 
             String path = urlElt.getAttribute(ATT_PATH_PATTERN);
+            if (useLowerCasePaths) {
+                path = path.toLowerCase();
+            }
+
+            String method = urlElt.getAttribute(ATT_HTTP_METHOD);
+            if (!StringUtils.hasText(method)) {
+                method = null;
+            }
 
             Assert.hasText(path, "path attribute cannot be empty or null");
 
@@ -287,7 +300,7 @@ public class HttpSecurityBeanDefinitionParser implements BeanDefinitionParser {
             // Convert the comma-separated list of access attributes to a ConfigAttributeDefinition
             if (StringUtils.hasText(access)) {
                 editor.setAsText(access);
-                interceptorFilterInvDefSource.addSecureUrl(path, (ConfigAttributeDefinition) editor.getValue());
+                interceptorFilterInvDefSource.addSecureUrl(path, method, (ConfigAttributeDefinition) editor.getValue());
             }
 
             String requiredChannel = urlElt.getAttribute(ATT_REQUIRES_CHANNEL);

+ 0 - 152
core/src/main/java/org/springframework/security/intercept/web/AbstractFilterInvocationDefinitionSource.java

@@ -1,152 +0,0 @@
-/* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
- *
- * 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.intercept.web;
-
-import org.springframework.security.ConfigAttributeDefinition;
-import org.springframework.security.util.UrlMatcher;
-
-import org.apache.commons.logging.Log;
-import org.apache.commons.logging.LogFactory;
-
-import java.util.Map;
-import java.util.LinkedHashMap;
-import java.util.Iterator;
-
-
-/**
- * Abstract implementation of <Code>FilterInvocationDefinitionSource</code>.
- * <p>
- * Stores an ordered map of compiled URL paths to <tt>ConfigAttributeDefinition</tt>s and provides URL matching
- * against the items stored in this map using the confgured <tt>UrlMatcher</tt>.
- * <p>
- * The order of registering the regular expressions using the {@link #addSecureUrl(String,
- * ConfigAttributeDefinition)} is very important. The system will identify the <b>first</b>  matching regular
- * expression for a given HTTP URL. It will not proceed to evaluate later regular expressions if a match has already
- * been found. Accordingly, the most specific regular expressions should be registered first, with the most general
- * regular expressions registered last.
- *
- * @author Ben Alex
- * @author Luke Taylor
- * @version $Id$
- */
-public abstract class AbstractFilterInvocationDefinitionSource implements FilterInvocationDefinitionSource {
-
-    protected final Log logger = LogFactory.getLog(getClass());
-
-    private Map requestMap = new LinkedHashMap();
-
-    private UrlMatcher urlMatcher;
-
-    protected AbstractFilterInvocationDefinitionSource(UrlMatcher urlMatcher) {
-        this.urlMatcher = urlMatcher;
-    }
-
-    //~ Methods ========================================================================================================
-
-    /**
-     * Adds a URL-ConfigAttributeDefinition pair to the request map, first allowing the <tt>UrlMatcher</tt> to
-     * process the pattern if required, using its <tt>compile</tt> method. The returned object will be used as the key
-     * to the request map and will be passed back to the <tt>UrlMatcher</tt> when iterating through the map to find
-     * a match for a particular URL.
-     */
-    public void addSecureUrl(String pattern, ConfigAttributeDefinition attr) {
-        requestMap.put(urlMatcher.compile(pattern), attr);
-
-        if (logger.isDebugEnabled()) {
-            logger.debug("Added URL pattern: " + pattern + "; attributes: " + attr);
-        }
-    }
-
-    public Iterator getConfigAttributeDefinitions() {
-        return getRequestMap().values().iterator();
-    }
-
-    public ConfigAttributeDefinition getAttributes(Object object) throws IllegalArgumentException {
-        if ((object == null) || !this.supports(object.getClass())) {
-            throw new IllegalArgumentException("Object must be a FilterInvocation");
-        }
-
-        String url = ((FilterInvocation) object).getRequestUrl();
-
-        return lookupAttributes(url);
-    }
-
-    /**
-     * Performs the actual lookup of the relevant <code>ConfigAttributeDefinition</code> for the specified
-     * <code>FilterInvocation</code>.
-     * <p>
-     * By default, iterates through the stored URL map and calls the
-     * {@link UrlMatcher#pathMatchesUrl(Object path, String url)} method until a match is found.
-     * <p>
-     * Subclasses can override if required to perform any modifications to the URL.
-     * <p>
-     * Public visiblity so that tablibs or other view helper classes can access the
-     * <code>ConfigAttributeDefinition</code> applying to a given URI pattern without needing to construct a mock
-     * <code>FilterInvocation</code> and retrieving the attibutes via the {@link #getAttributes(Object)} method.
-     *
-     * @param url the URI to retrieve configuration attributes for
-     *
-     * @return the <code>ConfigAttributeDefinition</code> that applies to the specified <code>FilterInvocation</code>
-     * or null if no match is foud
-     */
-    public ConfigAttributeDefinition lookupAttributes(String url) {
-        if (urlMatcher.requiresLowerCaseUrl()) {
-            url = url.toLowerCase();
-
-            if (logger.isDebugEnabled()) {
-                logger.debug("Converted URL to lowercase, from: '" + url + "'; to: '" + url + "'");
-            }
-        }
-
-        Iterator entries = requestMap.entrySet().iterator();
-
-        while (entries.hasNext()) {
-            Map.Entry entry = (Map.Entry) entries.next();
-            Object p = entry.getKey();
-            boolean matched = urlMatcher.pathMatchesUrl(p, url);
-
-            if (logger.isDebugEnabled()) {
-                logger.debug("Candidate is: '" + url + "'; pattern is " + p + "; matched=" + matched);
-            }
-
-            if (matched) {
-                return (ConfigAttributeDefinition) entry.getValue();
-            }
-        }
-
-        return null;
-    }
-
-    public boolean supports(Class clazz) {
-        return FilterInvocation.class.isAssignableFrom(clazz);
-    }
-
-    public int getMapSize() {
-        return this.requestMap.size();
-    }
-
-    Map getRequestMap() {
-        return requestMap;
-    }
-
-    protected UrlMatcher getUrlMatcher() {
-        return urlMatcher;
-    }
-
-    public boolean isConvertUrlToLowercaseBeforeComparison() {
-        return urlMatcher.requiresLowerCaseUrl();
-    }
-}

+ 263 - 0
core/src/main/java/org/springframework/security/intercept/web/DefaultFilterInvocationDefinitionSource.java

@@ -0,0 +1,263 @@
+/* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
+ *
+ * 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.intercept.web;
+
+import org.springframework.security.ConfigAttributeDefinition;
+import org.springframework.security.SecurityConfig;
+import org.springframework.security.util.UrlMatcher;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import java.util.Map;
+import java.util.LinkedHashMap;
+import java.util.Iterator;
+import java.util.HashMap;
+import java.util.Set;
+import java.util.HashSet;
+import java.util.Arrays;
+import java.util.List;
+
+
+/**
+ * Default implementation of <tt>FilterInvocationDefinitionSource</tt>.
+ * <p>
+ * Stores an ordered map of compiled URL paths to <tt>ConfigAttributeDefinition</tt>s and provides URL matching
+ * against the items stored in this map using the configured <tt>UrlMatcher</tt>.
+ * <p>
+ * The order of registering the regular expressions using the
+ * {@link #addSecureUrl(String, ConfigAttributeDefinition)} is very important.
+ * The system will identify the <b>first</b>  matching regular
+ * expression for a given HTTP URL. It will not proceed to evaluate later regular expressions if a match has already
+ * been found. Accordingly, the most specific regular expressions should be registered first, with the most general
+ * regular expressions registered last.
+ * <p>
+ * If URLs are registered for a particular HTTP method using
+ * {@link #addSecureUrl(String, String, ConfigAttributeDefinition)}, then the method-specific matches will take
+ * precedence over any URLs which are registered without an HTTP method. 
+ *
+ * @author Ben Alex
+ * @author Luke Taylor
+ * @version $Id$
+ */
+public class DefaultFilterInvocationDefinitionSource implements FilterInvocationDefinitionSource {
+
+    private static final Set HTTP_METHODS = new HashSet(Arrays.asList(new String[]{ "GET", "PUT", "DELETE", "POST" }));
+
+    protected final Log logger = LogFactory.getLog(getClass());
+
+    /**
+     * Non method-specific map of URL patterns to <tt>ConfigAttributeDefinition</tt>s
+     * TODO: Store in the httpMethod map with null key.  
+     */
+    private Map requestMap = new LinkedHashMap();
+    /** Stores request maps keyed by specific HTTP methods */
+    private Map httpMethodMap = new HashMap();
+
+    private UrlMatcher urlMatcher;
+
+    private boolean stripQueryStringFromUrls;
+
+    /**
+     * Creates a FilterInvocationDefinitionSource with the supplied URL matching strategy.
+     * @param urlMatcher
+     */
+    public DefaultFilterInvocationDefinitionSource(UrlMatcher urlMatcher) {
+        this.urlMatcher = urlMatcher;
+    }
+
+    //~ Methods ========================================================================================================
+
+    public void addSecureUrl(String pattern, ConfigAttributeDefinition attr) {
+        addSecureUrl(pattern, null, attr);
+    }
+
+    /**
+     * Adds a URL-ConfigAttributeDefinition pair to the request map, first allowing the <tt>UrlMatcher</tt> to
+     * process the pattern if required, using its <tt>compile</tt> method. The returned object will be used as the key
+     * to the request map and will be passed back to the <tt>UrlMatcher</tt> when iterating through the map to find
+     * a match for a particular URL.
+     */
+    public void addSecureUrl(String pattern, String method, ConfigAttributeDefinition attr) {
+        Map mapToUse = getRequestMapForHttpMethod(method);
+
+        mapToUse.put(urlMatcher.compile(pattern), attr);
+
+        if (logger.isDebugEnabled()) {
+            logger.debug("Added URL pattern: " + pattern + "; attributes: " + attr +
+                    (method == null ? "" : " for HTTP method '" + method + "'"));
+        }
+    }
+
+    /**
+     * Return the HTTP method specific request map, creating it if it doesn't already exist.
+     * @param method GET, POST etc
+     * @return map of URL patterns to <tt>ConfigAttributeDefinition</tt>s for this method.
+     */
+    private Map getRequestMapForHttpMethod(String method) {
+        if (method == null) {
+            return requestMap;
+        }
+        if (!HTTP_METHODS.contains(method)) {
+            throw new IllegalArgumentException("Unrecognised HTTP method: '" + method + "'");
+        }
+
+        Map methodRequestmap = (Map) httpMethodMap.get(method);
+
+        if (methodRequestmap == null) {
+            methodRequestmap = new LinkedHashMap();
+            httpMethodMap.put(method, methodRequestmap);
+        }
+
+        return methodRequestmap;
+    }
+
+    public Iterator getConfigAttributeDefinitions() {
+        return getRequestMap().values().iterator();
+    }
+
+    public ConfigAttributeDefinition getAttributes(Object object) throws IllegalArgumentException {
+        if ((object == null) || !this.supports(object.getClass())) {
+            throw new IllegalArgumentException("Object must be a FilterInvocation");
+        }
+
+        String url = ((FilterInvocation) object).getRequestUrl();
+        String method = ((FilterInvocation) object).getHttpRequest().getMethod();
+
+        return lookupAttributes(url, method);
+    }
+
+    protected ConfigAttributeDefinition lookupAttributes(String url) {
+        return lookupAttributes(url, null);
+    }
+
+    /**
+     * Performs the actual lookup of the relevant <code>ConfigAttributeDefinition</code> for the specified
+     * <code>FilterInvocation</code>.
+     * <p>
+     * By default, iterates through the stored URL map and calls the
+     * {@link UrlMatcher#pathMatchesUrl(Object path, String url)} method until a match is found.
+     * <p>
+     * Subclasses can override if required to perform any modifications to the URL.
+     *
+     * @param url the URI to retrieve configuration attributes for
+     * @param method the HTTP method (GET, POST, DELETE...).
+     *
+     * @return the <code>ConfigAttributeDefinition</code> that applies to the specified <code>FilterInvocation</code>
+     * or null if no match is foud
+     */
+    protected ConfigAttributeDefinition lookupAttributes(String url, String method) {
+        if (stripQueryStringFromUrls) {
+            // Strip anything after a question mark symbol, as per SEC-161. See also SEC-321
+            int firstQuestionMarkIndex = url.indexOf("?");
+
+            if (firstQuestionMarkIndex != -1) {
+                url = url.substring(0, firstQuestionMarkIndex);
+            }            
+        }
+
+        if (urlMatcher.requiresLowerCaseUrl()) {
+            url = url.toLowerCase();
+
+            if (logger.isDebugEnabled()) {
+                logger.debug("Converted URL to lowercase, from: '" + url + "'; to: '" + url + "'");
+            }
+        }
+
+        ConfigAttributeDefinition attributes = null;
+
+        Map methodSpecificMap = (Map) httpMethodMap.get(method);
+
+        if (methodSpecificMap != null) {
+            attributes = lookupUrlInMap(methodSpecificMap, url);
+        }
+
+        if (attributes == null) {
+            attributes = lookupUrlInMap(requestMap, url);
+        }
+
+        return attributes;
+    }
+
+    private ConfigAttributeDefinition lookupUrlInMap(Map requestMap, String url) {
+        Iterator entries = requestMap.entrySet().iterator();
+
+        while (entries.hasNext()) {
+            Map.Entry entry = (Map.Entry) entries.next();
+            Object p = entry.getKey();
+            boolean matched = urlMatcher.pathMatchesUrl(p, url);
+
+            if (logger.isDebugEnabled()) {
+                logger.debug("Candidate is: '" + url + "'; pattern is " + p + "; matched=" + matched);
+            }
+
+            if (matched) {
+                return (ConfigAttributeDefinition) entry.getValue();
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Allows or easier configuration using {@link FilterInvocationDefinitionSourceMapping}.
+     *
+     * @param mappings
+     *            {@link java.util.List} of
+     *            {@link FilterInvocationDefinitionSourceMapping} objects.
+     */
+    void setMappings(List mappings) {
+        Iterator it = mappings.iterator();
+
+        while (it.hasNext()) {
+            FilterInvocationDefinitionSourceMapping mapping = (FilterInvocationDefinitionSourceMapping) it.next();
+            ConfigAttributeDefinition configDefinition = new ConfigAttributeDefinition();
+
+            Iterator configAttributesIt = mapping.getConfigAttributes().iterator();
+            while (configAttributesIt.hasNext()) {
+                String s = (String) configAttributesIt.next();
+                configDefinition.addConfigAttribute(new SecurityConfig(s));
+            }
+
+            addSecureUrl(mapping.getUrl(), configDefinition);
+        }
+    }
+
+
+    public boolean supports(Class clazz) {
+        return FilterInvocation.class.isAssignableFrom(clazz);
+    }
+
+    public int getMapSize() {
+        return this.requestMap.size();
+    }
+
+    Map getRequestMap() {
+        return requestMap;
+    }
+
+    protected UrlMatcher getUrlMatcher() {
+        return urlMatcher;
+    }
+
+    public boolean isConvertUrlToLowercaseBeforeComparison() {
+        return urlMatcher.requiresLowerCaseUrl();
+    }
+
+    protected void setStripQueryStringFromUrls(boolean stripQueryStringFromUrls) {
+        this.stripQueryStringFromUrls = stripQueryStringFromUrls;
+    }
+}

+ 2 - 4
core/src/main/java/org/springframework/security/intercept/web/FIDSToFilterChainMapConverter.java

@@ -29,13 +29,11 @@ public class FIDSToFilterChainMapConverter {
         // TODO: Check if this is necessary. Retained from refactoring of FilterChainProxy
         Assert.notNull(source.getConfigAttributeDefinitions(), "FilterChainProxy requires the " +
                 "FilterInvocationDefinitionSource to return a non-null response to getConfigAttributeDefinitions()");
-        Assert.isTrue(
-            source instanceof PathBasedFilterInvocationDefinitionMap ||
-            source instanceof RegExpBasedFilterInvocationDefinitionMap,
+        Assert.isTrue(source instanceof DefaultFilterInvocationDefinitionSource,
                 "Can't handle FilterInvocationDefinitionSource type " + source.getClass());
         
 
-        AbstractFilterInvocationDefinitionSource fids = (AbstractFilterInvocationDefinitionSource)source;
+        DefaultFilterInvocationDefinitionSource fids = (DefaultFilterInvocationDefinitionSource)source;
         Map requestMap = fids.getRequestMap();
         Iterator paths = requestMap.keySet().iterator();
 

+ 46 - 25
core/src/main/java/org/springframework/security/intercept/web/FilterInvocationDefinitionSourceEditor.java

@@ -23,20 +23,31 @@ import java.util.ArrayList;
 import java.util.List;
 
 import org.springframework.security.util.StringSplitUtils;
+import org.springframework.security.util.RegexUrlPathMatcher;
+import org.springframework.security.util.UrlMatcher;
+import org.springframework.security.util.AntUrlPathMatcher;
+
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
 import org.springframework.util.StringUtils;
 
 
 /**
- * Property editor to assist with the setup of a {@link FilterInvocationDefinitionSource}.<p>The class creates and
- * populates a {@link RegExpBasedFilterInvocationDefinitionMap} or {@link PathBasedFilterInvocationDefinitionMap}
- * (depending on the type of patterns presented).</p>
- *  <p>By default the class treats presented patterns as regular expressions. If the keyword
+ * Property editor to assist with the setup of a {@link FilterInvocationDefinitionSource}.
+ * <p>
+ * Note that from version 2.0, the use of property-editor based configuration is deprecated in favour of namespace
+ * configuration options.
+ * <p>
+ * The class creates and populates a
+ * {@link org.springframework.security.intercept.web.DefaultFilterInvocationDefinitionSource}
+ * using either an Ant or Regular Expression URL matching strategy depending on the type of patterns presented.
+ * <p>
+ * By default the class treats presented patterns as regular expressions. If the keyword
  * <code>PATTERN_TYPE_APACHE_ANT</code> is present (case sensitive), patterns will be treated as Apache Ant paths
- * rather than regular expressions.</p>
+ * rather than regular expressions.
  *
  * @author Ben Alex
+ * @deprecated Use namespace configuration instead. May be removed in future versions.
  * @version $Id$
  */
 public class FilterInvocationDefinitionSourceEditor extends PropertyEditorSupport {
@@ -50,38 +61,50 @@ public class FilterInvocationDefinitionSourceEditor extends PropertyEditorSuppor
     //~ Methods ========================================================================================================
 
     public void setAsText(String s) throws IllegalArgumentException {
-        FilterInvocationDefinitionDecorator source = new FilterInvocationDefinitionDecorator();
+        //FilterInvocationDefinitionDecorator source = new FilterInvocationDefinitionDecorator();
 
         if ((s == null) || "".equals(s)) {
             // Leave target object empty
-            setValue(new RegExpBasedFilterInvocationDefinitionMap());
+            setValue(new DefaultFilterInvocationDefinitionSource(new RegexUrlPathMatcher()));
 
             return;
         }
 
-        // Check if we need to override the default definition map
-        if (s.lastIndexOf(DIRECTIVE_PATTERN_TYPE_APACHE_ANT) != -1) {
-            source.setDecorated(new PathBasedFilterInvocationDefinitionMap());
+        boolean useAnt = s.lastIndexOf(DIRECTIVE_PATTERN_TYPE_APACHE_ANT) != -1;
+        boolean converUrlToLowerCase = s.lastIndexOf(DIRECTIVE_CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON) != -1;
 
-            if (logger.isDebugEnabled()) {
+        if (logger.isDebugEnabled()) {
+            if (useAnt) {
                 logger.debug(("Detected " + DIRECTIVE_PATTERN_TYPE_APACHE_ANT
                     + " directive; using Apache Ant style path expressions"));
             }
-        } else {
-            source.setDecorated(new RegExpBasedFilterInvocationDefinitionMap());
-        }
 
-        if (s.lastIndexOf(DIRECTIVE_CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON) != -1) {
-            if (logger.isDebugEnabled()) {
-                logger.debug("Detected " + DIRECTIVE_CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
-                    + " directive; Instructing mapper to convert URLs to lowercase before comparison");
+            if (converUrlToLowerCase) {
+                if (logger.isDebugEnabled()) {
+                    logger.debug("Detected " + DIRECTIVE_CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
+                        + " directive; Instructing mapper to convert URLs to lowercase before comparison");
+                }
             }
+        }
+
+        UrlMatcher matcher;
+
+        if (useAnt) {
+            matcher = new AntUrlPathMatcher();
+            ((AntUrlPathMatcher)matcher).setRequiresLowerCaseUrl(converUrlToLowerCase);
 
-            source.setConvertUrlToLowercaseBeforeComparison(true);
         } else {
-            source.setConvertUrlToLowercaseBeforeComparison(false);
+            matcher = new RegexUrlPathMatcher();
+            ((RegexUrlPathMatcher)matcher).setRequiresLowerCaseUrl(converUrlToLowerCase);
         }
 
+        DefaultFilterInvocationDefinitionSource fids = new DefaultFilterInvocationDefinitionSource(matcher);
+
+        if (useAnt) {
+            fids.setStripQueryStringFromUrls(true);
+        }
+
+
         BufferedReader br = new BufferedReader(new StringReader(s));
         int counter = 0;
         String line;
@@ -147,8 +170,7 @@ public class FilterInvocationDefinitionSourceEditor extends PropertyEditorSuppor
             }
 
             // Attempt to detect malformed lines (as per SEC-204)
-            if (source.isConvertUrlToLowercaseBeforeComparison()
-                && source.getDecorated() instanceof PathBasedFilterInvocationDefinitionMap) {
+            if (converUrlToLowerCase && useAnt) {
                 // Should all be lowercase; check each character
                 // We only do this for Ant (regexp have control chars)
                 for (int i = 0; i < name.length(); i++) {
@@ -174,9 +196,8 @@ public class FilterInvocationDefinitionSourceEditor extends PropertyEditorSuppor
 
             mappings.add(mapping);
         }
-        source.setMappings(mappings);
-
+        fids.setMappings(mappings);
 
-        setValue(source.getDecorated());
+        setValue(fids);
     }
 }

+ 6 - 22
core/src/main/java/org/springframework/security/intercept/web/PathBasedFilterInvocationDefinitionMap.java

@@ -18,11 +18,8 @@ package org.springframework.security.intercept.web;
 import org.springframework.security.ConfigAttributeDefinition;
 import org.springframework.security.util.AntUrlPathMatcher;
 
-import java.util.Iterator;
-
-
 /**
- * Extends AbstractFilterInvocationDefinitionSource, configuring it with a {@link AntUrlPathMatcher} to match URLs
+ * Extends DefaultFilterInvocationDefinitionSource, configuring it with a {@link AntUrlPathMatcher} to match URLs
  * using Apache Ant path-based patterns.
  * <p>
  * Apache Ant path expressions are used to match a HTTP request URL against a <code>ConfigAttributeDefinition</code>.
@@ -39,42 +36,29 @@ import java.util.Iterator;
  *
  * @author Ben Alex
  * @author Luke taylor
+ * @deprecated DefaultFilterInvocationDefinitionSource should now be used with an AntUrlPathMatcher instead.
  * @version $Id$
  */
-public class PathBasedFilterInvocationDefinitionMap extends AbstractFilterInvocationDefinitionSource
+public class PathBasedFilterInvocationDefinitionMap extends DefaultFilterInvocationDefinitionSource
         implements FilterInvocationDefinition {
 
     //~ Constructors ===================================================================================================
 
     public PathBasedFilterInvocationDefinitionMap() {
         super(new AntUrlPathMatcher());
+        setStripQueryStringFromUrls(true);
     }
 
     //~ Methods ========================================================================================================
 
-    public void addSecureUrl(String antPath, ConfigAttributeDefinition attr) {
+    public void addSecureUrl(String antPath, String method, ConfigAttributeDefinition attr) {
         // SEC-501: If using lower case comparison, we should convert the paths to lower case
         // as any upper case characters included by mistake will prevent the URL from ever being matched.
         if (getUrlMatcher().requiresLowerCaseUrl()) {
             antPath = antPath.toLowerCase();
         }
 
-        super.addSecureUrl(antPath, attr);
-    }
-
-    public Iterator getConfigAttributeDefinitions() {
-        return getRequestMap().values().iterator();
-    }
-
-    public ConfigAttributeDefinition lookupAttributes(String url) {
-        // Strip anything after a question mark symbol, as per SEC-161. See also SEC-321
-        int firstQuestionMarkIndex = url.indexOf("?");
-
-        if (firstQuestionMarkIndex != -1) {
-            url = url.substring(0, firstQuestionMarkIndex);
-        }
-
-        return super.lookupAttributes(url);
+        super.addSecureUrl(antPath, method, attr);
     }
 
     public void setConvertUrlToLowercaseBeforeComparison(boolean bool) {

+ 3 - 3
core/src/main/java/org/springframework/security/intercept/web/RegExpBasedFilterInvocationDefinitionMap.java

@@ -16,18 +16,18 @@
 package org.springframework.security.intercept.web;
 
 import org.springframework.security.util.RegexUrlPathMatcher;
-import org.springframework.security.util.AntUrlPathMatcher;
 
 
 /**
- * Configures an {@link AbstractFilterInvocationDefinitionSource} with a regular expression URL matching strategy
+ * Configures an {@link DefaultFilterInvocationDefinitionSource} with a regular expression URL matching strategy
  * {@link RegexUrlPathMatcher}.
  *
  * @author Ben Alex
  * @author Luke Taylor
+ * @deprecated
  * @version $Id$
  */
-public class RegExpBasedFilterInvocationDefinitionMap extends AbstractFilterInvocationDefinitionSource
+public class RegExpBasedFilterInvocationDefinitionMap extends DefaultFilterInvocationDefinitionSource
     implements FilterInvocationDefinition {
 
     //~ Constructors ===================================================================================================

+ 1 - 1
core/src/main/java/org/springframework/security/util/FilterInvocationUtils.java

@@ -48,7 +48,7 @@ public final class FilterInvocationUtils {
 
     /**
      * Creates a <code>FilterInvocation</code> for the specified <code>contextPath</code> and <code>Uri</code>.
-     * Note the normal subclasses of <code>AbstractFilterInvocationDefinitionSource</code> disregard the
+     * Note the normal subclasses of <code>DefaultFilterInvocationDefinitionSource</code> disregard the
      * <code>contextPath</code> when evaluating which secure object metadata applies to a given
      * <code>FilterInvocation</code>, so generally the <code>contextPath</code> is unimportant unless you are using a
      * custom <code>FilterInvocationDefinitionSource</code>.

+ 4 - 0
core/src/main/resources/org/springframework/security/config/spring-security-2.0.rnc

@@ -132,6 +132,10 @@ intercept-url.attlist &=
 intercept-url.attlist &=
     ## The access configuration attributes that apply for the configured path.
     attribute access {xsd:string}?
+intercept-url.attlist &=
+    ## The HTTP Method for which the access configuration attributes should apply. If not specified, the attributes will apply to any method.
+    attribute method {"GET" | "PUT" | "POST" | "DELETE"}?
+
 intercept-url.attlist &=
     ## The filter list for the path. Currently can be set to "none" to remove a path from having any filters applied. The full filter stack (consisting of all defined filters, will be applied to any other paths).
     attribute filters {"none"}?

+ 13 - 0
core/src/main/resources/org/springframework/security/config/spring-security-2.0.xsd

@@ -355,6 +355,19 @@
         <xs:documentation>The access configuration attributes that apply for the configured path.</xs:documentation>
       </xs:annotation>
     </xs:attribute>
+    <xs:attribute name="method">
+      <xs:annotation>
+        <xs:documentation>The HTTP Method for which the access configuration attributes should apply. If not specified, the attributes will apply to any method.</xs:documentation>
+      </xs:annotation>
+      <xs:simpleType>
+        <xs:restriction base="xs:token">
+          <xs:enumeration value="GET"/>
+          <xs:enumeration value="PUT"/>
+          <xs:enumeration value="POST"/>
+          <xs:enumeration value="DELETE"/>
+        </xs:restriction>
+      </xs:simpleType>
+    </xs:attribute>
     <xs:attribute name="filters">
       <xs:annotation>
         <xs:documentation>The filter list for the path. Currently can be set to "none" to remove a path from having any filters applied. The full filter stack (consisting of all defined filters, will be applied to any other paths).</xs:documentation>

+ 21 - 3
core/src/test/java/org/springframework/security/config/HttpSecurityBeanDefinitionParserTests.java

@@ -132,15 +132,32 @@ public class HttpSecurityBeanDefinitionParserTests {
         FilterSecurityInterceptor fis = (FilterSecurityInterceptor) appContext.getBean(BeanIds.FILTER_SECURITY_INTERCEPTOR);
 
         FilterInvocationDefinitionSource fids = fis.getObjectDefinitionSource();
-        ConfigAttributeDefinition attrs = fids.getAttributes(createFilterinvocation("/Secure"));
+        ConfigAttributeDefinition attrs = fids.getAttributes(createFilterinvocation("/Secure", null));
         assertEquals(2, attrs.size());
         assertTrue(attrs.contains(new SecurityConfig("ROLE_A")));
         assertTrue(attrs.contains(new SecurityConfig("ROLE_B")));
-        attrs = fids.getAttributes(createFilterinvocation("/secure"));
+        attrs = fids.getAttributes(createFilterinvocation("/secure", null));
         assertEquals(1, attrs.size());
         assertTrue(attrs.contains(new SecurityConfig("ROLE_C")));
     }
 
+    @Test
+    public void httpMethodMatchIsSupported() throws Exception {
+        setContext(
+                "    <http auto-config='true'>" +
+                "        <intercept-url pattern='/**' access='ROLE_C' />" +
+                "        <intercept-url pattern='/secure*' method='DELETE' access='ROLE_SUPERVISOR' />" +                        
+                "        <intercept-url pattern='/secure*' method='POST' access='ROLE_A,ROLE_B' />" +
+                "    </http>" + AUTH_PROVIDER_XML);
+
+        FilterSecurityInterceptor fis = (FilterSecurityInterceptor) appContext.getBean(BeanIds.FILTER_SECURITY_INTERCEPTOR);
+        FilterInvocationDefinitionSource fids = fis.getObjectDefinitionSource();
+        ConfigAttributeDefinition attrs = fids.getAttributes(createFilterinvocation("/secure", "POST"));
+        assertEquals(2, attrs.size());
+        assertTrue(attrs.contains(new SecurityConfig("ROLE_A")));
+        assertTrue(attrs.contains(new SecurityConfig("ROLE_B")));
+    }
+
     @Test
     public void minimalConfigurationParses() {
         setContext("<http><http-basic /></http>" + AUTH_PROVIDER_XML);
@@ -213,8 +230,9 @@ public class HttpSecurityBeanDefinitionParserTests {
         return (FilterChainProxy) appContext.getBean(BeanIds.FILTER_CHAIN_PROXY);
     }
 
-    private FilterInvocation createFilterinvocation(String path) {
+    private FilterInvocation createFilterinvocation(String path, String method) {
         MockHttpServletRequest request = new MockHttpServletRequest();
+        request.setMethod(method);
         request.setRequestURI(null);
 
         request.setServletPath(path);

+ 1 - 1
core/src/test/java/org/springframework/security/intercept/web/AbstractFilterInvocationDefinitionSourceTests.java

@@ -29,7 +29,7 @@ import javax.servlet.ServletResponse;
 
 
 /**
- * Tests {@link AbstractFilterInvocationDefinitionSource}.
+ * Tests {@link DefaultFilterInvocationDefinitionSource}.
  *
  * @author Ben Alex
  * @version $Id$

+ 15 - 14
core/src/test/java/org/springframework/security/intercept/web/FilterInvocationDefinitionSourceEditorTests.java

@@ -20,6 +20,7 @@ import junit.framework.TestCase;
 import org.springframework.security.ConfigAttributeDefinition;
 import org.springframework.security.MockFilterChain;
 import org.springframework.security.SecurityConfig;
+import org.springframework.security.util.RegexUrlPathMatcher;
 
 import org.springframework.mock.web.MockHttpServletRequest;
 import org.springframework.mock.web.MockHttpServletResponse;
@@ -51,7 +52,7 @@ public class FilterInvocationDefinitionSourceEditorTests extends TestCase {
         FilterInvocationDefinitionSourceEditor editor = new FilterInvocationDefinitionSourceEditor();
         editor.setAsText("\\A/secure/super.*\\Z=ROLE_WE_DONT_HAVE\r\n\\A/secure/.*\\Z=ROLE_SUPERVISOR,ROLE_TELLER");
 
-        RegExpBasedFilterInvocationDefinitionMap map = (RegExpBasedFilterInvocationDefinitionMap) editor.getValue();
+        DefaultFilterInvocationDefinitionSource map = (DefaultFilterInvocationDefinitionSource) editor.getValue();
         assertFalse(map.isConvertUrlToLowercaseBeforeComparison());
     }
 
@@ -72,7 +73,7 @@ public class FilterInvocationDefinitionSourceEditorTests extends TestCase {
         editor.setAsText(
             "CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON\r\n\\A/secure/super.*\\Z=ROLE_WE_DONT_HAVE\r\n\\A/secure/.*\\Z=ROLE_SUPERVISOR,ROLE_TELLER");
 
-        RegExpBasedFilterInvocationDefinitionMap map = (RegExpBasedFilterInvocationDefinitionMap) editor.getValue();
+        DefaultFilterInvocationDefinitionSource map = (DefaultFilterInvocationDefinitionSource) editor.getValue();
         assertTrue(map.isConvertUrlToLowercaseBeforeComparison());
     }
 
@@ -80,8 +81,8 @@ public class FilterInvocationDefinitionSourceEditorTests extends TestCase {
         FilterInvocationDefinitionSourceEditor editor = new FilterInvocationDefinitionSourceEditor();
         editor.setAsText("\\A/secure/super.*\\Z=ROLE_WE_DONT_HAVE\r\n\\A/secure/.*\\Z=ROLE_SUPERVISOR,ROLE_TELLER");
 
-        FilterInvocationDefinitionMap map = (FilterInvocationDefinitionMap) editor.getValue();
-        assertTrue(map instanceof RegExpBasedFilterInvocationDefinitionMap);
+        DefaultFilterInvocationDefinitionSource map = (DefaultFilterInvocationDefinitionSource) editor.getValue();
+        assertTrue(map.getUrlMatcher() instanceof RegexUrlPathMatcher);
     }
 
     public void testDetectsDuplicateDirectivesOnSameLineSituation1() {
@@ -124,7 +125,7 @@ public class FilterInvocationDefinitionSourceEditorTests extends TestCase {
         FilterInvocationDefinitionSourceEditor editor = new FilterInvocationDefinitionSourceEditor();
         editor.setAsText("");
 
-        RegExpBasedFilterInvocationDefinitionMap map = (RegExpBasedFilterInvocationDefinitionMap) editor.getValue();
+        DefaultFilterInvocationDefinitionSource map = (DefaultFilterInvocationDefinitionSource) editor.getValue();
         assertEquals(0, map.getMapSize());
     }
 
@@ -142,7 +143,7 @@ public class FilterInvocationDefinitionSourceEditorTests extends TestCase {
         FilterInvocationDefinitionSourceEditor editor = new FilterInvocationDefinitionSourceEditor();
         editor.setAsText("\\A/secure/super.*\\Z=ROLE_WE_DONT_HAVE\r\n\\A/secure/.*\\Z=ROLE_SUPERVISOR,ROLE_TELLER");
 
-        RegExpBasedFilterInvocationDefinitionMap map = (RegExpBasedFilterInvocationDefinitionMap) editor.getValue();
+        DefaultFilterInvocationDefinitionSource map = (DefaultFilterInvocationDefinitionSource) editor.getValue();
         Iterator iter = map.getConfigAttributeDefinitions();
         int counter = 0;
 
@@ -158,7 +159,7 @@ public class FilterInvocationDefinitionSourceEditorTests extends TestCase {
         FilterInvocationDefinitionSourceEditor editor = new FilterInvocationDefinitionSourceEditor();
         editor.setAsText("\\A/secure/super.*\\Z=ROLE_WE_DONT_HAVE,ANOTHER_ROLE");
 
-        RegExpBasedFilterInvocationDefinitionMap map = (RegExpBasedFilterInvocationDefinitionMap) editor.getValue();
+        DefaultFilterInvocationDefinitionSource map = (DefaultFilterInvocationDefinitionSource) editor.getValue();
 
         MockHttpServletRequest httpRequest = new MockHttpServletRequest(null, null);
         httpRequest.setServletPath("/totally/different/path/index.html");
@@ -173,7 +174,7 @@ public class FilterInvocationDefinitionSourceEditorTests extends TestCase {
         FilterInvocationDefinitionSourceEditor editor = new FilterInvocationDefinitionSourceEditor();
         editor.setAsText("\\A/secure/super.*\\Z=ROLE_WE_DONT_HAVE\r\n\\A/secure/.*\\Z=ROLE_SUPERVISOR,ROLE_TELLER");
 
-        RegExpBasedFilterInvocationDefinitionMap map = (RegExpBasedFilterInvocationDefinitionMap) editor.getValue();
+        DefaultFilterInvocationDefinitionSource map = (DefaultFilterInvocationDefinitionSource) editor.getValue();
         assertEquals(2, map.getMapSize());
     }
 
@@ -181,7 +182,7 @@ public class FilterInvocationDefinitionSourceEditorTests extends TestCase {
         FilterInvocationDefinitionSourceEditor editor = new FilterInvocationDefinitionSourceEditor();
         editor.setAsText(null);
 
-        RegExpBasedFilterInvocationDefinitionMap map = (RegExpBasedFilterInvocationDefinitionMap) editor.getValue();
+        DefaultFilterInvocationDefinitionSource map = (DefaultFilterInvocationDefinitionSource) editor.getValue();
         assertEquals(0, map.getMapSize());
     }
 
@@ -190,7 +191,7 @@ public class FilterInvocationDefinitionSourceEditorTests extends TestCase {
         editor.setAsText(
             "\\A/secure/super.*\\Z=ROLE_WE_DONT_HAVE,ANOTHER_ROLE\r\n\\A/secure/.*\\Z=ROLE_SUPERVISOR,ROLE_TELLER");
 
-        RegExpBasedFilterInvocationDefinitionMap map = (RegExpBasedFilterInvocationDefinitionMap) editor.getValue();
+        DefaultFilterInvocationDefinitionSource map = (DefaultFilterInvocationDefinitionSource) editor.getValue();
 
         // Test ensures we match the first entry, not the second
         MockHttpServletRequest httpRequest = new MockHttpServletRequest(null, null);
@@ -211,7 +212,7 @@ public class FilterInvocationDefinitionSourceEditorTests extends TestCase {
         editor.setAsText(
             "\\A/secure/.*\\Z=ROLE_SUPERVISOR,ROLE_TELLER\r\n\\A/secure/super.*\\Z=ROLE_WE_DONT_HAVE,ANOTHER_ROLE");
 
-        RegExpBasedFilterInvocationDefinitionMap map = (RegExpBasedFilterInvocationDefinitionMap) editor.getValue();
+        DefaultFilterInvocationDefinitionSource map = (DefaultFilterInvocationDefinitionSource) editor.getValue();
 
         MockHttpServletRequest httpRequest = new MockHttpServletRequest(null, null);
         httpRequest.setServletPath("/secure/super/very_secret.html");
@@ -230,7 +231,7 @@ public class FilterInvocationDefinitionSourceEditorTests extends TestCase {
         FilterInvocationDefinitionSourceEditor editor = new FilterInvocationDefinitionSourceEditor();
         editor.setAsText("\\A/secure/super.*\\Z=ROLE_WE_DONT_HAVE,ANOTHER_ROLE");
 
-        RegExpBasedFilterInvocationDefinitionMap map = (RegExpBasedFilterInvocationDefinitionMap) editor.getValue();
+        DefaultFilterInvocationDefinitionSource map = (DefaultFilterInvocationDefinitionSource) editor.getValue();
 
         MockHttpServletRequest httpRequest = new MockHttpServletRequest(null, null);
         httpRequest.setServletPath("/secure/super/very_secret.html");
@@ -249,7 +250,7 @@ public class FilterInvocationDefinitionSourceEditorTests extends TestCase {
         FilterInvocationDefinitionSourceEditor editor = new FilterInvocationDefinitionSourceEditor();
         editor.setAsText("PATTERN_TYPE_APACHE_ANT\r\n/secure/super/**=ROLE_WE_DONT_HAVE,ANOTHER_ROLE");
 
-        PathBasedFilterInvocationDefinitionMap map = (PathBasedFilterInvocationDefinitionMap) editor.getValue();
+        DefaultFilterInvocationDefinitionSource map = (DefaultFilterInvocationDefinitionSource) editor.getValue();
 
         MockHttpServletRequest httpRequest = new MockHttpServletRequest(null, null);
         httpRequest.setServletPath("/secure/super/very_secret.html");
@@ -269,7 +270,7 @@ public class FilterInvocationDefinitionSourceEditorTests extends TestCase {
         editor.setAsText(
             "         \\A/secure/super.*\\Z=ROLE_WE_DONT_HAVE,ANOTHER_ROLE      \r\n   \r\n     \r\n   // comment line  \r\n   \\A/testing.*\\Z=ROLE_TEST   \r\n");
 
-        RegExpBasedFilterInvocationDefinitionMap map = (RegExpBasedFilterInvocationDefinitionMap) editor.getValue();
+        DefaultFilterInvocationDefinitionSource map = (DefaultFilterInvocationDefinitionSource) editor.getValue();
         assertEquals(2, map.getMapSize());
     }
 }

+ 14 - 13
core/src/test/java/org/springframework/security/intercept/web/FilterInvocationDefinitionSourceEditorWithPathsTests.java

@@ -20,6 +20,7 @@ import junit.framework.TestCase;
 import org.springframework.security.ConfigAttributeDefinition;
 import org.springframework.security.MockFilterChain;
 import org.springframework.security.SecurityConfig;
+import org.springframework.security.util.AntUrlPathMatcher;
 
 import org.springframework.mock.web.MockHttpServletRequest;
 import org.springframework.mock.web.MockHttpServletResponse;
@@ -52,8 +53,8 @@ public class FilterInvocationDefinitionSourceEditorWithPathsTests extends TestCa
         editor.setAsText(
             "PATTERN_TYPE_APACHE_ANT\r\n/secure/super/*=ROLE_WE_DONT_HAVE\r\n/secure/*=ROLE_SUPERVISOR,ROLE_TELLER");
 
-        FilterInvocationDefinitionMap map = (FilterInvocationDefinitionMap) editor.getValue();
-        assertTrue(map instanceof PathBasedFilterInvocationDefinitionMap);
+        DefaultFilterInvocationDefinitionSource map = (DefaultFilterInvocationDefinitionSource) editor.getValue();
+        assertTrue(map.getUrlMatcher() instanceof AntUrlPathMatcher);
     }
 
     public void testConvertUrlToLowercaseDefaultSettingUnchangedByEditor() {
@@ -61,8 +62,8 @@ public class FilterInvocationDefinitionSourceEditorWithPathsTests extends TestCa
         editor.setAsText(
             "PATTERN_TYPE_APACHE_ANT\r\n/secure/super/*=ROLE_WE_DONT_HAVE\r\n/secure/*=ROLE_SUPERVISOR,ROLE_TELLER");
 
-        PathBasedFilterInvocationDefinitionMap map = (PathBasedFilterInvocationDefinitionMap) editor.getValue();
-        assertFalse(map.isConvertUrlToLowercaseBeforeComparison());
+        DefaultFilterInvocationDefinitionSource map = (DefaultFilterInvocationDefinitionSource) editor.getValue();
+        assertFalse(map.getUrlMatcher().requiresLowerCaseUrl());
     }
 
     public void testConvertUrlToLowercaseSettingApplied() {
@@ -70,8 +71,8 @@ public class FilterInvocationDefinitionSourceEditorWithPathsTests extends TestCa
         editor.setAsText(
             "CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON\r\nPATTERN_TYPE_APACHE_ANT\r\n/secure/super/*=ROLE_WE_DONT_HAVE\r\n/secure/*=ROLE_SUPERVISOR,ROLE_TELLER");
 
-        PathBasedFilterInvocationDefinitionMap map = (PathBasedFilterInvocationDefinitionMap) editor.getValue();
-        assertTrue(map.isConvertUrlToLowercaseBeforeComparison());
+        DefaultFilterInvocationDefinitionSource map = (DefaultFilterInvocationDefinitionSource) editor.getValue();
+        assertTrue(map.getUrlMatcher().requiresLowerCaseUrl());
     }
 
     public void testInvalidNameValueFailsToParse() {
@@ -89,7 +90,7 @@ public class FilterInvocationDefinitionSourceEditorWithPathsTests extends TestCa
         editor.setAsText(
             "PATTERN_TYPE_APACHE_ANT\r\n/secure/super/*=ROLE_WE_DONT_HAVE\r\n/secure/*=ROLE_SUPERVISOR,ROLE_TELLER");
 
-        PathBasedFilterInvocationDefinitionMap map = (PathBasedFilterInvocationDefinitionMap) editor.getValue();
+        DefaultFilterInvocationDefinitionSource map = (DefaultFilterInvocationDefinitionSource) editor.getValue();
         Iterator iter = map.getConfigAttributeDefinitions();
         int counter = 0;
 
@@ -105,7 +106,7 @@ public class FilterInvocationDefinitionSourceEditorWithPathsTests extends TestCa
         FilterInvocationDefinitionSourceEditor editor = new FilterInvocationDefinitionSourceEditor();
         editor.setAsText("PATTERN_TYPE_APACHE_ANT\r\n/secure/super/*=ROLE_WE_DONT_HAVE");
 
-        PathBasedFilterInvocationDefinitionMap map = (PathBasedFilterInvocationDefinitionMap) editor.getValue();
+        DefaultFilterInvocationDefinitionSource map = (DefaultFilterInvocationDefinitionSource) editor.getValue();
 
         MockHttpServletRequest httpRequest = new MockHttpServletRequest(null, null);
         httpRequest.setServletPath("/totally/different/path/index.html");
@@ -121,7 +122,7 @@ public class FilterInvocationDefinitionSourceEditorWithPathsTests extends TestCa
         editor.setAsText(
             "PATTERN_TYPE_APACHE_ANT\r\n/secure/super/*=ROLE_WE_DONT_HAVE\r\n/secure/*=ROLE_SUPERVISOR,ROLE_TELLER");
 
-        PathBasedFilterInvocationDefinitionMap map = (PathBasedFilterInvocationDefinitionMap) editor.getValue();
+        DefaultFilterInvocationDefinitionSource map = (DefaultFilterInvocationDefinitionSource) editor.getValue();
         assertEquals(2, map.getMapSize());
     }
 
@@ -130,7 +131,7 @@ public class FilterInvocationDefinitionSourceEditorWithPathsTests extends TestCa
         editor.setAsText(
             "PATTERN_TYPE_APACHE_ANT\r\n/secure/super/**=ROLE_WE_DONT_HAVE,ANOTHER_ROLE\r\n/secure/**=ROLE_SUPERVISOR,ROLE_TELLER");
 
-        PathBasedFilterInvocationDefinitionMap map = (PathBasedFilterInvocationDefinitionMap) editor.getValue();
+        DefaultFilterInvocationDefinitionSource map = (DefaultFilterInvocationDefinitionSource) editor.getValue();
 
         // Test ensures we match the first entry, not the second
         MockHttpServletRequest httpRequest = new MockHttpServletRequest(null, null);
@@ -151,7 +152,7 @@ public class FilterInvocationDefinitionSourceEditorWithPathsTests extends TestCa
         editor.setAsText(
             "PATTERN_TYPE_APACHE_ANT\r\n/secure/**=ROLE_SUPERVISOR,ROLE_TELLER\r\n/secure/super/**=ROLE_WE_DONT_HAVE");
 
-        PathBasedFilterInvocationDefinitionMap map = (PathBasedFilterInvocationDefinitionMap) editor.getValue();
+        DefaultFilterInvocationDefinitionSource map = (DefaultFilterInvocationDefinitionSource) editor.getValue();
 
         MockHttpServletRequest httpRequest = new MockHttpServletRequest(null, null);
         httpRequest.setServletPath("/secure/super/very_secret.html");
@@ -170,7 +171,7 @@ public class FilterInvocationDefinitionSourceEditorWithPathsTests extends TestCa
         FilterInvocationDefinitionSourceEditor editor = new FilterInvocationDefinitionSourceEditor();
         editor.setAsText("PATTERN_TYPE_APACHE_ANT\r\n/secure/super/*=ROLE_WE_DONT_HAVE,ANOTHER_ROLE");
 
-        PathBasedFilterInvocationDefinitionMap map = (PathBasedFilterInvocationDefinitionMap) editor.getValue();
+        DefaultFilterInvocationDefinitionSource map = (DefaultFilterInvocationDefinitionSource) editor.getValue();
 
         MockHttpServletRequest httpRequest = new MockHttpServletRequest(null, null);
         httpRequest.setServletPath("/secure/super/very_secret.html");
@@ -190,7 +191,7 @@ public class FilterInvocationDefinitionSourceEditorWithPathsTests extends TestCa
         editor.setAsText(
             "         PATTERN_TYPE_APACHE_ANT\r\n    /secure/super/*=ROLE_WE_DONT_HAVE\r\n    /secure/*=ROLE_SUPERVISOR,ROLE_TELLER      \r\n   \r\n     \r\n   // comment line  \r\n    \r\n");
 
-        PathBasedFilterInvocationDefinitionMap map = (PathBasedFilterInvocationDefinitionMap) editor.getValue();
+        DefaultFilterInvocationDefinitionSource map = (DefaultFilterInvocationDefinitionSource) editor.getValue();
         assertEquals(2, map.getMapSize());
     }
 }

+ 2 - 2
core/src/test/java/org/springframework/security/intercept/web/MockFilterInvocationDefinitionSource.java

@@ -30,7 +30,7 @@ import java.util.Vector;
  * @author Ben Alex
  * @version $Id$
  */
-public class MockFilterInvocationDefinitionSource extends AbstractFilterInvocationDefinitionSource {
+public class MockFilterInvocationDefinitionSource extends DefaultFilterInvocationDefinitionSource {
     //~ Instance fields ================================================================================================
 
     private List list;
@@ -77,7 +77,7 @@ public class MockFilterInvocationDefinitionSource extends AbstractFilterInvocati
         }
     }
 
-    public ConfigAttributeDefinition lookupAttributes(String url) {
+    public ConfigAttributeDefinition lookupAttributes(String url, String method) {
         throw new UnsupportedOperationException("mock method not implemented");
     }
 }

+ 80 - 28
core/src/test/java/org/springframework/security/intercept/web/PathBasedFilterDefinitionMapTests.java → core/src/test/java/org/springframework/security/intercept/web/PathBasedFilterInvocationDefinitionMapTests.java

@@ -15,8 +15,6 @@
 
 package org.springframework.security.intercept.web;
 
-import junit.framework.TestCase;
-
 import org.springframework.security.ConfigAttributeDefinition;
 import org.springframework.security.MockFilterChain;
 import org.springframework.security.SecurityConfig;
@@ -24,6 +22,9 @@ import org.springframework.security.SecurityConfig;
 import org.springframework.mock.web.MockHttpServletRequest;
 import org.springframework.mock.web.MockHttpServletResponse;
 
+import org.junit.Test;
+import static org.junit.Assert.*;
+import static org.junit.Assert.assertEquals;
 
 /**
  * Tests parts of {@link PathBasedFilterInvocationDefinitionMap} not tested by {@link
@@ -32,30 +33,25 @@ import org.springframework.mock.web.MockHttpServletResponse;
  * @author Ben Alex
  * @version $Id$
  */
-public class PathBasedFilterDefinitionMapTests extends TestCase {
-    //~ Constructors ===================================================================================================
-
-    public PathBasedFilterDefinitionMapTests() {
-    }
-
-    public PathBasedFilterDefinitionMapTests(String arg0) {
-        super(arg0);
-    }
+public class PathBasedFilterInvocationDefinitionMapTests {
 
     //~ Methods ========================================================================================================
 
-    public void testConvertUrlToLowercaseIsTrueByDefault() {
+    @Test
+    public void convertUrlToLowercaseIsTrueByDefault() {
         PathBasedFilterInvocationDefinitionMap map = new PathBasedFilterInvocationDefinitionMap();
         assertTrue(map.isConvertUrlToLowercaseBeforeComparison());
     }
 
-    public void testConvertUrlToLowercaseSetterRespected() {
+    @Test
+    public void convertUrlToLowercaseSetterRespected() {
         PathBasedFilterInvocationDefinitionMap map = new PathBasedFilterInvocationDefinitionMap();
         map.setConvertUrlToLowercaseBeforeComparison(false);
         assertFalse(map.isConvertUrlToLowercaseBeforeComparison());
     }
 
-    public void testLookupNotRequiringExactMatchSuccessIfNotMatching() {
+    @Test
+    public void lookupNotRequiringExactMatchSuccessIfNotMatching() {
         PathBasedFilterInvocationDefinitionMap map = new PathBasedFilterInvocationDefinitionMap();
         map.setConvertUrlToLowercaseBeforeComparison(true);
 
@@ -63,90 +59,146 @@ public class PathBasedFilterDefinitionMapTests extends TestCase {
         def.addConfigAttribute(new SecurityConfig("ROLE_ONE"));
         map.addSecureUrl("/secure/super/**", def);
 
-        FilterInvocation fi = createFilterinvocation("/SeCuRE/super/somefile.html");
+        FilterInvocation fi = createFilterInvocation("/SeCuRE/super/somefile.html", null);
 
         ConfigAttributeDefinition response = map.lookupAttributes(fi.getRequestUrl());
         assertEquals(def, response);
     }
 
     /**
-     * SEC-501. Not that as of 2.0, lower case comparisons are the default for this class.
+     * SEC-501. Note that as of 2.0, lower case comparisons are the default for this class.
      */
-    public void testLookupNotRequiringExactMatchSucceedsIfSecureUrlPathContainsUpperCase() {
+    @Test
+    public void lookupNotRequiringExactMatchSucceedsIfSecureUrlPathContainsUpperCase() {
         PathBasedFilterInvocationDefinitionMap map = new PathBasedFilterInvocationDefinitionMap();
 
         ConfigAttributeDefinition def = new ConfigAttributeDefinition();
         def.addConfigAttribute(new SecurityConfig("ROLE_ONE"));
         map.addSecureUrl("/SeCuRE/super/**", def);
 
-        FilterInvocation fi = createFilterinvocation("/secure/super/somefile.html");
+        FilterInvocation fi = createFilterInvocation("/secure/super/somefile.html", null);
 
         ConfigAttributeDefinition response = map.lookupAttributes(fi.getRequestUrl());
         assertEquals(def, response);
     }
 
 
-    public void testLookupRequiringExactMatchFailsIfNotMatching() {
+    @Test
+    public void lookupRequiringExactMatchFailsIfNotMatching() {
         PathBasedFilterInvocationDefinitionMap map = new PathBasedFilterInvocationDefinitionMap();
         map.setConvertUrlToLowercaseBeforeComparison(false);
         ConfigAttributeDefinition def = new ConfigAttributeDefinition();
         def.addConfigAttribute(new SecurityConfig("ROLE_ONE"));
         map.addSecureUrl("/secure/super/**", def);
 
-        FilterInvocation fi = createFilterinvocation("/SeCuRE/super/somefile.html");
+        FilterInvocation fi = createFilterInvocation("/SeCuRE/super/somefile.html", null);
 
         ConfigAttributeDefinition response = map.lookupAttributes(fi.getRequestUrl());
         assertEquals(null, response);
     }
 
-    public void testLookupRequiringExactMatchIsSuccessful() {
+    @Test
+    public void lookupRequiringExactMatchIsSuccessful() {
         PathBasedFilterInvocationDefinitionMap map = new PathBasedFilterInvocationDefinitionMap();
         map.setConvertUrlToLowercaseBeforeComparison(false);
         ConfigAttributeDefinition def = new ConfigAttributeDefinition();
         def.addConfigAttribute(new SecurityConfig("ROLE_ONE"));
         map.addSecureUrl("/SeCurE/super/**", def);
 
-        FilterInvocation fi = createFilterinvocation("/SeCurE/super/somefile.html");
+        FilterInvocation fi = createFilterInvocation("/SeCurE/super/somefile.html", null);
 
         ConfigAttributeDefinition response = map.lookupAttributes(fi.getRequestUrl());
         assertEquals(def, response);
     }
 
-    public void testLookupRequiringExactMatchWithAdditionalSlashesIsSuccessful() {
+    @Test
+    public void lookupRequiringExactMatchWithAdditionalSlashesIsSuccessful() {
         PathBasedFilterInvocationDefinitionMap map = new PathBasedFilterInvocationDefinitionMap();
         ConfigAttributeDefinition def = new ConfigAttributeDefinition();
         def.addConfigAttribute(new SecurityConfig("ROLE_ONE"));
         map.addSecureUrl("/someAdminPage.html**", def);
 
-        FilterInvocation fi = createFilterinvocation("/someAdminPage.html?a=/test");
+        FilterInvocation fi = createFilterInvocation("/someAdminPage.html?a=/test", null);
 
         ConfigAttributeDefinition response = map.lookupAttributes(fi.getRequestUrl());
         assertEquals(def, response); // see SEC-161 (it should truncate after ? sign)
     }
 
+    @Test(expected = IllegalArgumentException.class)
+    public void unknownHttpMethodIsRejected() {
+        PathBasedFilterInvocationDefinitionMap map = new PathBasedFilterInvocationDefinitionMap();
+        ConfigAttributeDefinition def = new ConfigAttributeDefinition();
+        def.addConfigAttribute(new SecurityConfig("ROLE_ONE"));
+        map.addSecureUrl("/someAdminPage.html**", "UNKNOWN", def);
+    }
+
+    @Test
+    public void httpMethodLookupSucceeds() {
+        PathBasedFilterInvocationDefinitionMap map = new PathBasedFilterInvocationDefinitionMap();
+        ConfigAttributeDefinition def = new ConfigAttributeDefinition();
+        def.addConfigAttribute(new SecurityConfig("ROLE_ONE"));
+        map.addSecureUrl("/somepage**", "GET", def);
+
+        FilterInvocation fi = createFilterInvocation("/somepage", "GET");
+        ConfigAttributeDefinition attrs = map.getAttributes(fi);
+        assertEquals(def, attrs);
+    }
+
+    @Test
+    public void requestWithDifferentHttpMethodDoesntMatch() {
+        PathBasedFilterInvocationDefinitionMap map = new PathBasedFilterInvocationDefinitionMap();
+        ConfigAttributeDefinition def = new ConfigAttributeDefinition();
+        def.addConfigAttribute(new SecurityConfig("ROLE_ONE"));
+        map.addSecureUrl("/somepage**", "GET", def);
+
+        FilterInvocation fi = createFilterInvocation("/somepage", null);
+        ConfigAttributeDefinition attrs = map.getAttributes(fi);
+        assertNull(attrs);
+    }
+
+    @Test
+    public void httpMethodSpecificUrlTakesPrecedence() {
+        PathBasedFilterInvocationDefinitionMap map = new PathBasedFilterInvocationDefinitionMap();
+
+        // Even though this is added before the method-specific def, the latter should match
+        ConfigAttributeDefinition allMethodDef = new ConfigAttributeDefinition();
+        allMethodDef.addConfigAttribute(new SecurityConfig("ROLE_ONE"));
+        map.addSecureUrl("/**", null, allMethodDef);
+
+        ConfigAttributeDefinition postOnlyDef = new ConfigAttributeDefinition();
+        postOnlyDef.addConfigAttribute(new SecurityConfig("ROLE_TWO"));
+        map.addSecureUrl("/somepage**", "POST", postOnlyDef);
+
+        FilterInvocation fi = createFilterInvocation("/somepage", "POST");
+        ConfigAttributeDefinition attrs = map.getAttributes(fi);
+        assertEquals(postOnlyDef, attrs);
+    }
+
     /**
      * Check fixes for SEC-321
      */
-    public void testExtraQuestionMarkStillMatches() {
+    @Test
+    public void extraQuestionMarkStillMatches() {
         PathBasedFilterInvocationDefinitionMap map = new PathBasedFilterInvocationDefinitionMap();
         ConfigAttributeDefinition def = new ConfigAttributeDefinition();
         def.addConfigAttribute(new SecurityConfig("ROLE_ONE"));
         map.addSecureUrl("/someAdminPage.html*", def);
 
-        FilterInvocation fi = createFilterinvocation("/someAdminPage.html?x=2/aa?y=3");
+        FilterInvocation fi = createFilterInvocation("/someAdminPage.html?x=2/aa?y=3", null);
 
         ConfigAttributeDefinition response = map.lookupAttributes(fi.getRequestUrl());
         assertEquals(def, response);
 
-        fi = createFilterinvocation("/someAdminPage.html??");
+        fi = createFilterInvocation("/someAdminPage.html??", null);
 
         response = map.lookupAttributes(fi.getRequestUrl());
         assertEquals(def, response);
     }
 
-    private FilterInvocation createFilterinvocation(String path) {
+    private FilterInvocation createFilterInvocation(String path, String method) {
         MockHttpServletRequest request = new MockHttpServletRequest();
         request.setRequestURI(null);
+        request.setMethod(method);
 
         request.setServletPath(path);