View Javadoc

1   /**
2    * Copyright 2005-2007 the original author or authors.
3    *
4    * Licensed under the Gnu General Pubic License, Version 2.0 (the
5    * "License"); you may not use this file except in compliance with
6    * the License. You may obtain a copy of the License at
7    *
8    *      http://www.opensource.org/licenses/gpl-license.php
9    *
10   * This program is distributed in the hope that it will be useful,
11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
13   * See the Gnu General Public License for more details.
14   */
15  package org.figure8.join.control;
16  
17  import org.figure8.join.control.config.RoleConfig;
18  import org.figure8.join.control.config.AccessControlConfig;
19  import org.figure8.join.control.config.AccessControlRuleSet;
20  import org.figure8.join.control.config.ActionConstraintConfig;
21  import org.figure8.join.control.config.OperationConstraintConfig;
22  import org.figure8.join.services.cache.Cache;
23  import org.figure8.join.services.cache.EternalCacheKeys;
24  import org.figure8.join.services.cache.EternalCacheAccessor;
25  import org.figure8.join.businessobjects.security.Role;
26  import org.figure8.join.util.LogUtil;
27  
28  import org.xml.sax.InputSource;
29  import org.apache.commons.logging.Log;
30  import org.apache.commons.digester.Digester;
31  
32  import javax.servlet.Filter;
33  import javax.servlet.FilterChain;
34  import javax.servlet.FilterConfig;
35  import javax.servlet.ServletRequest;
36  import javax.servlet.ServletResponse;
37  import javax.servlet.ServletException;
38  import javax.servlet.UnavailableException;
39  import javax.servlet.ServletContext;
40  import javax.servlet.http.HttpServletRequest;
41  import javax.servlet.http.HttpServletResponse;
42  
43  import java.net.URL;
44  import java.io.InputStream;
45  import java.io.IOException;
46  import java.util.Iterator;
47  import java.util.Collection;
48  /**
49   * Servlet filter that checks is user associated with request we are
50   * processing is allowed to call the specified action or business operation
51   * with the action.
52   * @author  <a href="mailto:laurent.broudoux@free.fr">Laurent Broudoux</a>
53   * @version $Revision: 1.2 $
54   */
55  public class AccessControlFilter implements Filter{
56  
57     // Static -------------------------------------------------------------------
58  
59     /** Get a commons logger. */
60     private static final Log log = LogUtil.getLog(AccessControlFilter.class);
61  
62  
63     // Attributes ---------------------------------------------------------------
64     
65     /** The context relative name of the XML configuration file. */
66     private String config;
67     /** The filter configuration object we are associated with. */
68     private FilterConfig filterConfig;
69     /** The Access control configuration object (retrieved from servlet context). */
70     private AccessControlConfig accessConfig;
71     
72     
73     // Constructors -------------------------------------------------------------
74     
75     /** Creates a new AccessControlFilter instance. */
76     public AccessControlFilter(){
77     }
78     
79     
80     // Implementation of Filter -------------------------------------------------
81     
82     /**
83      * Called by the web container to indicate to a filter that is being placed
84      * into service. This method stores filterConfig into inner attribute and
85      * creates an AccessControlConfig object by "digestering" the join-access-control.xml
86      * configuration file.
87      * @param filterConfig The configuration of this filter
88      * @throws ServletException if app servlet context is not correctly initialized
89      */
90     public void init(FilterConfig filterConfig) throws ServletException{
91        this.filterConfig = filterConfig;
92  
93        // Retrieve configuration file name.
94        config = filterConfig.getInitParameter("config");
95        log.info("Initializing AccessControlFilter with: " + config);
96        
97        InputStream input = null;
98        try{
99           accessConfig = new AccessControlConfig();
100          // Init digester with correct rules and push accessConfig.
101          Digester digester = initDigester();
102          digester.push(accessConfig);
103          // Parse the security control configuration.
104          URL url = filterConfig.getServletContext().getResource(config);
105          InputSource is = new InputSource(url.toExternalForm());
106          input = filterConfig.getServletContext().getResourceAsStream(config);
107          is.setByteStream(input);
108          
109          digester.parse(is);
110       }
111       catch (Exception e){
112          // Log and propagate.
113          log.error("AccessControl configuration file cannot be parsed !");
114          log.error("Here's the detailed message: " + e.getMessage());
115          throw new UnavailableException("Access Control configuration file cannot be parsed.");
116       }
117       finally{
118          // Try to close input stream.
119          if (input != null){
120             try {input.close();}
121             catch (Exception ee) {/* Do nothing here */}
122          }
123       }
124       log.info("Filter initialized. AccessControlFilter is enable");
125    }
126    
127    /**
128     * Apply the Access Control filter to rh request we are processing. This
129     * involves using the AccessControlConfig object created during initilization
130     * to check if there's constraints on the current requested action and
131     * business operation.<br/>
132     * If user is allowed to call action or business operation, <code>chain.doFilter()
133     * </code> is called. Else, user response is redierect to the page stored
134     * under "authorization.fails.url" servlet context attribute.
135     * @param request The servlet request we are processing
136     * @param response The servlet response we are creating
137     * @param chain The filter chain we are processing
138     * @exception IOException if an input/output error occurs
139     * @exception ServletException if a servlet error occurs
140     */
141    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
142            throws IOException, ServletException{
143 
144       // Identify the path component we will use to select a constraint.
145       String path = extractPath((HttpServletRequest)request);
146       if (log.isDebugEnabled())
147          log.debug("Applying AccessControlFilter for path: " + path);
148       
149       if (path != null){
150          // Is there any ActionConstraint config for this path ?
151          ActionConstraintConfig constraint = accessConfig.findActionConstraint(path);
152          
153          if (constraint != null){
154             boolean hasRole = false;
155             // Is there any roles associated to action or operation ?
156             Collection roles = getRequiredRoles((HttpServletRequest)request, constraint);
157             
158             if (roles != null && !roles.isEmpty()){
159                // Try retrieving roles from cache.
160                Cache roleCache = getRoleNameToRoleCache(filterConfig.getServletContext());
161                if (roleCache == null){
162                   // Log an throw an exception.
163                   log.error("Application is in illegal state: Role cache is not present in context");
164                   throw new IllegalStateException("App. is in illegal state: Role cache is not present in context");
165                }
166                // Get the user container associated with request.
167                UserContainer container = JoinAction.getUserContainer((HttpServletRequest)request);
168 
169                // Check if the user has one of the specified roles.
170                Iterator iterator = roles.iterator();
171                while (iterator.hasNext() && !hasRole){
172                   RoleConfig role = (RoleConfig)iterator.next();
173                   Role cachedRole = (Role)roleCache.get(role.getName());
174                   // Test role config upon container.
175                   if (!role.hasResource())
176                      hasRole = container.isUserInRole(cachedRole);
177                   else if (!role.hasResourceParameter())
178                      hasRole = container.isUserInRoleForResource(cachedRole, role.getResource());
179                   else{
180                      String resourceId = request.getParameter(role.getResourceParameter());
181                      try{
182                         // Retrieve resource for check if everything is ok.
183                         if (resourceId != null && cachedRole.getPermissionResourceResolver() != null){
184                            Object resource = cachedRole.getPermissionResourceResolver().getResource(resourceId);
185                            hasRole = container.isUserInRoleForResource(cachedRole, resource);
186                         }
187                      }
188                      catch (Throwable t){
189                         // Just warn into logs...
190                         log.warn("Throwable was caught when checking authorization for " + path);
191                         log.warn("Maybe join-access-control.xml is misconfigured or cached roles out of synch ?...");
192                      }
193                   }
194                }
195             }
196             else{
197                if (log.isDebugEnabled())
198                   log.debug("No required role for this path: allowing user to access");
199                hasRole = true;
200             }
201             
202             if (!hasRole){
203                if (log.isDebugEnabled())
204                   log.debug("Required role is not endorsed by user: forwarding to authorization failure page");
205                // Forward response to page with error message.
206                HttpServletRequest httpRequest = (HttpServletRequest)request;
207                HttpServletResponse httpResponse = (HttpServletResponse)response;
208                httpResponse.sendRedirect(httpRequest.getContextPath()
209                                           + filterConfig.getInitParameter("failurePage"));
210             }
211          }
212       }
213       
214       // Path is not identifiable or there is not declared constraints or everything is OK ...
215       chain.doFilter(request, response);
216    }
217    
218    /**
219     * Called by the container to indicate to a filter that is being taken out
220     * of service. Just release handles on <b>filterConfig</b> and <b>accessConfig</b>.
221     */
222    public void destroy(){
223       this.filterConfig = null;
224       this.accessConfig = null;
225    }
226    
227    
228    // Protected ----------------------------------------------------------------
229    
230    /**
231     * Create and return a new Digester instance that has been initialized
232     * to process Join security control configuration files and configure a
233     * corresponding AccessControlConfig object (which must be pushed on to
234     * the evaluation stack before parsing begins).
235     */
236    protected Digester initDigester(){
237       // Create a new digester instance with standard capabilities.
238       Digester digester = new Digester();
239       digester.setValidating(false);
240       digester.setNamespaceAware(true);
241       digester.setUseContextClassLoader(true);
242       digester.addRuleSet(new AccessControlRuleSet());
243       
244       // Return the completely configured Digester instance.
245       return digester;
246    }
247    
248    /**
249     * Identify and return the path component (from the request URI) that
250     * we will use to select an ActionConstraintConfig. If no such path can
251     * be identified, create an error response and return <code>null</code>.
252     * @param request The servlet request we are processing
253     */
254    protected String extractPath(HttpServletRequest request){
255       // Try simplest thing ...
256       String path = request.getPathInfo();
257       if ((path != null) && (path.length() > 0))
258          return (path);
259       
260       // Try something else ...
261       path = request.getServletPath();
262       int slash = path.lastIndexOf("/");
263       int period = path.lastIndexOf(".");
264       if ((period >= 0) && (period > slash))
265          path = path.substring(0, period);
266       
267       return path;
268    }
269    
270    /**
271     * Extract a collection of required roles from the ActionContraintConfig.
272     * Check if action constraint specifies constraints for the business operation
273     * requested.
274     * @param request The servlet request we are processing.
275     * @param constraint The security control constraints related to requested action.
276     * @return Collection of <code>org.figure8.join.security.RoleConfig</code>
277     */
278    protected Collection getRequiredRoles(HttpServletRequest request, ActionConstraintConfig constraint){
279       // Is there any OperationConstraint for this operation ?
280       OperationConstraintConfig opConstraint = constraint.findOperationConstraint(request.getParameter("op"));
281       if (opConstraint != null)
282          return opConstraint.getRoles();
283       
284       // Else simply return the roles associated with the action.
285       return constraint.getRoles();
286    }
287 
288 
289    // Private ------------------------------------------------------------------
290 
291    /** Get the cache for roles or null if it is not available */
292    private Cache getRoleNameToRoleCache(ServletContext ctx){
293       // First, get the cacheAccessor.
294       EternalCacheAccessor cacheAccessor = (EternalCacheAccessor)ctx.getAttribute(EternalCacheAccessor.CONTEXT_KEY);
295       // Return cache or null if no accessor.
296       if (cacheAccessor != null){
297          return cacheAccessor.getCache(EternalCacheKeys.ROLE_NAME_TO_ROLE_KEY);
298       }
299       return null;
300    }
301 }