/* 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 org.apache.commons.lang.WordUtils;
import org.codehaus.groovy.grails.commons.*;
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;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Field;
import java.util.*;
/**
* A {@link FilterInvocationDefinitionSource} that uses rules defined with Controller annotations
* combined with static rules defined in <code>SecurityConfig.groovy</code>, e.g. for js, images, css
* or for rules that cannot be expressed in a controller like '/**'.
*
* @author <a href='mailto:beckwithb@studentsonly.com'>Burt Beckwith</a>
*/
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<String, Object> 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<String, Object> savedParams) {
// reset params since mapping.configure() sets values
GrailsParameterMap params = grailsRequest.getParams();
params.clear();
params.putAll(savedParams);
mapping.configure(grailsRequest);
}
@SuppressWarnings("unchecked")
private Map<String, Object> copyParams(final GrailsWebRequest grailsRequest) {
return new HashMap<String, Object>(grailsRequest.getParams());
}
/**
* Called by the plugin to set controller role info.<br/>
* <p/>
* Reinitialize by calling <code>ctx.objectDefinitionSource.initialize(
* ctx.authenticateService.securityConfig.security.annotationStaticRules,
* ctx.grailsUrlMappingsHolder,
* ApplicationHolder.application.controllerClasses)</code>
*
* @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<String, Collection<String>> staticRules,
final UrlMappingsHolder urlMappingsHolder,
final GrailsClass[] controllerClasses) {
Map<String, Map<String, Set<String>>> actionRoleMap = new HashMap<String, Map<String, Set<String>>>();
Map<String, Set<String>> classRoleMap = new HashMap<String, Set<String>>();
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<String, Map<String, Set<String>>> map) {
for (Map.Entry<String, Map<String, Set<String>>> controllerEntry : map.entrySet()) {
String controllerName = controllerEntry.getKey();
Map<String, Set<String>> actionRoles = controllerEntry.getValue();
for (Map.Entry<String, Set<String>> actionEntry : actionRoles.entrySet()) {
String actionName = actionEntry.getKey();
Set<String> roles = actionEntry.getValue();
storeMapping(controllerName, actionName, roles, false);
}
}
}
private void compileClassMap(final Map<String, Set<String>> classRoleMap) {
for (Map.Entry<String, Set<String>> entry : classRoleMap.entrySet()) {
String controllerName = entry.getKey();
Set<String> roles = entry.getValue();
storeMapping(controllerName, null, roles, false);
}
}
private void compileStaticRules(final Map<String, Collection<String>> staticRules) {
for (Map.Entry<String, Collection<String>> entry : staticRules.entrySet()) {
String pattern = entry.getKey();
Collection<String> roles = entry.getValue();
storeMapping(pattern, null, roles, true);
}
}
private void storeMapping(final String controllerNameOrPattern, final String actionName,
final Collection<String> 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<String, Map<String, Set<String>>> actionRoleMap,
final Map<String, Set<String>> 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<String, Set<String>> annotatedClosureNames = findActionRoles(clazz);
if (annotatedClosureNames != null) {
actionRoleMap.put(controllerName, annotatedClosureNames);
}
}
private Map<String, Set<String>> findActionRoles(final Class<?> clazz) {
// since action closures are defined as "def foo = ..." they're
// fields, but they end up as private
Map<String, Set<String>> actionRoles = new HashMap<String, Set<String>>();
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<String> asSet(final String[] strings) {
Set<String> set = new HashSet<String>();
for (String string : strings) {
set.add(string);
}
return set;
}
}