View Javadoc

1   /* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
2    *
3    * Licensed under the Apache License, Version 2.0 (the "License");
4    * you may not use this file except in compliance with the License.
5    * You may obtain a copy of the License at
6    *
7    *     http://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
14   */
15  
16  package org.springframework.security.intercept.web;
17  
18  import org.springframework.security.ConfigAttributeDefinition;
19  import org.springframework.security.util.UrlMatcher;
20  
21  import org.apache.commons.logging.Log;
22  import org.apache.commons.logging.LogFactory;
23  
24  import java.util.Map;
25  import java.util.LinkedHashMap;
26  import java.util.Iterator;
27  import java.util.HashMap;
28  import java.util.Set;
29  import java.util.HashSet;
30  import java.util.Arrays;
31  import java.util.Collection;
32  import java.util.Collections;
33  
34  
35  /**
36   * Default implementation of <tt>FilterInvocationDefinitionSource</tt>.
37   * <p>
38   * Stores an ordered map of compiled URL paths to <tt>ConfigAttributeDefinition</tt>s and provides URL matching
39   * against the items stored in this map using the configured <tt>UrlMatcher</tt>.
40   * <p>
41   * The order of registering the regular expressions using the
42   * {@link #addSecureUrl(String, ConfigAttributeDefinition)} is very important.
43   * The system will identify the <b>first</b>  matching regular
44   * expression for a given HTTP URL. It will not proceed to evaluate later regular expressions if a match has already
45   * been found. Accordingly, the most specific regular expressions should be registered first, with the most general
46   * regular expressions registered last.
47   * <p>
48   * If URLs are registered for a particular HTTP method using
49   * {@link #addSecureUrl(String, String, ConfigAttributeDefinition)}, then the method-specific matches will take
50   * precedence over any URLs which are registered without an HTTP method. 
51   *
52   * @author Ben Alex
53   * @author Luke Taylor
54   * @version $Id: DefaultFilterInvocationDefinitionSource.java 3045 2008-05-09 18:08:32Z luke_t $
55   */
56  public class DefaultFilterInvocationDefinitionSource implements FilterInvocationDefinitionSource {
57  
58      private static final Set HTTP_METHODS = new HashSet(Arrays.asList(new String[]{ "DELETE", "GET", "HEAD", "OPTIONS", "POST", "PUT", "TRACE" }));
59  
60      protected final Log logger = LogFactory.getLog(getClass());
61  
62      /**
63       * Non method-specific map of URL patterns to <tt>ConfigAttributeDefinition</tt>s
64       * TODO: Store in the httpMethod map with null key.
65       */
66      private Map requestMap = new LinkedHashMap();
67      /** Stores request maps keyed by specific HTTP methods */
68      private Map httpMethodMap = new HashMap();
69  
70      private UrlMatcher urlMatcher;
71  
72      private boolean stripQueryStringFromUrls;
73  
74      /**
75       * Creates a FilterInvocationDefinitionSource with the supplied URL matching strategy.
76       * @param urlMatcher
77       */
78      DefaultFilterInvocationDefinitionSource(UrlMatcher urlMatcher) {
79          this.urlMatcher = urlMatcher;
80      }
81      
82      /**
83       * Builds the internal request map from the supplied map. The key elements should be of type {@link RequestKey},
84       * which contains a URL path and an optional HTTP method (may be null). The path stored in the key will depend on 
85       * the type of the supplied UrlMatcher.
86       * 
87       * @param urlMatcher typically an ant or regular expression matcher.
88       * @param requestMap order-preserving map of <RequestKey, ConfigAttributeDefinition>.
89       */
90      public DefaultFilterInvocationDefinitionSource(UrlMatcher urlMatcher, LinkedHashMap requestMap) {
91          this.urlMatcher = urlMatcher;
92  
93          Iterator iterator = requestMap.entrySet().iterator();
94  
95          while (iterator.hasNext()) {
96              Map.Entry entry = (Map.Entry) iterator.next();
97              RequestKey reqKey = (RequestKey) entry.getKey();
98              addSecureUrl(reqKey.getUrl(), reqKey.getMethod(), (ConfigAttributeDefinition) entry.getValue());
99          }
100     }
101 
102     //~ Methods ========================================================================================================
103 
104     void addSecureUrl(String pattern, ConfigAttributeDefinition attr) {
105         addSecureUrl(pattern, null, attr);
106     }
107 
108     /**
109      * Adds a URL-ConfigAttributeDefinition pair to the request map, first allowing the <tt>UrlMatcher</tt> to
110      * process the pattern if required, using its <tt>compile</tt> method. The returned object will be used as the key
111      * to the request map and will be passed back to the <tt>UrlMatcher</tt> when iterating through the map to find
112      * a match for a particular URL.
113      */
114     void addSecureUrl(String pattern, String method, ConfigAttributeDefinition attr) {
115         Map mapToUse = getRequestMapForHttpMethod(method);
116 
117         mapToUse.put(urlMatcher.compile(pattern), attr);
118 
119         if (logger.isDebugEnabled()) {
120             logger.debug("Added URL pattern: " + pattern + "; attributes: " + attr +
121                     (method == null ? "" : " for HTTP method '" + method + "'"));
122         }
123     }
124 
125     /**
126      * Return the HTTP method specific request map, creating it if it doesn't already exist.
127      * @param method GET, POST etc
128      * @return map of URL patterns to <tt>ConfigAttributeDefinition</tt>s for this method.
129      */
130     private Map getRequestMapForHttpMethod(String method) {
131         if (method == null) {
132             return requestMap;
133         }
134         if (!HTTP_METHODS.contains(method)) {
135             throw new IllegalArgumentException("Unrecognised HTTP method: '" + method + "'");
136         }
137 
138         Map methodRequestmap = (Map) httpMethodMap.get(method);
139 
140         if (methodRequestmap == null) {
141             methodRequestmap = new LinkedHashMap();
142             httpMethodMap.put(method, methodRequestmap);
143         }
144 
145         return methodRequestmap;
146     }
147 
148     public Collection getConfigAttributeDefinitions() {
149         return Collections.unmodifiableCollection(getRequestMap().values());
150     }
151 
152     public ConfigAttributeDefinition getAttributes(Object object) throws IllegalArgumentException {
153         if ((object == null) || !this.supports(object.getClass())) {
154             throw new IllegalArgumentException("Object must be a FilterInvocation");
155         }
156 
157         String url = ((FilterInvocation) object).getRequestUrl();
158         String method = ((FilterInvocation) object).getHttpRequest().getMethod();
159 
160         return lookupAttributes(url, method);
161     }
162 
163     protected ConfigAttributeDefinition lookupAttributes(String url) {
164         return lookupAttributes(url, null);
165     }
166 
167     /**
168      * Performs the actual lookup of the relevant <code>ConfigAttributeDefinition</code> for the specified
169      * <code>FilterInvocation</code>.
170      * <p>
171      * By default, iterates through the stored URL map and calls the
172      * {@link UrlMatcher#pathMatchesUrl(Object path, String url)} method until a match is found.
173      * <p>
174      * Subclasses can override if required to perform any modifications to the URL.
175      *
176      * @param url the URI to retrieve configuration attributes for
177      * @param method the HTTP method (GET, POST, DELETE...).
178      *
179      * @return the <code>ConfigAttributeDefinition</code> that applies to the specified <code>FilterInvocation</code>
180      * or null if no match is foud
181      */
182     public ConfigAttributeDefinition lookupAttributes(String url, String method) {
183         if (stripQueryStringFromUrls) {
184             // Strip anything after a question mark symbol, as per SEC-161. See also SEC-321
185             int firstQuestionMarkIndex = url.indexOf("?");
186 
187             if (firstQuestionMarkIndex != -1) {
188                 url = url.substring(0, firstQuestionMarkIndex);
189             }            
190         }
191 
192         if (urlMatcher.requiresLowerCaseUrl()) {
193             url = url.toLowerCase();
194 
195             if (logger.isDebugEnabled()) {
196                 logger.debug("Converted URL to lowercase, from: '" + url + "'; to: '" + url + "'");
197             }
198         }
199 
200         ConfigAttributeDefinition attributes = null;
201 
202         Map methodSpecificMap = (Map) httpMethodMap.get(method);
203 
204         if (methodSpecificMap != null) {
205             attributes = lookupUrlInMap(methodSpecificMap, url);
206         }
207 
208         if (attributes == null) {
209             attributes = lookupUrlInMap(requestMap, url);
210         }
211 
212         return attributes;
213     }
214 
215     private ConfigAttributeDefinition lookupUrlInMap(Map requestMap, String url) {
216         Iterator entries = requestMap.entrySet().iterator();
217 
218         while (entries.hasNext()) {
219             Map.Entry entry = (Map.Entry) entries.next();
220             Object p = entry.getKey();
221             boolean matched = urlMatcher.pathMatchesUrl(p, url);
222 
223             if (logger.isDebugEnabled()) {
224                 logger.debug("Candidate is: '" + url + "'; pattern is " + p + "; matched=" + matched);
225             }
226 
227             if (matched) {
228                 return (ConfigAttributeDefinition) entry.getValue();
229             }
230         }
231 
232         return null;
233     }
234 
235     public boolean supports(Class clazz) {
236         return FilterInvocation.class.isAssignableFrom(clazz);
237     }
238 
239     public int getMapSize() {
240         return this.requestMap.size();
241     }
242 
243     Map getRequestMap() {
244         return requestMap;
245     }
246 
247     protected UrlMatcher getUrlMatcher() {
248         return urlMatcher;
249     }
250 
251     public boolean isConvertUrlToLowercaseBeforeComparison() {
252         return urlMatcher.requiresLowerCaseUrl();
253     }
254 
255     public void setStripQueryStringFromUrls(boolean stripQueryStringFromUrls) {
256         this.stripQueryStringFromUrls = stripQueryStringFromUrls;
257     }
258 }