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("user:read")</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("user:create")</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}