001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.apache.shiro.web.filter.authz;
020
021import org.apache.shiro.lang.util.StringUtils;
022import org.slf4j.Logger;
023import org.slf4j.LoggerFactory;
024
025import javax.servlet.ServletRequest;
026import javax.servlet.ServletResponse;
027import javax.servlet.http.HttpServletRequest;
028import java.io.IOException;
029import java.util.HashMap;
030import java.util.Map;
031
032/**
033 * A filter that translates an HTTP Request's Method (e.g. GET, POST, etc.)
034 * into an corresponding action (verb) and uses that verb to construct a permission that will be checked to determine
035 * access.
036 * <p/>
037 * This Filter is primarily provided to support REST environments where the type (Method)
038 * of request translates to an action being performed on one or more resources.  This paradigm works well with Shiro's
039 * concepts of using permissions for access control and can be leveraged to easily perform permission checks.
040 * <p/>
041 * This filter functions as follows:
042 * <ol>
043 * <li>The incoming HTTP request's Method (GET, POST, PUT, DELETE, etc.) is discovered.</li>
044 * <li>The Method is translated into a more 'application friendly' verb, such as 'create', edit', 'delete', etc.</li>
045 * <li>The verb is appended to any configured permissions for the
046 * {@link org.apache.shiro.web.filter.PathMatchingFilter currently matching path}.</li>
047 * <li>If the current {@code Subject} {@link org.apache.shiro.subject.Subject#isPermitted(String) isPermitted} to
048 * perform the resolved action, the request is allowed to continue.</li>
049 * </ol>
050 * <p/>
051 * For example, if the following filter chain was defined, where 'rest' was the name given to a filter instance of
052 * this class:
053 * <pre>
054 * /user/** = rest[user]</pre>
055 * Then an HTTP {@code GET} request to {@code /user/1234} would translate to the constructed permission
056 * {@code user:read} (GET is mapped to the 'read' action) and execute the permission check
057 * <code>Subject.isPermitted(&quot;user:read&quot;)</code> in order to allow the request to continue.
058 * <p/>
059 * Similarly, an HTTP {@code POST} to {@code /user} would translate to the constructed permission
060 * {@code user:create} (POST is mapped to the 'create' action) and execute the permission check
061 * <code>Subject.isPermitted(&quot;user:create&quot;)</code> in order to allow the request to continue.
062 * <p/>
063 * <h3>Method To Verb Mapping</h3>
064 * The following table represents the default HTTP Method-to-action verb mapping:
065 * <table>
066 * <tr><th>HTTP Method</th><th>Mapped Action</th><th>Example Permission</th><th>Runtime Check</th></tr>
067 * <tr><td>head</td><td>read</td><td>perm1</td><td>perm1:read</td></tr>
068 * <tr><td>get</td><td>read</td><td>perm2</td><td>perm2:read</td></tr>
069 * <tr><td>put</td><td>update</td><td>perm3</td><td>perm3:update</td></tr>
070 * <tr><td>post</td><td>create</td><td>perm4</td><td>perm4:create</td></tr>
071 * <tr><td>mkcol</td><td>create</td><td>perm5</td><td>perm5:create</td></tr>
072 * <tr><td>options</td><td>read</td><td>perm6</td><td>perm6:read</td></tr>
073 * <tr><td>trace</td><td>read</td><td>perm7</td><td>perm7:read</td></tr>
074 * </table>
075 *
076 * @since 1.0
077 */
078public class HttpMethodPermissionFilter extends PermissionsAuthorizationFilter {
079
080    /**
081     * This class's private logger.
082     */
083    private static final Logger LOGGER = LoggerFactory.getLogger(HttpMethodPermissionFilter.class);
084
085    //Actions representing HTTP Method values (GET -> read, POST -> create, etc.)
086    private static final String CREATE_ACTION = "create";
087    private static final String READ_ACTION = "read";
088    private static final String UPDATE_ACTION = "update";
089    private static final String DELETE_ACTION = "delete";
090
091    /**
092     * Map that contains a mapping between http methods to permission actions (verbs)
093     */
094    private final Map<String, String> httpMethodActions = new HashMap<String, String>();
095
096    /**
097     * Enum of constants for well-defined mapping values.  Used in the Filter's constructor to perform the map instance
098     * used at runtime.
099     */
100    private enum HttpMethodAction {
101
102        /**
103         * DELETE
104         */
105        DELETE(DELETE_ACTION),
106        /**
107         * GET
108         */
109        GET(READ_ACTION),
110        /**
111         * HEAD
112         */
113        HEAD(READ_ACTION),
114        /**
115         * MKCOL
116         */
117        MKCOL(CREATE_ACTION),
118        /**
119         * OPTIONS
120         * webdav, but useful here
121         */
122        OPTIONS(READ_ACTION),
123        /**
124         * POST
125         */
126        POST(CREATE_ACTION),
127        /**
128         * PUT
129         */
130        PUT(UPDATE_ACTION),
131        /**
132         * TRACE
133         */
134        TRACE(READ_ACTION);
135
136        private final String action;
137
138        HttpMethodAction(String action) {
139            this.action = action;
140        }
141
142        public String getAction() {
143            return this.action;
144        }
145    }
146
147    /**
148     * Creates the filter instance with default method-to-action values in the instance's
149     * {@link #getHttpMethodActions() http method actions map}.
150     */
151    public HttpMethodPermissionFilter() {
152        for (HttpMethodAction methodAction : HttpMethodAction.values()) {
153            httpMethodActions.put(methodAction.name().toLowerCase(), methodAction.getAction());
154        }
155    }
156
157    /**
158     * Returns the HTTP Method name (key) to action verb (value) mapping used to resolve actions based on an
159     * incoming {@code HttpServletRequest}.  All keys and values are lower-case.  The
160     * default key/value pairs are defined in the top class-level JavaDoc.
161     *
162     * @return the HTTP Method lower-case name (key) to lower-case action verb (value) mapping
163     */
164    protected Map<String, String> getHttpMethodActions() {
165        return this.httpMethodActions;
166    }
167
168    /**
169     * Determines the action (verb) attempting to be performed on the filtered resource by the current request.
170     * <p/>
171     * This implementation expects the incoming request to be an {@link HttpServletRequest} and returns a mapped
172     * action based on the HTTP request {@link javax.servlet.http.HttpServletRequest#getMethod() method}.
173     *
174     * @param request to pull the method from.
175     * @return The string equivalent verb of the http method.
176     */
177    protected String getHttpMethodAction(ServletRequest request) {
178        String method = ((HttpServletRequest) request).getMethod();
179        return getHttpMethodAction(method);
180    }
181
182    /**
183     * Determines the corresponding application action that will be performed on the filtered resource based on the
184     * specified HTTP method (GET, POST, etc.).
185     *
186     * @param method to be translated into the verb.
187     * @return The string equivalent verb of the method.
188     */
189    protected String getHttpMethodAction(String method) {
190        String lc = method.toLowerCase();
191        String resolved = getHttpMethodActions().get(lc);
192        return resolved != null ? resolved : method;
193    }
194
195    /**
196     * Returns a collection of String permissions with which to perform a permission check to determine if the filter
197     * will allow the request to continue.
198     * <p/>
199     * This implementation merely delegates to {@link #buildPermissions(String[], String)} and ignores the inbound
200     * HTTP servlet request, but it can be overridden by subclasses for more complex request-specific building logic
201     * if necessary.
202     *
203     * @param request         the inbound HTTP request - ignored in this implementation, but available to
204     *                        subclasses for more complex construction building logic if necessary
205     * @param configuredPerms any url-specific permissions mapped to this filter in the URL rules mappings.
206     * @param action          the application-friendly action (verb) resolved based on the HTTP Method name.
207     * @return a collection of String permissions with which to perform a permission check to determine if the filter
208     * will allow the request to continue.
209     */
210    protected String[] buildPermissions(HttpServletRequest request, String[] configuredPerms, String action) {
211        return buildPermissions(configuredPerms, action);
212    }
213
214    /**
215     * Builds a new array of permission strings based on the original argument, appending the specified action verb
216     * to each one per {@link org.apache.shiro.authz.permission.WildcardPermission WildcardPermission} conventions.  The
217     * built permission strings will be the ones used at runtime during the permission check that determines if filter
218     * access should be allowed to continue or not.
219     * <p/>
220     * For example, if the {@code configuredPerms} argument contains the following 3 permission strings:
221     * <p/>
222     * <ol>
223     * <li>permission:one</li>
224     * <li>permission:two</li>
225     * <li>permission:three</li>
226     * </ol>
227     * And the action is {@code read}, then the return value will be:
228     * <ol>
229     * <li>permission:one:read</li>
230     * <li>permission:two:read</li>
231     * <li>permission:three:read</li>
232     * </ol>
233     * per {@link org.apache.shiro.authz.permission.WildcardPermission WildcardPermission} conventions.  Subclasses
234     * are of course free to override this method or the
235     * {@link #buildPermissions(javax.servlet.http.HttpServletRequest, String[], String) buildPermissions} request
236     * variant for custom building logic or with different permission formats.
237     *
238     * @param configuredPerms list of configuredPerms to be converted.
239     * @param action          the resolved action based on the request method to be appended to permission strings.
240     * @return an array of permission strings with each element appended with the action.
241     */
242    protected String[] buildPermissions(String[] configuredPerms, String action) {
243        if (configuredPerms == null || configuredPerms.length <= 0 || !StringUtils.hasText(action)) {
244            return configuredPerms;
245        }
246
247        String[] mappedPerms = new String[configuredPerms.length];
248
249        // loop and append :action
250        for (int i = 0; i < configuredPerms.length; i++) {
251            mappedPerms[i] = configuredPerms[i] + ":" + action;
252        }
253
254        if (LOGGER.isTraceEnabled()) {
255            StringBuilder sb = new StringBuilder();
256            for (int i = 0; i < mappedPerms.length; i++) {
257                if (i > 0) {
258                    sb.append(", ");
259                }
260                sb.append(mappedPerms[i]);
261            }
262            LOGGER.trace("MAPPED '{}' action to permission(s) '{}'", action, sb);
263        }
264
265        return mappedPerms;
266    }
267
268    /**
269     * Resolves an 'application friendly' action verb based on the {@code HttpServletRequest}'s method, appends that
270     * action to each configured permission (the {@code mappedValue} argument is a {@code String[]} array), and
271     * delegates the permission check for the newly constructed permission(s) to the superclass
272     * {@link PermissionsAuthorizationFilter#isAccessAllowed(ServletRequest, ServletResponse, Object) isAccessAllowed}
273     * implementation to perform the actual permission check.
274     *
275     * @param request     the inbound {@code ServletRequest}
276     * @param response    the outbound {@code ServletResponse}
277     * @param mappedValue the filter-specific config value mapped to this filter in the URL rules mappings.
278     * @return {@code true} if the request should proceed through the filter normally, {@code false} if the
279     * request should be processed by this filter's
280     * {@link #onAccessDenied(ServletRequest, ServletResponse, Object)} method instead.
281     * @throws IOException
282     */
283    @Override
284    public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws IOException {
285        String[] perms = (String[]) mappedValue;
286        // append the http action to the end of the permissions and then back to super
287        String action = getHttpMethodAction(request);
288        String[] resolvedPerms = buildPermissions(perms, action);
289        return super.isAccessAllowed(request, response, resolvedPerms);
290    }
291}