View Javadoc

1   /*--
2    Copyright (C) 2005 Tim Solley.
3    All rights reserved.
4   
5    Redistribution and use in source and binary forms, with or without
6    modification, are permitted provided that the following conditions
7    are met:
8   
9    1. Redistributions of source code must retain the above copyright
10   notice, this list of conditions, and the following disclaimer.
11  
12   2. Redistributions in binary form must reproduce the above copyright
13   notice, this list of conditions, and the disclaimer that follows
14   these conditions in the documentation and/or other materials
15   provided with the distribution.
16  
17   3. The name "Deadbolt" may be used to endorse or promote products
18   derived from this software without prior written permission.
19  
20   4. Products derived from this software may not be called "Deadbolt", nor
21   may "Deadbolt" appear in their name, without prior written permission
22   from the Deadbolt Project Management timsolley@yahoo.com.
23  
24   In addition, we request (but do not require) that you include in the
25   end-user documentation provided with the redistribution and/or in the
26   software itself an acknowledgement equivalent to the following:
27   "This product includes software developed by the
28   Deadbolt Project (http://deadbolt.sourceforge.net/)."
29   Alternatively, the acknowledgment may be graphical using the logos
30   available at http://deadbolt.sourceforge.net.
31  
32   THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
33   WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
34   OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
35   DISCLAIMED.  IN NO EVENT SHALL THE DEADBOLT AUTHORS OR THE PROJECT
36   CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
37   SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
38   LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
39   USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
40   ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
41   OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
42   OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
43   SUCH DAMAGE.
44  
45   This software consists of voluntary contributions made by many
46   individuals on behalf of the Deadbolt Project and was originally
47   created by Tim Solley timsolley@yahoo.com.  For more information
48   on the Deadbolt Project, please see <http://deadbolt.sourceforge.net/>.
49   */
50  
51  package net.sf.deadbolt;
52  
53  import java.io.File;
54  import java.io.IOException;
55  import java.util.*;
56  
57  import javax.servlet.*;
58  import javax.servlet.http.HttpServletRequest;
59  import javax.servlet.http.HttpServletResponse;
60  
61  import net.sf.deadbolt.handlers.DeadboltHandler;
62  import net.sf.deadbolt.model.Room;
63  import net.sf.deadbolt.model.URLMapping;
64  import net.sf.deadbolt.tags.DisplayErrorsTag;
65  
66  import org.apache.commons.lang.StringUtils;
67  import org.apache.log4j.Logger;
68  import org.jdom.Document;
69  import org.jdom.Element;
70  import org.jdom.JDOMException;
71  import org.jdom.input.SAXBuilder;
72  
73  
74  /***
75   * This class is the meat of the Deadbolt framework. This filter is executed on
76   * every request for an application, and it determines the authentication and
77   * authorization setup for the requested resource.
78   * 
79   * @author Tim Solley <timsolley@yahoo.com>
80   */
81  public class DeadboltFilter implements Filter {
82      /* Jakarta Commons Logging is used here */
83      private static Logger logger = Logger.getLogger(DeadboltFilter.class.getName());
84  
85      /* Added to the object in case a handler needs access to the ServletContext */
86      private static ServletContext context;
87  
88      /*
89       * This is a list of mappings from URL pattern to a room. This indicates the
90       * room to be used when authenticating a resource.
91       */
92      private static List urlMappings;
93  
94      /*
95       * The list of error messages that will be shown to the user if any are
96       * added.
97       */
98      private static Map errorMessages;
99      
100     /*
101      * This is the Room to contain global information.
102      */
103     private static Room globalRoom;
104 
105     /***
106      * This method will return the <code>Map</code> of error messages, which
107      * are <code>String</code> objects. This is used by the
108      * {@link DeadboltHandler}to put messages in the request for later display
109      * by the {@link DisplayErrorsTag}custom tag on the JSP page.
110      * 
111      * @return Returns the errorMessages.
112      */
113     public static Map getErrorMessages() {
114         return errorMessages;
115     }
116 
117     /***
118      * This method is the starting point the the Deadbolt enabled application.
119      * It processes the configuration XML file and stores the configuration
120      * information in memory when the application starts up. That information
121      * will be later used by the <code>doFilter</code> method to secure the
122      * requested resource.
123      */
124     public void init(FilterConfig config) throws ServletException {
125         logger.debug("ENTERING: init");
126 
127         context = config.getServletContext();
128         /*
129          * Get the config file: The config file is specified in the web.xml file
130          * under an init-param named "config-file" for the filter. We simply use
131          * the application context to the get the path within the application.
132          * If the user wants to put the file outside the application, this part
133          * will not work. This is a good area for someone to make that more
134          * flexible, allowing a user to put the file anywhere, taking the
135          * configuration information out of the application and allowing it to
136          * be modified without having to create another build.
137          */
138         File configFile = new File(context.getRealPath(config
139                 .getInitParameter("config-file")));
140 
141         /*
142          * Read the XML and do setup work: JDOM is used to process the config
143          * file. This section takes the XML file in pieces, processing each
144          * block sequentially.
145          */
146         SAXBuilder builder = new SAXBuilder();
147         builder.setValidation(true);
148         builder.setFeature("http://apache.org/xml/features/validation/schema", true);
149         urlMappings = new ArrayList();
150 
151         try {
152             Document document = builder.build(configFile);
153             Element deadboltConfigElement = document.getRootElement();
154 
155             // Process the error messages, starting with the global error page
156             String globalErrorPage = deadboltConfigElement
157                     .getChildText("global-error-page");
158 
159             /*
160              * Process the handlers: Handlers can be reused within the config
161              * file. So, to avoid redundency, the sets up the unique list of
162              * them. They then just reference the handler name in the room. Here
163              * we just process that list, and store them in memory so that they
164              * can be accessed when processing rooms.
165              */
166             List handlerList = deadboltConfigElement.getChildren("handler");
167             Map handlers = new Hashtable();
168             for (Iterator it = handlerList.iterator(); it.hasNext();) {
169                 Element handlerElement = (Element) it.next();
170                 handlers.put(handlerElement.getChildText("handler-name"),
171                         handlerElement.getChildText("handler-class"));
172             }
173             
174             /*
175              * Process the global handlers.  These will be run on every request
176              * that Deadbolt responds to, before control is passed off to the
177              * Rooms for processing.
178              */
179             List globalHandlersList = deadboltConfigElement.getChildren("global-handler");
180             if(globalHandlersList.size() != 0) {
181                 globalRoom = new Room();
182                 TreeMap globalHandlerMap = new TreeMap();
183                 Map globalInitParams = new Hashtable();
184                 for(Iterator iterator = globalHandlersList.iterator(); iterator.hasNext();) {
185                     Element handler = (Element) iterator.next();
186                     String handlerClass = (String) handlers.get(handler.getChildText("handler-name"));
187                     globalHandlerMap.put(handler.getChildText("execution-order"), handlerClass);
188                     logger.debug("Global handler: " + handler.getChildText("execution-order") + " | " +
189                             handlerClass);
190                     
191                     /*
192                      * Set up the initial parameters for this global handler
193                      */
194                     List initParamsElements = handler.getChildren("init-param");
195                     for (Iterator i = initParamsElements.iterator(); i.hasNext();) {
196                         Element initParam = (Element) i.next();
197                         // Add the current param to the list
198                         globalInitParams.put(initParam.getChildText("param-name"),
199                                 initParam.getChildText("param-value"));
200                         logger.debug("Param name: " + initParam.getChildText("param-name"));
201                         logger.debug("Param value: " + initParam.getChildText("param-value"));
202                     }
203                 }
204                 globalRoom.setInitParams(globalInitParams);
205                 globalRoom.setHandler(globalHandlerMap);
206             }
207             
208 
209             /*
210              * Process the rooms.
211              */
212             List roomList = deadboltConfigElement.getChildren("room");
213             for (Iterator it = roomList.iterator(); it.hasNext();) {
214                 // Set up a new room
215                 Room room = new Room();
216                 Map initParams = new Hashtable();
217                 TreeMap handlerMap = new TreeMap();
218                 Element roomElement = (Element) it.next();
219 
220                 /*
221                  * Set up the handler chain for this room: The user can specify
222                  * multiple handlers for a room. When they do this, they will be
223                  * executed sequentially when a request for the specified
224                  * resource comes in. The user specifies the execution order
225                  * using the "execution-order" element. This section reads in
226                  * the handler, and adds it to a TreeMap, which will
227                  * automatically put it in the correct order based on the
228                  * "execution-order".
229                  */
230                 List handlerListElement = roomElement.getChildren("handler");
231                 for (Iterator iter = handlerListElement.iterator(); iter
232                         .hasNext();) {
233                     Element handler = (Element) iter.next();
234                     String handlerClass = (String) handlers.get(handler
235                             .getChildText("handler-name"));
236                     // Add the handler to the TreeMap, going into the correct
237                     // order
238                     handlerMap.put(handler.getChildText("execution-order"),
239                             handlerClass);
240                     logger.debug("Handler: "
241                             + handler.getChildText("execution-order") + " | "
242                             + handlerClass);
243                 }
244                 // Add the handler chain to the room
245                 room.setHandler(handlerMap);
246 
247                 /*
248                  * Set up the init parameters for this room: Init params can be
249                  * defined by the user. They can have as many as they want.
250                  * These will be available to the custom handler by way of the
251                  * room.
252                  */
253                 List initParamsElements = roomElement.getChildren("init-param");
254                 for (Iterator i = initParamsElements.iterator(); i.hasNext();) {
255                     Element initParam = (Element) i.next();
256                     // Add the current param to the list
257                     initParams.put(initParam.getChildText("param-name"),
258                             initParam.getChildText("param-value"));
259                     logger.debug("Param name: " + initParam.getChildText("param-name"));
260                     logger.debug("Param value: " + initParam.getChildText("param-value"));
261                 }
262                 // Add the init params to the room
263                 room.setInitParams(initParams);
264 
265                 /*
266                  * Set up the error page for this room: If no error page is
267                  * specified, then the global error page will be used.
268                  */
269                 String errorPage = roomElement.getChildText("error-page");
270                 if (errorPage == null) {
271                     room.setErrorPage(globalErrorPage);
272                 } else {
273                     room.setErrorPage(errorPage);
274                 }
275 
276                 /*
277                  * Set up the URL mappings for this room: This will be the
278                  * mechanism that matches a URL pattern to a room.
279                  */
280                 List urls = roomElement.getChildren("url-pattern");
281                 for (Iterator i = urls.iterator(); i.hasNext();) {
282                     URLMapping mapping = new URLMapping();
283                     mapping.setRoom(room);
284                     // Get the URL pattern from the config file
285                     String pattern = ((Element) i.next()).getText();
286                     /*
287                      * The requested pattern will be tested against this pattern
288                      * later on in the doFilter method using a regular
289                      * expression. To do this, we need to ensure that any
290                      * asterisks entered by the user are translated to a regular
291                      * express character. So we replace stars with ".++", for
292                      * zero to infinity of any character.
293                      */
294                     pattern = StringUtils.replace(pattern, "*", ".++");
295                     mapping.setUrlPattern(pattern);
296                     // Add this mapping to the list of mappings
297                     urlMappings.add(mapping);
298                 }
299             }
300 
301             /*
302              * Set up the list of error message keys: These keys are used by the
303              * user in the handlers to send error messages to the client using
304              * consistent verbiage. This also enables the user to make spelling,
305              * grammatical, or wording changes in one place, outside code,
306              * without having to recompile the code.
307              */
308             errorMessages = new Hashtable();
309             Element errorMessagesElement = deadboltConfigElement
310                     .getChild("error-messages");
311             List errorMessageList = errorMessagesElement
312                     .getChildren("error-message");
313             for (Iterator it = errorMessageList.iterator(); it.hasNext();) {
314                 Element errorMessage = (Element) it.next();
315                 errorMessages.put(errorMessage.getChildText("message-key"),
316                         errorMessage.getChildText("message-content"));
317             }
318         } catch (IOException ioe) {
319             logger.warn("An IOException occurred during init: "
320                             + ioe.getMessage());
321         } catch (JDOMException jde) {
322             logger.warn("A JDOM Exception occurred during init: " + jde.getMessage());
323         }
324         logger.debug("EXITING: init");
325     }
326 
327     /***
328      * This method will be called on each request to the application, provided
329      * the user used <code>/*</code> as the url-pattern element in the web.xml
330      * file. This method will examine the request, compare it's URL to a list of
331      * URL patterns defined in the config file, and execute handlers based on
332      * any room found that matches the URL pattern. It will then let the user
333      * pass to the requested resource, or send the user to the specified error
334      * page if not allowed. If any handler sends back a false, the user will not
335      * be permitted to enter.
336      * 
337      * It should be noted that if a resource exists, and it's URL pattern is not
338      * specified in the config file, and a blanket <code>/*</code> is not
339      * used, a client will be allowed to access the resource and will not be
340      * protected by Deadbolt.
341      */
342     public void doFilter(ServletRequest request, ServletResponse response,
343             FilterChain chain) throws IOException, ServletException {
344         logger.debug("ENTERING: doFilter");
345         // Cast the request and response so HTTP specific methods can be used
346         HttpServletRequest httpRequest = (HttpServletRequest) request;
347         HttpServletResponse httpResponse = (HttpServletResponse) response;
348         Room room = null;
349 
350         // This is a flag to specify if the request will be allowed in or not
351         boolean forwardRequest = false;
352         
353         /*
354          * Before we execute any URL specific Rooms, check for a global Room
355          * and execute it first.
356          */
357         if(globalRoom != null) {
358             try {
359                 DeadboltHandler handler = null;
360                 Collection handlers = globalRoom.getHandlers().values();
361                 for(Iterator handlerIterator = handlers.iterator();
362                     handlerIterator.hasNext();) {
363                     String handlerClass = (String) handlerIterator.next();
364                     logger.debug("Executing global handler: " + handlerClass);
365                     /*
366                      * Since we don't know the class of the actual handler,
367                      * we're using the DeadboltHandler class, which all handlers
368                      * extend. We instantiate the actual class using the String
369                      * name.
370                      */
371                     handler = (DeadboltHandler) Class.forName(handlerClass)
372                             .newInstance();
373                     
374                     // Here's the meat. Do the work on this handler.
375                     forwardRequest = handler.authenticate(httpRequest,
376                             httpResponse, globalRoom);
377                     logger.debug("Global handler returned: " + forwardRequest);
378                     if (!forwardRequest)
379                         break;
380                 }
381             } catch (InstantiationException ie) {
382                 logger.warn(ie);
383                 throw new ServletException(ie); 
384             } catch (IllegalAccessException iae) {
385                 logger.warn(iae);
386                 throw new ServletException(iae);
387             } catch (ClassNotFoundException cnfe) {
388                 logger.warn(cnfe);
389                 throw new ServletException(cnfe);
390             }
391         }
392         
393         // This is the URL pattern of the request that just came in
394         String currentPattern = httpRequest.getServletPath();
395         // This is a flag to identify if a matching room is found
396         boolean matchFound = false;
397 
398         /*
399          * Iterate over the mappings to find the handlers to execute on this
400          * request On each pass, compare the URL pattern in the mapping to that
401          * of the request, and set the matchFound flag to true and set the room
402          * if something is found.
403          */
404         for (Iterator it = urlMappings.iterator(); it.hasNext();) {
405             URLMapping mapping = (URLMapping) it.next();
406             // Use a regular expression to compare the URL for the request to
407             // the URL in the mapping
408             if (currentPattern.matches(mapping.getUrlPattern())) {
409                 matchFound = true;
410                 // Set the room to be used below
411                 room = mapping.getRoom();
412                 break;
413             }
414         }
415 
416         // If a match was found, execute the handlers for that room
417         if(!matchFound) {
418             forwardRequest = true;
419         } else if(forwardRequest & room != null) {
420             try {
421                 DeadboltHandler handler = null;
422                 // Get a Collection, because TreeMap has no Iterator or the like
423                 Collection handlers = room.getHandlers().values();
424                 /*
425                  * Iterate over the handler chain for this room, calling the
426                  * authenticate method on each one. If any handler returns a
427                  * false, then set forwardRequest to false and exit the loop.
428                  */
429                 for (Iterator i = handlers.iterator(); i.hasNext();) {
430                     String handlerClass = (String) i.next();
431                     logger.debug("Executing handler: " + handlerClass);
432                     /*
433                      * Since we don't know the class of the actual handler,
434                      * we're using the DeadboltHandler class, which all handlers
435                      * extend. We instantiate the actual class using the String
436                      * name.
437                      */
438                     handler = (DeadboltHandler) Class.forName(handlerClass)
439                             .newInstance();
440 
441                     // Here's the meat. Do the work on this handler.
442                     forwardRequest = handler.authenticate(httpRequest,
443                             httpResponse, room);
444                     logger.debug("Handler returned: " + forwardRequest);
445                     if (!forwardRequest)
446                         break;
447                 }
448             } catch (InstantiationException ie) {
449                 logger.warn(ie);
450                 throw new ServletException(ie); 
451             } catch (IllegalAccessException iae) {
452                 logger.warn(iae);
453                 throw new ServletException(iae);
454             } catch (ClassNotFoundException cnfe) {
455                 logger.warn(cnfe);
456                 throw new ServletException(cnfe);
457             }
458         }
459 
460         /*
461          * If the user is authenticated and authorized, send the request off to
462          * the resource. Otherwise, send the user to the error page defined for
463          * the room.
464          */
465         if (forwardRequest) {
466             logger.debug("The request was approved.  Continuing to resource.");
467             chain.doFilter(request, response);
468         } else {
469             logger.debug("The request was denied.  Forwarding to error page.");
470             if (room.getErrorPage() != null) {
471                 RequestDispatcher dispatcher = httpRequest
472                         .getRequestDispatcher(room.getErrorPage());
473                 dispatcher.forward(request, response);
474             }
475         }
476         logger.debug("EXITING: doFilter");
477     }
478 
479     /***
480      * This is here for compliance with the <code>Filter</code> interface.
481      * Nothing is done in this method.
482      */
483     public void destroy() {
484     }
485 }