/* Copyright 2006-2009 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.codehaus.groovy.grails.plugins.springsecurity; import java.lang.reflect.Field; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.lang.WordUtils; import org.codehaus.groovy.grails.commons.ApplicationHolder; import org.codehaus.groovy.grails.commons.ControllerArtefactHandler; import org.codehaus.groovy.grails.commons.GrailsApplication; import org.codehaus.groovy.grails.commons.GrailsClass; import org.codehaus.groovy.grails.commons.GrailsControllerClass; import org.codehaus.groovy.grails.web.context.ServletContextHolder; import org.codehaus.groovy.grails.web.mapping.UrlMappingInfo; import org.codehaus.groovy.grails.web.mapping.UrlMappingsHolder; import org.codehaus.groovy.grails.web.servlet.mvc.GrailsParameterMap; import org.codehaus.groovy.grails.web.servlet.mvc.GrailsWebRequest; import org.codehaus.groovy.grails.web.util.WebUtils; import org.springframework.security.ConfigAttributeDefinition; import org.springframework.security.intercept.web.FilterInvocation; import org.springframework.security.intercept.web.FilterInvocationDefinitionSource; import org.springframework.util.Assert; import org.springframework.util.StringUtils; /** * A {@link FilterInvocationDefinitionSource} that uses rules defined with Controller annotations * combined with static rules defined in SecurityConfig.groovy, e.g. for js, images, css * or for rules that cannot be expressed in a controller like '/**'. * * @author Burt Beckwith */ public class AnnotationFilterInvocationDefinition extends AbstractFilterInvocationDefinition { private UrlMappingsHolder _urlMappingsHolder; @Override protected String determineUrl(final FilterInvocation filterInvocation) { HttpServletRequest request = filterInvocation.getHttpRequest(); HttpServletResponse response = filterInvocation.getHttpResponse(); ServletContext servletContext = ServletContextHolder.getServletContext(); GrailsApplication application = ApplicationHolder.getApplication(); GrailsWebRequest existingRequest = WebUtils.retrieveGrailsWebRequest(); String requestUrl = request.getRequestURI().substring(request.getContextPath().length()); String url = null; try { GrailsWebRequest grailsRequest = new GrailsWebRequest(request, response, servletContext); WebUtils.storeGrailsWebRequest(grailsRequest); Map savedParams = copyParams(grailsRequest); for (UrlMappingInfo mapping : _urlMappingsHolder.matchAll(requestUrl)) { configureMapping(mapping, grailsRequest, savedParams); url = findGrailsUrl(mapping, application); if (url != null) { break; } } } finally { if (existingRequest == null) { WebUtils.clearGrailsWebRequest(); } else { WebUtils.storeGrailsWebRequest(existingRequest); } } if (!StringUtils.hasLength(url)) { // probably css/js/image url = requestUrl; } return lowercaseAndStringQuerystring(url); } private String findGrailsUrl(final UrlMappingInfo mapping, final GrailsApplication application) { String actionName = mapping.getActionName(); if (!StringUtils.hasLength(actionName)) { actionName = ""; } String controllerName = mapping.getControllerName(); if (isController(controllerName, actionName, application)) { if (!StringUtils.hasLength(actionName) || "null".equals(actionName)) { actionName = "index"; } return ("/" + controllerName + "/" + actionName).trim(); } return null; } private boolean isController(final String controllerName, final String actionName, final GrailsApplication application) { return application.getArtefactForFeature(ControllerArtefactHandler.TYPE, "/" + controllerName + "/" + actionName) != null; } private void configureMapping(final UrlMappingInfo mapping, final GrailsWebRequest grailsRequest, final Map savedParams) { // reset params since mapping.configure() sets values GrailsParameterMap params = grailsRequest.getParams(); params.clear(); params.putAll(savedParams); mapping.configure(grailsRequest); } @SuppressWarnings("unchecked") private Map copyParams(final GrailsWebRequest grailsRequest) { return new HashMap(grailsRequest.getParams()); } /** * Called by the plugin to set controller role info.
* * Reinitialize by calling ctx.objectDefinitionSource.initialize( * ctx.authenticateService.securityConfig.security.annotationStaticRules, * ctx.grailsUrlMappingsHolder, * ApplicationHolder.application.controllerClasses) * * @param staticRules keys are URL patterns, values are role names for that pattern * @param urlMappingsHolder mapping holder * @param controllerClasses all controllers */ public void initialize( final Map> staticRules, final UrlMappingsHolder urlMappingsHolder, final GrailsClass[] controllerClasses) { Map>> actionRoleMap = new HashMap>>(); Map> classRoleMap = new HashMap>(); Assert.notNull(staticRules, "staticRules map is required"); Assert.notNull(urlMappingsHolder, "urlMappingsHolder is required"); _compiled.clear(); _urlMappingsHolder = urlMappingsHolder; for (GrailsClass controllerClass : controllerClasses) { findControllerAnnotations((GrailsControllerClass)controllerClass, actionRoleMap, classRoleMap); } compileActionMap(actionRoleMap); compileClassMap(classRoleMap); compileStaticRules(staticRules); if (_log.isTraceEnabled()) { _log.trace("configs: " + _compiled); } } private void compileActionMap(final Map>> map) { for (Map.Entry>> controllerEntry : map.entrySet()) { String controllerName = controllerEntry.getKey(); Map> actionRoles = controllerEntry.getValue(); for (Map.Entry> actionEntry : actionRoles.entrySet()) { String actionName = actionEntry.getKey(); Set roles = actionEntry.getValue(); storeMapping(controllerName, actionName, roles, false); } } } private void compileClassMap(final Map> classRoleMap) { for (Map.Entry> entry : classRoleMap.entrySet()) { String controllerName = entry.getKey(); Set roles = entry.getValue(); storeMapping(controllerName, null, roles, false); } } private void compileStaticRules(final Map> staticRules) { for (Map.Entry> entry : staticRules.entrySet()) { String pattern = entry.getKey(); Collection roles = entry.getValue(); storeMapping(pattern, null, roles, true); } } private void storeMapping(final String controllerNameOrPattern, final String actionName, final Collection roles, final boolean isPattern) { String fullPattern; if (isPattern) { fullPattern = controllerNameOrPattern; } else { StringBuilder sb = new StringBuilder(); sb.append('/').append(controllerNameOrPattern); if (actionName != null) { sb.append('/').append(actionName); } sb.append("/**"); fullPattern = sb.toString(); } ConfigAttributeDefinition configAttribute = new ConfigAttributeDefinition( roles.toArray(new String[roles.size()])); Object key = getUrlMatcher().compile(fullPattern); ConfigAttributeDefinition replaced = _compiled.put(key, configAttribute); if (replaced != null) { _log.warn("replaced rule for '" + key + "' with roles " + replaced.getConfigAttributes() + " with roles " + configAttribute.getConfigAttributes()); } } private void findControllerAnnotations(final GrailsControllerClass controllerClass, final Map>> actionRoleMap, final Map> classRoleMap) { Class clazz = controllerClass.getClazz(); String controllerName = WordUtils.uncapitalize(controllerClass.getName()); Secured annotation = clazz.getAnnotation(Secured.class); if (annotation != null) { classRoleMap.put(controllerName, asSet(annotation.value())); } Map> annotatedClosureNames = findActionRoles(clazz); if (annotatedClosureNames != null) { actionRoleMap.put(controllerName, annotatedClosureNames); } } private Map> findActionRoles(final Class clazz) { // since action closures are defined as "def foo = ..." they're // fields, but they end up as private Map> actionRoles = new HashMap>(); for (Field field : clazz.getDeclaredFields()) { Secured annotation = field.getAnnotation(Secured.class); if (annotation != null) { actionRoles.put(field.getName(), asSet(annotation.value())); } } return actionRoles; } private Set asSet(final String[] strings) { Set set = new HashSet(); for (String string : strings) { set.add(string); } return set; } }