View Javadoc

1   package com.atlassian.xwork.interceptors;
2   
3   import com.opensymphony.xwork.interceptor.Interceptor;
4   import com.opensymphony.xwork.ActionInvocation;
5   import com.opensymphony.webwork.ServletActionContext;
6   import com.atlassian.xwork.PermittedMethods;
7   import com.atlassian.xwork.HttpMethod;
8   
9   import javax.servlet.http.HttpServletRequest;
10  import java.lang.reflect.Method;
11  import java.util.List;
12  import java.util.ArrayList;
13  import java.util.Arrays;
14  
15  import org.apache.log4j.Logger;
16  
17  /**
18   * Interceptor used to restrict which HTTP methods are allowed to access which Action methods. Best used as a first
19   * line of defence against XSRF attacks.
20   *
21   * <p>What HTTP methods are permitted may be configured either by adding the {@link com.atlassian.xwork.PermittedMethods}
22   * annotation to the method that will be invoked on the action class, enumerating the methods that will be accepted, or
23   * by adding a configuration parameter to the action definition in <code>xwork.xml</code>. If both are provided, the
24   * <code>xwork.xml</code> configuration will be used, and any annotation-based configuration will be ignored. An example
25   * of the parameter configuration:
26   *
27   * <blockquote><pre>&lt;action name="blah" class="com.example.MyAction">
28   *     &lt;param name="permittedMethods">GET, POST, PUT&lt;/param>
29   *     &lt;result name="success" type="redirect">/index.html&lt;result>
30   * &lt;/action></pre></blockquote>
31   *
32   * <p>Note that method names are case sensitive, and all upper case. They must correspond to one of the values of the
33   * {@link com.atlassian.xwork.HttpMethod} enum.
34   *
35   * <p>Implementations should extend this class to configure a <code>SecurityLevel</code>. See the Javadoc of the
36   * relevant class for what effect different security levels have on the operation of the interceptor.
37   *
38   * <p>If the method execution is rejected, the interceptor returns an "invalidmethod" result. It is up to the
39   * implementor to do something useful with that information.
40   *
41   * @since 1.6
42   */
43  public abstract class RestrictHttpMethodInterceptor implements Interceptor
44  {
45      private static final Logger log = Logger.getLogger(RestrictHttpMethodInterceptor.class);
46      public static final String INVALID_METHOD_RESULT = "invalidmethod";
47      public static final String PERMITTED_METHODS_PARAM_NAME = "permittedMethods";
48  
49      public static enum SecurityLevel {
50          /** Do not restrict access at all */
51          NONE
52          {
53              @Override
54              public boolean isPermitted(String invocationMethodName, HttpMethod[] permittedMethods, String httpMethod)
55              {
56                  return true;
57              }
58          },
59          /** Restrict access only on methods that are annotated */
60          OPT_IN
61          {
62              @Override
63              public boolean isPermitted(String invocationMethodName, HttpMethod[] permittedMethods, String httpMethod)
64              {
65                  if (permittedMethods.length == 0)
66                      return true;
67  
68                  return methodMatches(httpMethod, permittedMethods);
69              }
70          },
71          /**
72           * Restrict annotated methods as annotated. Allow GET and POST to un-annotated methods named doDefault().
73           * Only allow POST to other methods.
74           */
75          DEFAULT
76          {
77              @Override
78              public boolean isPermitted(String invocationMethodName, HttpMethod[] permittedMethods, String httpMethod)
79              {
80                  if (permittedMethods.length == 0)
81                  {
82                      if (invocationMethodName.equals("doDefault"))
83                          return methodMatches(httpMethod, HttpMethod.GET, HttpMethod.POST);
84                      else
85                          return methodMatches(httpMethod, HttpMethod.POST);
86                  }
87  
88                  return methodMatches(httpMethod, permittedMethods);
89              }
90          },
91          /**
92           * Do not allow any invocation of methods that are not annotated
93           */
94          STRICT
95          {
96              @Override
97              public boolean isPermitted(String invocationMethodName, HttpMethod[] permittedMethods, String httpMethod)
98              {
99                  return methodMatches(httpMethod, permittedMethods);
100             }
101         };
102 
103         private static boolean methodMatches(String httpMethod, HttpMethod... allowedMethods)
104         {
105             for (HttpMethod allowedMethod : allowedMethods)
106             {
107                 if (allowedMethod.matches(httpMethod))
108                     return true;
109             }
110 
111             return false;
112         }
113 
114         public abstract boolean isPermitted(String invocationMethodName, HttpMethod[] permittedMethods, String httpMethod);
115     }
116 
117     public final String intercept(ActionInvocation invocation) throws Exception
118     {
119         Method invocationMethod = invocation.getProxy().getConfig().getMethod();
120         String configParam = (String) invocation.getProxy().getConfig().getParams().get(PERMITTED_METHODS_PARAM_NAME);
121         PermittedMethods annotation = invocationMethod.getAnnotation(PermittedMethods.class);
122         HttpMethod[] permittedMethods = toPermittedMethodArray(configParam, annotation);
123 
124         String httpMethod = getHttpMethod();
125 
126         if (log.isDebugEnabled())
127             log.debug("Checking HTTP method: " + getHttpMethod() + " permitted against " + fullMethodName(invocationMethod));
128 
129         if (getSecurityLevel().isPermitted(invocationMethod.getName(), permittedMethods, httpMethod))
130         {
131             log.debug("Invocation proceeding");
132             return invocation.invoke();
133         }
134         else
135         {
136             // TODO: calculate the canonical list of permitted methods here and add an Allow: header to make our 405 response RFC-compliant
137             log.info("Refusing HTTP method: " + httpMethod + " against " + fullMethodName(invocationMethod) + " (configured allowed methods: " + Arrays.toString(permittedMethods) + ")");
138             return INVALID_METHOD_RESULT;
139         }
140     }
141 
142     private HttpMethod[] toPermittedMethodArray(String configParam, PermittedMethods annotation)
143     {
144         if (configParam != null && configParam.trim().length() > 0)
145         {
146             String[] methodNames = configParam.trim().split("\\s*,\\s*");
147             List<HttpMethod> permittedMethods = new ArrayList<HttpMethod>(methodNames.length);
148             for (String methodName : methodNames)
149             {
150                 try
151                 {
152                     permittedMethods.add(HttpMethod.valueOf(methodName));
153                 }
154                 catch (IllegalArgumentException e)
155                 {
156                     log.error("XWork configuration error: " + methodName + " is not a recognised HTTP method (method names are case sensitive).");
157                 }
158             }
159 
160             return permittedMethods.toArray(new HttpMethod[permittedMethods.size()]);
161         }
162         else if (annotation != null)
163         {
164             return annotation.value();
165         }
166         else
167         {
168             return new HttpMethod[0];
169         }
170     }
171 
172     private String fullMethodName(Method invocationMethod)
173     {
174         return invocationMethod.getDeclaringClass().getName() + "#" + invocationMethod.getName();
175     }
176 
177     private String getHttpMethod()
178     {
179         HttpServletRequest servletRequest = ServletActionContext.getRequest();
180         return servletRequest == null ? "" : servletRequest.getMethod();
181     }
182 
183     ///CLOVER:OFF
184     public final void destroy()
185     {
186     }
187 
188     public final void init()
189     {
190     }
191 
192     /**
193      * Get the currently configured security level for the interceptor. The default implementation will always return
194      * SecurityLevel.DEFAULT. Implementors should override this method if they want to provide a mechanism for
195      * configuring security levels.
196      *
197      * @return the currently configured security level for this interceptor
198      */
199     protected SecurityLevel getSecurityLevel()
200     {
201         return SecurityLevel.DEFAULT;
202     }
203 }