View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *     http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.shiro.web.filter.authz;
20  
21  import org.apache.shiro.util.StringUtils;
22  import org.slf4j.Logger;
23  import org.slf4j.LoggerFactory;
24  
25  import javax.servlet.ServletRequest;
26  import javax.servlet.ServletResponse;
27  import javax.servlet.http.HttpServletRequest;
28  import java.io.IOException;
29  import java.util.HashMap;
30  import java.util.Map;
31  
32  /**
33   * A filter that translates an HTTP Request's Method (eg GET, POST, etc)
34   * into an corresponding action (verb) and uses that verb to construct a permission that will be checked to determine
35   * access.
36   * <p/>
37   * This Filter is primarily provided to support REST environments where the type (Method)
38   * of request translates to an action being performed on one or more resources.  This paradigm works well with Shiro's
39   * concepts of using permissions for access control and can be leveraged to easily perform permission checks.
40   * <p/>
41   * This filter functions as follows:
42   * <ol>
43   * <li>The incoming HTTP request's Method (GET, POST, PUT, DELETE, etc) is discovered.</li>
44   * <li>The Method is translated into a more 'application friendly' verb, such as 'create', edit', 'delete', etc.</li>
45   * <li>The verb is appended to any configured permissions for the
46   * {@link org.apache.shiro.web.filter.PathMatchingFilter currently matching path}.</li>
47   * <li>If the current {@code Subject} {@link org.apache.shiro.subject.Subject#isPermitted(String) isPermitted} to
48   * perform the resolved action, the request is allowed to continue.</li>
49   * </ol>
50   * <p/>
51   * For example, if the following filter chain was defined, where 'rest' was the name given to a filter instance of
52   * this class:
53   * <pre>
54   * /user/** = rest[user]</pre>
55   * Then an HTTP {@code GET} request to {@code /user/1234} would translate to the constructed permission
56   * {@code user:read} (GET is mapped to the 'read' action) and execute the permission check
57   * <code>Subject.isPermitted(&quot;user:read&quot;)</code> in order to allow the request to continue.
58   * <p/>
59   * Similarly, an HTTP {@code POST} to {@code /user} would translate to the constructed permission
60   * {@code user:create} (POST is mapped to the 'create' action) and execute the permission check
61   * <code>Subject.isPermitted(&quot;user:create&quot;)</code> in order to allow the request to continue.
62   * <p/>
63   * <h3>Method To Verb Mapping</h3>
64   * The following table represents the default HTTP Method-to-action verb mapping:
65   * <table>
66   * <tr><th>HTTP Method</th><th>Mapped Action</th><th>Example Permission</th><th>Runtime Check</th></tr>
67   * <tr><td>head</td><td>read</td><td>perm1</td><td>perm1:read</td></tr>
68   * <tr><td>get</td><td>read</td><td>perm2</td><td>perm2:read</td></tr>
69   * <tr><td>put</td><td>update</td><td>perm3</td><td>perm3:update</td></tr>
70   * <tr><td>post</td><td>create</td><td>perm4</td><td>perm4:create</td></tr>
71   * <tr><td>mkcol</td><td>create</td><td>perm5</td><td>perm5:create</td></tr>
72   * <tr><td>options</td><td>read</td><td>perm6</td><td>perm6:read</td></tr>
73   * <tr><td>trace</td><td>read</td><td>perm7</td><td>perm7:read</td></tr>
74   * </table>
75   *
76   * @since 1.0
77   */
78  public class HttpMethodPermissionFilter extends PermissionsAuthorizationFilter {
79  
80      /**
81       * This class's private logger.
82       */
83      private static final Logger log = LoggerFactory.getLogger(HttpMethodPermissionFilter.class);
84  
85      /**
86       * Map that contains a mapping between http methods to permission actions (verbs)
87       */
88      private final Map<String, String> httpMethodActions = new HashMap<String, String>();
89  
90      //Actions representing HTTP Method values (GET -> read, POST -> create, etc)
91      private static final String CREATE_ACTION = "create";
92      private static final String READ_ACTION = "read";
93      private static final String UPDATE_ACTION = "update";
94      private static final String DELETE_ACTION = "delete";
95  
96      /**
97       * Enum of constants for well-defined mapping values.  Used in the Filter's constructor to perform the map instance
98       * used at runtime.
99       */
100     private static enum HttpMethodAction {
101 
102         DELETE(DELETE_ACTION),
103         GET(READ_ACTION),
104         HEAD(READ_ACTION),
105         MKCOL(CREATE_ACTION), //webdav, but useful here
106         OPTIONS(READ_ACTION),
107         POST(CREATE_ACTION),
108         PUT(UPDATE_ACTION),
109         TRACE(READ_ACTION);
110 
111         private final String action;
112 
113         private HttpMethodAction(String action) {
114             this.action = action;
115         }
116 
117         public String getAction() {
118             return this.action;
119         }
120     }
121 
122     /**
123      * Creates the filter instance with default method-to-action values in the instance's
124      * {@link #getHttpMethodActions() http method actions map}.
125      */
126     public HttpMethodPermissionFilter() {
127         for (HttpMethodAction methodAction : HttpMethodAction.values()) {
128             httpMethodActions.put(methodAction.name().toLowerCase(), methodAction.getAction());
129         }
130     }
131 
132     /**
133      * Returns the HTTP Method name (key) to action verb (value) mapping used to resolve actions based on an
134      * incoming {@code HttpServletRequest}.  All keys and values are lower-case.  The
135      * default key/value pairs are defined in the top class-level JavaDoc.
136      *
137      * @return the HTTP Method lower-case name (key) to lower-case action verb (value) mapping
138      */
139     protected Map<String, String> getHttpMethodActions() {
140         return this.httpMethodActions;
141     }
142 
143     /**
144      * Determines the action (verb) attempting to be performed on the filtered resource by the current request.
145      * <p/>
146      * This implementation expects the incoming request to be an {@link HttpServletRequest} and returns a mapped
147      * action based on the HTTP request {@link javax.servlet.http.HttpServletRequest#getMethod() method}.
148      *
149      * @param request to pull the method from.
150      * @return The string equivalent verb of the http method.
151      */
152     protected String getHttpMethodAction(ServletRequest request) {
153         String method = ((HttpServletRequest) request).getMethod();
154         return getHttpMethodAction(method);
155     }
156 
157     /**
158      * Determines the corresponding application action that will be performed on the filtered resource based on the
159      * specified HTTP method (GET, POST, etc).
160      *
161      * @param method to be translated into the verb.
162      * @return The string equivalent verb of the method.
163      */
164     protected String getHttpMethodAction(String method) {
165         String lc = method.toLowerCase();
166         String resolved = getHttpMethodActions().get(lc);
167         return resolved != null ? resolved : method;
168     }
169 
170     /**
171      * Returns a collection of String permissions with which to perform a permission check to determine if the filter
172      * will allow the request to continue.
173      * <p/>
174      * This implementation merely delegates to {@link #buildPermissions(String[], String)} and ignores the inbound
175      * HTTP servlet request, but it can be overridden by subclasses for more complex request-specific building logic
176      * if necessary.
177      *
178      * @param request         the inbound HTTP request - ignored in this implementation, but available to
179      *                        subclasses for more complex construction building logic if necessary
180      * @param configuredPerms any url-specific permissions mapped to this filter in the URL rules mappings.
181      * @param action          the application-friendly action (verb) resolved based on the HTTP Method name.
182      * @return a collection of String permissions with which to perform a permission check to determine if the filter
183      *         will allow the request to continue.
184      */
185     protected String[] buildPermissions(HttpServletRequest request, String[] configuredPerms, String action) {
186         return buildPermissions(configuredPerms, action);
187     }
188 
189     /**
190      * Builds a new array of permission strings based on the original argument, appending the specified action verb
191      * to each one per {@link org.apache.shiro.authz.permission.WildcardPermission WildcardPermission} conventions.  The
192      * built permission strings will be the ones used at runtime during the permission check that determines if filter
193      * access should be allowed to continue or not.
194      * <p/>
195      * For example, if the {@code configuredPerms} argument contains the following 3 permission strings:
196      * <p/>
197      * <ol>
198      * <li>permission:one</li>
199      * <li>permission:two</li>
200      * <li>permission:three</li>
201      * </ol>
202      * And the action is {@code read}, then the return value will be:
203      * <ol>
204      * <li>permission:one:read</li>
205      * <li>permission:two:read</li>
206      * <li>permission:three:read</li>
207      * </ol>
208      * per {@link org.apache.shiro.authz.permission.WildcardPermission WildcardPermission} conventions.  Subclasses
209      * are of course free to override this method or the
210      * {@link #buildPermissions(javax.servlet.http.HttpServletRequest, String[], String) buildPermissions} request
211      * variant for custom building logic or with different permission formats.
212      *
213      * @param configuredPerms list of configuredPerms to be converted.
214      * @param action          the resolved action based on the request method to be appended to permission strings.
215      * @return an array of permission strings with each element appended with the action.
216      */
217     protected String[] buildPermissions(String[] configuredPerms, String action) {
218         if (configuredPerms == null || configuredPerms.length <= 0 || !StringUtils.hasText(action)) {
219             return configuredPerms;
220         }
221 
222         String[] mappedPerms = new String[configuredPerms.length];
223 
224         // loop and append :action
225         for (int i = 0; i < configuredPerms.length; i++) {
226             mappedPerms[i] = configuredPerms[i] + ":" + action;
227         }
228 
229         if (log.isTraceEnabled()) {
230             StringBuilder sb = new StringBuilder();
231             for (int i = 0; i < mappedPerms.length; i++) {
232                 if (i > 0) {
233                     sb.append(", ");
234                 }
235                 sb.append(mappedPerms[i]);
236             }
237             log.trace("MAPPED '{}' action to permission(s) '{}'", action, sb);
238         }
239 
240         return mappedPerms;
241     }
242 
243     /**
244      * Resolves an 'application friendly' action verb based on the {@code HttpServletRequest}'s method, appends that
245      * action to each configured permission (the {@code mappedValue} argument is a {@code String[]} array), and
246      * delegates the permission check for the newly constructed permission(s) to the superclass
247      * {@link PermissionsAuthorizationFilter#isAccessAllowed(javax.servlet.ServletRequest, javax.servlet.ServletResponse, Object) isAccessAllowed}
248      * implementation to perform the actual permission check.
249      *
250      * @param request     the inbound {@code ServletRequest}
251      * @param response    the outbound {@code ServletResponse}
252      * @param mappedValue the filter-specific config value mapped to this filter in the URL rules mappings.
253      * @return {@code true} if the request should proceed through the filter normally, {@code false} if the
254      *         request should be processed by this filter's
255      *         {@link #onAccessDenied(ServletRequest,ServletResponse,Object)} method instead.
256      * @throws IOException
257      */
258     @Override
259     public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws IOException {
260         String[] perms = (String[]) mappedValue;
261         // append the http action to the end of the permissions and then back to super
262         String action = getHttpMethodAction(request);
263         String[] resolvedPerms = buildPermissions(perms, action);
264         return super.isAccessAllowed(request, response, resolvedPerms);
265     }
266 }