Permalink
Browse files

added support for SpEL in request mappings

  • Loading branch information...
1 parent 61ddb1d commit 0042da3ae548707f7114bd49e1a7ccc5603e4d37 @burtbeckwith burtbeckwith committed May 12, 2010
@@ -47,6 +47,7 @@ import org.springframework.security.web.access.channel.InsecureChannelProcessor
import org.springframework.security.web.access.channel.RetryWithHttpEntryPoint
import org.springframework.security.web.access.channel.RetryWithHttpsEntryPoint
import org.springframework.security.web.access.channel.SecureChannelProcessor
+import org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor
import org.springframework.security.web.authentication.AnonymousAuthenticationFilter
import org.springframework.security.web.authentication.Http403ForbiddenEntryPoint
@@ -94,10 +95,11 @@ import org.codehaus.groovy.grails.plugins.springsecurity.SecurityEventListener
import org.codehaus.groovy.grails.plugins.springsecurity.SecurityFilterPosition
import org.codehaus.groovy.grails.plugins.springsecurity.SecurityRequestHolder
import org.codehaus.groovy.grails.plugins.springsecurity.SpringSecurityUtils
+import org.codehaus.groovy.grails.plugins.springsecurity.WebExpressionVoter
class SpringSecurityCoreGrailsPlugin {
- String version = '0.2.1'
+ String version = '0.3'
String grailsVersion = '1.2.2 > *'
List observe = ['controllers']
List loadAfter = ['controllers', 'services', 'hibernate']
@@ -261,10 +263,13 @@ class SpringSecurityCoreGrailsPlugin {
filterInvocationInterceptor(FilterSecurityInterceptor) {
authenticationManager = ref('authenticationManager')
accessDecisionManager = ref('accessDecisionManager')
- objectDefinitionSource = ref('objectDefinitionSource')
+ securityMetadataSource = ref('objectDefinitionSource')
}
if (conf.securityConfigType == SecurityConfigType.Annotation) {
objectDefinitionSource(AnnotationFilterInvocationDefinition) {
+ roleVoter = ref('roleVoter')
+ authenticatedVoter = ref('authenticatedVoter')
+ expressionHandler = ref('webExpressionHandler')
boolean lowercase = conf.controllerAnnotations.lowercase // true
if ('ant'.equals(conf.controllerAnnotations.matcher)) {
urlMatcher = new AntUrlPathMatcher(lowercase)
@@ -279,6 +284,9 @@ class SpringSecurityCoreGrailsPlugin {
}
else if (conf.securityConfigType == SecurityConfigType.Requestmap) {
objectDefinitionSource(RequestmapFilterInvocationDefinition) {
+ roleVoter = ref('roleVoter')
+ authenticatedVoter = ref('authenticatedVoter')
+ expressionHandler = ref('webExpressionHandler')
urlMatcher = new AntUrlPathMatcher(true)
if (conf.rejectIfNoRule instanceof Boolean) {
rejectIfNoRule = conf.rejectIfNoRule
@@ -287,6 +295,9 @@ class SpringSecurityCoreGrailsPlugin {
}
else if (conf.securityConfigType == SecurityConfigType.InterceptUrlMap) {
objectDefinitionSource(InterceptUrlMapFilterInvocationDefinition) {
+ roleVoter = ref('roleVoter')
+ authenticatedVoter = ref('authenticatedVoter')
+ expressionHandler = ref('webExpressionHandler')
urlMatcher = new AntUrlPathMatcher(true)
if (conf.rejectIfNoRule instanceof Boolean) {
rejectIfNoRule = conf.rejectIfNoRule
@@ -620,6 +631,14 @@ class SpringSecurityCoreGrailsPlugin {
authenticationTrustResolver = ref('authenticationTrustResolver')
}
+ webExpressionHandler(DefaultWebSecurityExpressionHandler) {
+ roleHierarchy = ref('roleHierarchy')
+ }
+
+ webExpressionVoter(WebExpressionVoter) {
+ expressionHandler = ref('webExpressionHandler')
+ }
+
// create the default list here, will be replaced in doWithApplicationContext
def voters = createRefList(SpringSecurityUtils.VOTER_NAMES)
@@ -1,6 +1,7 @@
grails.project.class.dir = 'target/classes'
grails.project.test.class.dir = 'target/test-classes'
-grails.project.test.reports.dir = 'target/test-reports'
+grails.project.test.reports.dir = 'target/test-reports'
+grails.project.docs.output.dir = 'docs' // for backwards-compatibility, the docs are checked into gh-pages branch
grails.project.dependency.resolution = {
@@ -48,4 +48,3 @@ grails {
grails.doc.authors = 'Burt Beckwith'
grails.doc.license = 'Apache License 2.0'
-grails.doc.images = new File('src/docs/resources/img')
@@ -79,7 +79,7 @@ class SecurityTagLib {
def username = { attrs ->
if (springSecurityService.isLoggedIn()) {
- out << springSecurityService.principal.username
+ out << springSecurityService.authentication.name
}
}
@@ -0,0 +1,76 @@
+Spring Security's expression support (which uses [Spring Expression Language|http://static.springsource.org/spring/docs/3.0.x/spring-framework-reference/html/expressions.html]) allows you to declare the rules for guarding URLs more descriptively than using the traditional approach, and also allows much more fine-grained rules. Where you traditionally would specify a list of role names and/or special tokens (e.g. @IS_AUTHENTICATED_FULLY@), with SpEL you can instead use the embedded scripting language to define simple or complex access rules.
+
+You can read about Spring Security's expression support [here|http://static.springsource.org/spring-security/site/docs/3.0.x/reference/el-access.html].
+
+You can use expressions using any of the three approaches. For example, consider this annotated controller:
+
+{code}
+package com.yourcompany.yourapp
+
+import grails.plugins.springsecurity.Secured
+
+class SecureController {
+
+ @Secured(["hasRole('ROLE_ADMIN')"])
+ def someAction = {
+ ...
+ }
+
+ @Secured(["authentication.name == 'ralph'"])
+ def someOtherAction = {
+ ...
+ }
+}
+{code}
+
+Here we're saying that @someAction@ requires @ROLE_ADMIN@, and @someOtherAction@ requires that the user be logged in with username 'ralph'.
+
+The corresponding @Requestmap@s would be
+
+{code}
+new Requestmap(url: "/secure/someAction",
+ configAttribute: "hasRole('ROLE_ADMIN'").save()
+
+new Requestmap(url: "/secure/someOtherAction",
+ configAttribute: "authentication.name == 'ralph'").save()
+{code}
+
+and the corresponding static mappings would be
+
+{code}
+grails.plugins.springsecurity.interceptUrlMap = [
+ '/secure/someAction': ["hasRole('ROLE_ADMIN'"],
+ '/secure/someOtherAction': ["authentication.name == 'ralph'"]
+]
+{code}
+
+The Spring Security docs have a [table listing the standard expressions|http://static.springsource.org/spring-security/site/docs/3.0.x/reference/el-access.html#el-common-built-in] which is copied here for reference:
+
+{table}
+*Expression* | *Description*
+@hasRole([role])@ | Returns true if the current principal has the specified role.
+@hasAnyRole([role1,role2])@ | Returns true if the current principal has any of the supplied roles (given as a comma-separated list of strings)
+@principal@ | Allows direct access to the principal object representing the current user
+@authentication@ | Allows direct access to the current Authentication object obtained from the SecurityContext
+@permitAll@ | Always evaluates to true
+@denyAll@ | Always evaluates to false
+@isAnonymous()@ | Returns true if the current principal is an anonymous user
+@isRememberMe()@ | Returns true if the current principal is a remember-me user
+@isAuthenticated()@ | Returns true if the user is not anonymous
+@isFullyAuthenticated()@ | Returns true if the user is not an anonymous or a remember-me user
+{table}
+
+In addition, there is a web-specific expression @hasIpAddress@ which you can use. You may find however that it's more convenient to separate IP restrictions from role restrictions using the [IP address filter|guide:10.8. IP Address Restrictions].
+
+To help migrate traditional configurations to expressions, here's a table comparing various configurations and their corresponding expressions:
+
+{table}
+*Traditional* | *Expression*
+@ROLE_ADMIN@ | @hasRole('ROLE_USER')@
+@ROLE_USER,ROLE_ADMIN@ | @hasAnyRole('ROLE_USER,ROLE_ADMIN')@
+@ROLE_ADMIN,IS_AUTHENTICATED_FULLY@ | @hasRole('ROLE_ADMIN') and isFullyAuthenticated()@
+@IS_AUTHENTICATED_ANONYMOUSLY@ | @permitAll@
+@IS_AUTHENTICATED_REMEMBERED@ | @isAnonymous() or isRememberMe()@
+@IS_AUTHENTICATED_FULLY@ | @isFullyAuthenticated()@
+{table}
+
@@ -24,9 +24,13 @@
import org.apache.log4j.Logger;
import org.springframework.beans.factory.InitializingBean;
+import org.springframework.expression.Expression;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
+import org.springframework.security.access.vote.AuthenticatedVoter;
+import org.springframework.security.access.vote.RoleVoter;
import org.springframework.security.web.FilterInvocation;
+import org.springframework.security.web.access.expression.WebSecurityExpressionHandler;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.security.web.util.AntUrlPathMatcher;
import org.springframework.security.web.util.UrlMatcher;
@@ -42,6 +46,9 @@
private UrlMatcher _urlMatcher;
private boolean _rejectIfNoRule;
private boolean _stripQueryStringFromUrls = true;
+ private RoleVoter _roleVoter;
+ private AuthenticatedVoter _authenticatedVoter;
+ private WebSecurityExpressionHandler _expressionHandler;
private final Map<Object, Collection<ConfigAttribute>> _compiled = new HashMap<Object, Collection<ConfigAttribute>>();
@@ -207,27 +214,88 @@ protected UrlMatcher getUrlMatcher() {
protected void compileAndStoreMapping(final String pattern, final List<String> tokens) {
- Collection<ConfigAttribute> configAttributes = new HashSet<ConfigAttribute>();
- for (String token : tokens) {
- configAttributes.add(new SecurityConfig(token));
- }
-
Object key = getUrlMatcher().compile(pattern);
- Collection<ConfigAttribute> replaced = storeMapping(key, Collections.unmodifiableCollection(configAttributes));
+ Collection<ConfigAttribute> configAttributes = buildConfigAttributes(tokens);
+
+ Collection<ConfigAttribute> replaced = storeMapping(key,
+ Collections.unmodifiableCollection(configAttributes));
if (replaced != null) {
- _log.warn("replaced rule for '" + key + "' with roles " + replaced + " with roles " + configAttributes);
+ _log.warn("replaced rule for '" + key + "' with roles " + replaced +
+ " with roles " + configAttributes);
}
}
- protected Collection<ConfigAttribute> storeMapping(Object key, Collection<ConfigAttribute> configAttributes) {
+ protected Collection<ConfigAttribute> buildConfigAttributes(final Collection<String> tokens) {
+ Collection<ConfigAttribute> configAttributes = new HashSet<ConfigAttribute>();
+ for (String token : tokens) {
+ ConfigAttribute config = new SecurityConfig(token);
+ if ((_roleVoter != null && _roleVoter.supports(config)) ||
+ (_authenticatedVoter != null && _authenticatedVoter.supports(config))) {
+ configAttributes.add(config);
+ }
+ else {
+ Expression expression = _expressionHandler.getExpressionParser().parseExpression(token);
+ configAttributes.add(new WebExpressionConfigAttribute(expression));
+ }
+ }
+ return configAttributes;
+ }
+
+ protected Collection<ConfigAttribute> storeMapping(final Object key,
+ final Collection<ConfigAttribute> configAttributes) {
return _compiled.put(key, configAttributes);
}
protected void resetConfigs() {
_compiled.clear();
}
+ public Collection<ConfigAttribute> findMatchingAttributes(final String url) {
+ for (Map.Entry<Object, Collection<ConfigAttribute>> entry : _compiled.entrySet()) {
+ if (_urlMatcher.pathMatchesUrl(entry.getKey(), url)) {
+ return entry.getValue();
+ }
+ }
+ return Collections.emptyList();
+ }
+
+ /**
+ * Dependency injection for the role voter.
+ * @param voter the voter
+ */
+ public void setRoleVoter(final RoleVoter voter) {
+ _roleVoter = voter;
+ }
+
+ protected RoleVoter getRoleVoter() {
+ return _roleVoter;
+ }
+
+ /**
+ * Dependency injection for the authenticated voter.
+ * @param voter the voter
+ */
+ public void setAuthenticatedVoter(final AuthenticatedVoter voter) {
+ _authenticatedVoter = voter;
+ }
+
+ protected AuthenticatedVoter getAuthenticatedVoter() {
+ return _authenticatedVoter;
+ }
+
+ /**
+ * Dependency injection for the expression handler.
+ * @param handler the handler
+ */
+ public void setExpressionHandler(final WebSecurityExpressionHandler handler) {
+ _expressionHandler = handler;
+ }
+
+ protected WebSecurityExpressionHandler getExpressionHandler() {
+ return _expressionHandler;
+ }
+
/**
* {@inheritDoc}
* @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
@@ -40,7 +40,6 @@
import org.codehaus.groovy.grails.web.servlet.mvc.GrailsWebRequest;
import org.codehaus.groovy.grails.web.util.WebUtils;
import org.springframework.security.access.ConfigAttribute;
-import org.springframework.security.access.SecurityConfig;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.util.Assert;
@@ -227,10 +226,7 @@ private void storeMapping(final String controllerNameOrPattern, final String act
fullPattern = sb.toString();
}
- Collection<ConfigAttribute> configAttributes = new HashSet<ConfigAttribute>();
- for (String token : tokens) {
- configAttributes.add(new SecurityConfig(token));
- }
+ Collection<ConfigAttribute> configAttributes = buildConfigAttributes(tokens);
Object key = getUrlMatcher().compile(fullPattern);
Collection<ConfigAttribute> replaced = storeMapping(key, configAttributes);
@@ -90,6 +90,7 @@
static {
VOTER_NAMES.add("authenticatedVoter");
VOTER_NAMES.add("roleVoter");
+ VOTER_NAMES.add("webExpressionVoter");
PROVIDER_NAMES.add("daoAuthenticationProvider");
PROVIDER_NAMES.add("anonymousAuthenticationProvider");
@@ -0,0 +1,61 @@
+/* Copyright 2006-2010 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.springframework.expression.Expression;
+import org.springframework.security.access.ConfigAttribute;
+
+/**
+ * Simple expression configuration attribute for use in web request authorizations.
+ * Based on the class of the same name in Spring Security which is package-default.
+ *
+ * @author Luke Taylor
+ * @author <a href='mailto:burt@burtbeckwith.com'>Burt Beckwith</a>
+ */
+public class WebExpressionConfigAttribute implements ConfigAttribute {
+
+ private static final long serialVersionUID = 1L;
+
+ private final Expression _authorizeExpression;
+
+ /**
+ * Constructor.
+ * @param authorizeExpression the expression
+ */
+ public WebExpressionConfigAttribute(final Expression authorizeExpression) {
+ _authorizeExpression = authorizeExpression;
+ }
+
+ /**
+ * Accessor for the expression.
+ * @return the expression
+ */
+ public Expression getAuthorizeExpression() {
+ return _authorizeExpression;
+ }
+
+ /**
+ * {@inheritDoc}
+ * @see org.springframework.security.access.ConfigAttribute#getAttribute()
+ */
+ public String getAttribute() {
+ return null;
+ }
+
+ @Override
+ public String toString() {
+ return _authorizeExpression.getExpressionString();
+ }
+}
Oops, something went wrong.

0 comments on commit 0042da3

Please sign in to comment.