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    }