Source code: org/securityfilter/filter/SecurityFilter.java
1 /*
2 * $Header: /cvsroot/securityfilter/securityfilter/src/share/org/securityfilter/filter/SecurityFilter.java,v 1.21 2003/07/07 13:12:57 maxcooper Exp $
3 * $Revision: 1.21 $
4 * $Date: 2003/07/07 13:12:57 $
5 *
6 * ====================================================================
7 * The SecurityFilter Software License, Version 1.1
8 *
9 * (this license is derived and fully compatible with the Apache Software
10 * License - see http://www.apache.org/LICENSE.txt)
11 *
12 * Copyright (c) 2002 SecurityFilter.org. All rights reserved.
13 *
14 * Redistribution and use in source and binary forms, with or without
15 * modification, are permitted provided that the following conditions
16 * are met:
17 *
18 * 1. Redistributions of source code must retain the above copyright
19 * notice, this list of conditions and the following disclaimer.
20 *
21 * 2. Redistributions in binary form must reproduce the above copyright
22 * notice, this list of conditions and the following disclaimer in
23 * the documentation and/or other materials provided with the
24 * distribution.
25 *
26 * 3. The end-user documentation included with the redistribution,
27 * if any, must include the following acknowledgment:
28 * "This product includes software developed by
29 * SecurityFilter.org (http://www.securityfilter.org/)."
30 * Alternately, this acknowledgment may appear in the software itself,
31 * if and wherever such third-party acknowledgments normally appear.
32 *
33 * 4. The name "SecurityFilter" must not be used to endorse or promote
34 * products derived from this software without prior written permission.
35 * For written permission, please contact license@securityfilter.org .
36 *
37 * 5. Products derived from this software may not be called "SecurityFilter",
38 * nor may "SecurityFilter" appear in their name, without prior written
39 * permission of SecurityFilter.org.
40 *
41 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
42 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
43 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
44 * DISCLAIMED. IN NO EVENT SHALL THE SECURITY FILTER PROJECT OR
45 * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
46 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
47 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
48 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
49 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
50 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
51 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
52 * SUCH DAMAGE.
53 * ====================================================================
54 */
55
56 package org.securityfilter.filter;
57
58 import org.securityfilter.authenticator.*;
59 import org.securityfilter.config.*;
60 import org.securityfilter.realm.SecurityRealmInterface;
61
62 import javax.servlet.*;
63 import javax.servlet.http.*;
64 import java.io.IOException;
65 import java.net.URL;
66 import java.security.Principal;
67 import java.util.*;
68
69 /**
70 * SecurityFilter provides authentication and authorization services.
71 *
72 * @author Max Cooper (max@maxcooper.com)
73 * @author Daya Sharma (iamdaya@yahoo.com, billydaya@sbcglobal.net)
74 * @author Torgeir Veimo (torgeir@pobox.com)
75 * @version $Revision: 1.21 $ $Date: 2003/07/07 13:12:57 $
76 */
77 public class SecurityFilter implements Filter {
78 public static final String CONFIG_FILE_KEY = "config";
79 public static final String DEFAULT_CONFIG_FILE = "/WEB-INF/securityfilter-config.xml";
80 public static final String VALIDATE_KEY = "validate";
81
82 public static final String TRUE = "true";
83
84 public static final String ALREADY_PROCESSED = SecurityFilter.class.getName() + ".ALREADY_PROCESSED";
85
86 public static final String SAVED_REQUEST_URL = SecurityFilter.class.getName() + ".SAVED_REQUEST_URL";
87 public static final String SAVED_REQUEST = SecurityFilter.class.getName() + ".SAVED_REQUEST";
88
89 protected FilterConfig config;
90 protected SecurityRealmInterface realm;
91 protected List patternList;
92 protected URLPatternFactory patternFactory;
93 protected Authenticator authenticator;
94
95 /**
96 * Perform filtering operation, and optionally pass the request down the chain.
97 *
98 * @param request the current request
99 * @param response the current response
100 * @param chain request handler chain
101 * @exception IOException
102 * @exception ServletException
103 */
104 public void doFilter(
105 ServletRequest request,
106 ServletResponse response,
107 FilterChain chain
108 ) throws IOException, ServletException {
109
110 HttpServletRequest hReq = (HttpServletRequest) request;
111 HttpServletResponse hRes = (HttpServletResponse) response;
112 SecurityRequestWrapper wrappedRequest;
113
114 // if the request has already been processed by the filter, pass it through unchecked
115 if (!TRUE.equals(request.getAttribute(ALREADY_PROCESSED))) {
116 // set an attribute on this request to indicate that it has already been processed
117 request.setAttribute(ALREADY_PROCESSED, TRUE);
118
119 // get a URLPatternMatcher to use for this thread
120 URLPatternMatcher patternMatcher = patternFactory.createURLPatternMatcher();
121
122 // get saved request, if any (returns null if not applicable)
123 SavedRequest savedRequest = getSavedRequest(hReq);
124
125 // wrap request
126 wrappedRequest = new SecurityRequestWrapper(hReq, savedRequest, realm, authenticator.getAuthMethod());
127
128 URLPattern match = null;
129 try {
130 // check if this request includes login info
131 if (authenticator.processLogin(wrappedRequest, hRes)) {
132 return;
133 }
134
135 // match the url if the authenticator does not indicate that security should be bypassed
136 if (!authenticator.bypassSecurityForThisRequest(wrappedRequest, patternMatcher)) {
137 // check if request matches security constraint
138 match = matchPattern(wrappedRequest.getMatchableURL(), wrappedRequest.getMethod(), patternMatcher);
139 }
140 } catch (Exception e) {
141 throw new ServletException("Error matching patterns", e);
142 }
143
144 // check security constraint, if any
145 if (match != null) {
146 // TODO: check user-data-constraint
147 // check auth constraint
148 AuthConstraint authConstraint = match.getSecurityConstraint().getAuthConstraint();
149 if (authConstraint != null) {
150 Collection roles = authConstraint.getRoles();
151 Principal principal = wrappedRequest.getUserPrincipal();
152 // if roles is empty, access will be blocked no matter who the user is, so skip the login
153 // todo: do we still need this DUMMY_TOKEN check for BASIC auth?
154 if (!roles.isEmpty() && principal == null /* && hReq.getSession().getAttribute(DUMMY_TOKEN) == null */) {
155 // user needs to be authenticated
156 authenticator.showLogin(hReq, hRes);
157 return;
158 } else {
159 boolean authorized = false;
160 for (Iterator i = roles.iterator(); i.hasNext() && !authorized;) {
161 String role = (String) i.next();
162 // TODO: if *, do you need to have at least one role to be authorized?
163 // if so, we need to iterate through the roles defined in config file or change the
164 // realm inteface to get a list of roles for the user (both solutions are undesireable)
165 if ("*".equals(role) || realm.isUserInRole(principal, role)) {
166 authorized = true;
167 }
168 }
169 if (!authorized) {
170 // user does not meet role constraint
171 hRes.sendError(HttpServletResponse.SC_FORBIDDEN);
172 return;
173 }
174 }
175 }
176 }
177 // send wrapped request down the chain
178 request = wrappedRequest;
179 }
180
181 // pass the request down the filter chain
182 chain.doFilter(request, response);
183 }
184
185 /**
186 * Initialize the SecurityFilter.
187 *
188 * @param config filter configuration object
189 */
190 public void init(FilterConfig config) throws ServletException {
191 this.config = config;
192 try {
193 // parse config file
194
195 // config file name
196 String configFile = config.getInitParameter(CONFIG_FILE_KEY);
197 if (configFile == null) {
198 configFile = DEFAULT_CONFIG_FILE;
199 }
200 URL configURL = config.getServletContext().getResource(configFile);
201
202 // validate config file?
203 boolean validate = TRUE.equalsIgnoreCase(config.getInitParameter(VALIDATE_KEY));
204
205 SecurityConfig securityConfig = new SecurityConfig(validate);
206 securityConfig.loadConfig(configURL);
207
208 // get the realm
209 realm = securityConfig.getRealm();
210
211 // create an Authenticator
212 authenticator = AuthenticatorFactory.createAuthenticator(config, securityConfig);
213
214 // create pattern list
215 patternFactory = new URLPatternFactory();
216 patternList = new ArrayList();
217 int order = 1;
218 List constraints = securityConfig.getSecurityConstraints();
219 for (Iterator cIter = constraints.iterator(); cIter.hasNext();) {
220 SecurityConstraint constraint = (SecurityConstraint) cIter.next();
221 for (Iterator rIter = constraint.getWebResourceCollections().iterator(); rIter.hasNext();) {
222 WebResourceCollection resourceCollection = (WebResourceCollection) rIter.next();
223 for (Iterator pIter = resourceCollection.getURLPatterns().iterator(); pIter.hasNext();) {
224 URLPattern pattern = patternFactory.createURLPattern(
225 (String) pIter.next(),
226 constraint,
227 resourceCollection,
228 order++
229 );
230 patternList.add(pattern);
231 }
232 }
233 }
234 Collections.sort(patternList);
235
236 } catch (java.io.IOException ioe) {
237 System.err.println("unable to parse input: " + ioe);
238 } catch (org.xml.sax.SAXException se) {
239 System.err.println("unable to parse input: " + se);
240 } catch (Exception e) {
241 System.err.println("invalid regular expression pattern: " + e);
242 }
243 }
244
245 /**
246 * Destroy the filter, releasing resources.
247 */
248 public void destroy() {
249 }
250
251 /**
252 * Find a match for the requested pattern & method, if any.
253 *
254 * @param pattern the pattern to match
255 * @param httpMethod the HTTP Method to match
256 * @param matcher the thread-local URLPatternMatcher object
257 * @return the matching URLPattern object, or null if there is no match.
258 */
259 protected URLPattern matchPattern(String pattern, String httpMethod, URLPatternMatcher matcher) throws Exception {
260 // PERFORMANCE IMPROVEMENT OPPORTUNITY: cahce pattern matches
261 Iterator i = patternList.iterator();
262 while (i.hasNext()) {
263 URLPattern urlPattern = (URLPattern) i.next();
264 if (matcher.match(pattern, httpMethod, urlPattern)) {
265 return urlPattern;
266 }
267 }
268 return null;
269 }
270
271 /**
272 * If this request matches the one we saved, return the SavedRequest and remove it from the session.
273 *
274 * @param request the current request
275 * @return usually null, but when the request matches the posted URL that initiated the login sequence a
276 * SavedRequest object is returned.
277 */
278 protected SavedRequest getSavedRequest(HttpServletRequest request) {
279 HttpSession session = request.getSession();
280 String savedURL = (String) session.getAttribute(SecurityFilter.SAVED_REQUEST_URL);
281 if (savedURL != null && savedURL.equals(getSaveableURL(request))) {
282 // this is a request for the request that caused the login,
283 // get the SavedRequest from the session
284 SavedRequest saved = (SavedRequest) session.getAttribute(SecurityFilter.SAVED_REQUEST);
285 // remove the saved request info from the session
286 session.removeAttribute(SecurityFilter.SAVED_REQUEST_URL);
287 session.removeAttribute(SecurityFilter.SAVED_REQUEST);
288 // and return the SavedRequest
289 return saved;
290 } else {
291 return null;
292 }
293 }
294
295 /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
296 // The following methods are provided as static utilities for use by SecurityFilter and other classes. //
297 /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
298
299 /**
300 * Get the URL to continue to after successful login. This may be the SAVED_REQUEST_URL if the authorization
301 * sequence was initiated by the filter, or the default URL (as specified in the config file) if a login
302 * request was spontaneously submitted.
303 *
304 * @param request the current request
305 */
306 public static String getContinueToURL(HttpServletRequest request) {
307 return (String) request.getSession().getAttribute(SAVED_REQUEST_URL);
308 }
309
310 /**
311 * Save request information to re-use when the user is successfully authenticated.
312 *
313 * @param request the current request
314 */
315 public static void saveRequestInformation(HttpServletRequest request) {
316 HttpSession session = request.getSession();
317 session.setAttribute(SecurityFilter.SAVED_REQUEST_URL, getSaveableURL(request));
318 session.setAttribute(SecurityFilter.SAVED_REQUEST, new SavedRequest(request));
319 }
320
321 /**
322 * Return a URL suitable for saving or matching against a saved URL.<p>
323 *
324 * This is the whole URL, plus the query string.
325 *
326 * @param request the request to construct a saveable URL for
327 */
328 private static String getSaveableURL(HttpServletRequest request) {
329 StringBuffer saveableURL = null;
330 try {
331 saveableURL = request.getRequestURL();
332 } catch (NoSuchMethodError e) {
333 saveableURL = getRequestURL(request);
334 }
335 // fix the protocol
336 fixProtocol(saveableURL, request);
337 // add the query string, if any
338 String queryString = request.getQueryString();
339 if (queryString != null) {
340 saveableURL.append("?" + queryString);
341 }
342 return saveableURL.toString();
343 }
344
345 /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
346 // The following methods are provided for compatibility with various app servers. //
347 /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
348
349 /**
350 * Set the filter configuration, included for WebLogic 6 compatibility.
351 *
352 * @param config filter configuration object
353 */
354 public void setFilterConfig(FilterConfig config) throws ServletException {
355 init(config);
356 }
357
358 /**
359 * Get the filter config object, included for WebLogic 6 compatibility.
360 */
361 public FilterConfig getFilterConfig() {
362 return config;
363 }
364
365 /**
366 * Get the requestURL.
367 * This method is called when the app server fails to implement HttpServletRequest.getRequestURL().
368 * Orion 1.5.2 is one such server.
369 */
370 private static StringBuffer getRequestURL(HttpServletRequest request) {
371 String protocol = request.getProtocol();
372 int port = request.getServerPort();
373 String portString = ":" + port;
374
375 // todo: this needs to be tested to see if it still an issue; remove it if it is not needed
376 // Set the portString to the empty string if the requrest came in on the default port.
377 // This will keep Netscape from dropping the session, which happens when the port is added where it wasn't before.
378 // This is not perfect, but most requests on the default ports will not be made with an explicit port number.
379 if (protocol.equals("HTTP/1.1")) {
380 if (!request.isSecure()) {
381 if (port == 80) {
382 portString = "";
383 }
384 } else {
385 if (port == 443) {
386 portString = "";
387 }
388 }
389 }
390
391 // construct the saveable URL string
392 return new StringBuffer(protocol + request.getServerName() + portString + request.getRequestURI());
393 }
394
395 /**
396 * Fix the protocol portion of an absolute url. Often, the protocol will be http: even for https: requests.
397 *
398 * todo: needs testing to make sure this is proper in all circumstances
399 *
400 * @param url
401 * @param request
402 */
403 private static void fixProtocol(StringBuffer url, HttpServletRequest request) {
404 // fix protocol, if needed (since HTTP is the same regardless of whether it runs on TCP or on SSL/TCP)
405 if (
406 request.getProtocol().equals("HTTP/1.1")
407 && request.isSecure()
408 && url.toString().startsWith("http://")
409 ) {
410 url.replace(0, 4, "https");
411 }
412 }
413 }
414
415 // ------------------------------------------------------------------------
416 // EOF