001 /* Copyright 2006-2009 the original author or authors. 002 * 003 * Licensed under the Apache License, Version 2.0 (the "License"); 004 * you may not use this file except in compliance with the License. 005 * You may obtain a copy of the License at 006 * 007 * http://www.apache.org/licenses/LICENSE-2.0 008 * 009 * Unless required by applicable law or agreed to in writing, software 010 * distributed under the License is distributed on an "AS IS" BASIS, 011 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 012 * See the License for the specific language governing permissions and 013 * limitations under the License. 014 */ 015 package org.codehaus.groovy.grails.plugins.springsecurity; 016 017 import java.lang.reflect.Field; 018 import java.util.Collection; 019 import java.util.HashMap; 020 import java.util.HashSet; 021 import java.util.Map; 022 import java.util.Set; 023 024 import javax.servlet.ServletContext; 025 import javax.servlet.http.HttpServletRequest; 026 import javax.servlet.http.HttpServletResponse; 027 028 import org.apache.commons.lang.WordUtils; 029 import org.codehaus.groovy.grails.commons.ApplicationHolder; 030 import org.codehaus.groovy.grails.commons.ControllerArtefactHandler; 031 import org.codehaus.groovy.grails.commons.GrailsApplication; 032 import org.codehaus.groovy.grails.commons.GrailsClass; 033 import org.codehaus.groovy.grails.commons.GrailsControllerClass; 034 import org.codehaus.groovy.grails.web.context.ServletContextHolder; 035 import org.codehaus.groovy.grails.web.mapping.UrlMappingInfo; 036 import org.codehaus.groovy.grails.web.mapping.UrlMappingsHolder; 037 import org.codehaus.groovy.grails.web.servlet.mvc.GrailsParameterMap; 038 import org.codehaus.groovy.grails.web.servlet.mvc.GrailsWebRequest; 039 import org.codehaus.groovy.grails.web.util.WebUtils; 040 import org.springframework.security.ConfigAttributeDefinition; 041 import org.springframework.security.intercept.web.FilterInvocation; 042 import org.springframework.security.intercept.web.FilterInvocationDefinitionSource; 043 import org.springframework.util.Assert; 044 import org.springframework.util.StringUtils; 045 046 /** 047 * A {@link FilterInvocationDefinitionSource} that uses rules defined with Controller annotations 048 * combined with static rules defined in <code>SecurityConfig.groovy</code>, e.g. for js, images, css 049 * or for rules that cannot be expressed in a controller like '/**'. 050 * 051 * @author <a href='mailto:beckwithb@studentsonly.com'>Burt Beckwith</a> 052 */ 053 public class AnnotationFilterInvocationDefinition extends AbstractFilterInvocationDefinition { 054 055 private UrlMappingsHolder _urlMappingsHolder; 056 057 @Override 058 protected String determineUrl(final FilterInvocation filterInvocation) { 059 HttpServletRequest request = filterInvocation.getHttpRequest(); 060 HttpServletResponse response = filterInvocation.getHttpResponse(); 061 ServletContext servletContext = ServletContextHolder.getServletContext(); 062 GrailsApplication application = ApplicationHolder.getApplication(); 063 064 GrailsWebRequest existingRequest = WebUtils.retrieveGrailsWebRequest(); 065 066 String requestUrl = request.getRequestURI().substring(request.getContextPath().length()); 067 068 String url = null; 069 try { 070 GrailsWebRequest grailsRequest = new GrailsWebRequest(request, response, servletContext); 071 WebUtils.storeGrailsWebRequest(grailsRequest); 072 073 Map<String, Object> savedParams = copyParams(grailsRequest); 074 075 for (UrlMappingInfo mapping : _urlMappingsHolder.matchAll(requestUrl)) { 076 configureMapping(mapping, grailsRequest, savedParams); 077 078 url = findGrailsUrl(mapping, application); 079 if (url != null) { 080 break; 081 } 082 } 083 } 084 finally { 085 if (existingRequest == null) { 086 WebUtils.clearGrailsWebRequest(); 087 } 088 else { 089 WebUtils.storeGrailsWebRequest(existingRequest); 090 } 091 } 092 093 if (!StringUtils.hasLength(url)) { 094 // probably css/js/image 095 url = requestUrl; 096 } 097 098 return lowercaseAndStringQuerystring(url); 099 } 100 101 private String findGrailsUrl(final UrlMappingInfo mapping, final GrailsApplication application) { 102 103 String actionName = mapping.getActionName(); 104 if (!StringUtils.hasLength(actionName)) { 105 actionName = ""; 106 } 107 108 String controllerName = mapping.getControllerName(); 109 110 if (isController(controllerName, actionName, application)) { 111 if (!StringUtils.hasLength(actionName) || "null".equals(actionName)) { 112 actionName = "index"; 113 } 114 return ("/" + controllerName + "/" + actionName).trim(); 115 } 116 117 return null; 118 } 119 120 private boolean isController(final String controllerName, final String actionName, 121 final GrailsApplication application) { 122 return application.getArtefactForFeature(ControllerArtefactHandler.TYPE, 123 "/" + controllerName + "/" + actionName) != null; 124 } 125 126 private void configureMapping(final UrlMappingInfo mapping, final GrailsWebRequest grailsRequest, 127 final Map<String, Object> savedParams) { 128 129 // reset params since mapping.configure() sets values 130 GrailsParameterMap params = grailsRequest.getParams(); 131 params.clear(); 132 params.putAll(savedParams); 133 134 mapping.configure(grailsRequest); 135 } 136 137 @SuppressWarnings("unchecked") 138 private Map<String, Object> copyParams(final GrailsWebRequest grailsRequest) { 139 return new HashMap<String, Object>(grailsRequest.getParams()); 140 } 141 142 /** 143 * Called by the plugin to set controller role info.<br/> 144 * 145 * Reinitialize by calling <code>ctx.objectDefinitionSource.initialize( 146 * ctx.authenticateService.securityConfig.security.annotationStaticRules, 147 * ctx.grailsUrlMappingsHolder, 148 * ApplicationHolder.application.controllerClasses)</code> 149 * 150 * @param staticRules keys are URL patterns, values are role names for that pattern 151 * @param urlMappingsHolder mapping holder 152 * @param controllerClasses all controllers 153 */ 154 public void initialize( 155 final Map<String, Collection<String>> staticRules, 156 final UrlMappingsHolder urlMappingsHolder, 157 final GrailsClass[] controllerClasses) { 158 159 Map<String, Map<String, Set<String>>> actionRoleMap = new HashMap<String, Map<String,Set<String>>>(); 160 Map<String, Set<String>> classRoleMap = new HashMap<String, Set<String>>(); 161 162 Assert.notNull(staticRules, "staticRules map is required"); 163 Assert.notNull(urlMappingsHolder, "urlMappingsHolder is required"); 164 165 _compiled.clear(); 166 167 _urlMappingsHolder = urlMappingsHolder; 168 169 for (GrailsClass controllerClass : controllerClasses) { 170 findControllerAnnotations((GrailsControllerClass)controllerClass, actionRoleMap, classRoleMap); 171 } 172 173 compileActionMap(actionRoleMap); 174 compileClassMap(classRoleMap); 175 compileStaticRules(staticRules); 176 177 if (_log.isTraceEnabled()) { 178 _log.trace("configs: " + _compiled); 179 } 180 } 181 182 private void compileActionMap(final Map<String, Map<String, Set<String>>> map) { 183 for (Map.Entry<String, Map<String, Set<String>>> controllerEntry : map.entrySet()) { 184 String controllerName = controllerEntry.getKey(); 185 Map<String, Set<String>> actionRoles = controllerEntry.getValue(); 186 for (Map.Entry<String, Set<String>> actionEntry : actionRoles.entrySet()) { 187 String actionName = actionEntry.getKey(); 188 Set<String> roles = actionEntry.getValue(); 189 storeMapping(controllerName, actionName, roles, false); 190 } 191 } 192 } 193 194 private void compileClassMap(final Map<String, Set<String>> classRoleMap) { 195 for (Map.Entry<String, Set<String>> entry : classRoleMap.entrySet()) { 196 String controllerName = entry.getKey(); 197 Set<String> roles = entry.getValue(); 198 storeMapping(controllerName, null, roles, false); 199 } 200 } 201 202 private void compileStaticRules(final Map<String, Collection<String>> staticRules) { 203 for (Map.Entry<String, Collection<String>> entry : staticRules.entrySet()) { 204 String pattern = entry.getKey(); 205 Collection<String> roles = entry.getValue(); 206 storeMapping(pattern, null, roles, true); 207 } 208 } 209 210 private void storeMapping(final String controllerNameOrPattern, final String actionName, 211 final Collection<String> roles, final boolean isPattern) { 212 213 String fullPattern; 214 if (isPattern) { 215 fullPattern = controllerNameOrPattern; 216 } 217 else { 218 StringBuilder sb = new StringBuilder(); 219 sb.append('/').append(controllerNameOrPattern); 220 if (actionName != null) { 221 sb.append('/').append(actionName); 222 } 223 sb.append("/**"); 224 fullPattern = sb.toString(); 225 } 226 227 ConfigAttributeDefinition configAttribute = new ConfigAttributeDefinition( 228 roles.toArray(new String[roles.size()])); 229 230 Object key = getUrlMatcher().compile(fullPattern); 231 ConfigAttributeDefinition replaced = _compiled.put(key, configAttribute); 232 if (replaced != null) { 233 _log.warn("replaced rule for '" + key + "' with roles " + replaced.getConfigAttributes() 234 + " with roles " + configAttribute.getConfigAttributes()); 235 } 236 } 237 238 private void findControllerAnnotations(final GrailsControllerClass controllerClass, 239 final Map<String, Map<String, Set<String>>> actionRoleMap, 240 final Map<String, Set<String>> classRoleMap) { 241 242 Class<?> clazz = controllerClass.getClazz(); 243 String controllerName = WordUtils.uncapitalize(controllerClass.getName()); 244 245 Secured annotation = clazz.getAnnotation(Secured.class); 246 if (annotation != null) { 247 classRoleMap.put(controllerName, asSet(annotation.value())); 248 } 249 250 Map<String, Set<String>> annotatedClosureNames = findActionRoles(clazz); 251 if (annotatedClosureNames != null) { 252 actionRoleMap.put(controllerName, annotatedClosureNames); 253 } 254 } 255 256 private Map<String, Set<String>> findActionRoles(final Class<?> clazz) { 257 // since action closures are defined as "def foo = ..." they're 258 // fields, but they end up as private 259 Map<String, Set<String>> actionRoles = new HashMap<String, Set<String>>(); 260 for (Field field : clazz.getDeclaredFields()) { 261 Secured annotation = field.getAnnotation(Secured.class); 262 if (annotation != null) { 263 actionRoles.put(field.getName(), asSet(annotation.value())); 264 } 265 } 266 return actionRoles; 267 } 268 269 private Set<String> asSet(final String[] strings) { 270 Set<String> set = new HashSet<String>(); 271 for (String string : strings) { 272 set.add(string); 273 } 274 return set; 275 } 276 }