4 Configuring Request Mappings to Secure URLs - Reference Documentation
Authors: Burt Beckwith, Beverley Talbott
Version: 2.0.0
Table of Contents
4 Configuring Request Mappings to Secure URLs
You can choose among the following approaches to configuring request mappings for secure application URLs. The goal is to map URL patterns to the roles required to access those URLs.@Secured
annotations (default approach)- A simple Map in
Config.groovy
Requestmap
domain class instances stored in the database
securityConfigType
attribute; the value has to be an SecurityConfigType
enum value or the name of the enum as a String.Pessimistic Lockdown
Many applications are mostly public, with some pages only accessible to authenticated users with various roles. In this case, it might make sense to leave URLs open by default and restrict access on a case-by-case basis. However, if your application is primarily secure, you can use a pessimistic lockdown approach to deny access to all URLs that do not have an applicable URL-Role request mapping. But the pessimistic approach is safer; if you forget to restrict access to a URL using the optimistic approach, it might take a while to discover that unauthorized users can access the URL, but if you forget to allow access when using the pessimistic approach, no user can access it and the error should be quickly discovered.The pessimistic approach is the default, and there are two configuration options that apply. IfrejectIfNoRule
is true
(the default) then any URL that has no request mappings (an annotation, entry in controllerAnnotations.staticRules
or interceptUrlMap
, or a Requestmap
instance) will be denied to all users. The other option is fii.rejectPublicInvocations
and if it is true
(the default) then un-mapped URLs will trigger an IllegalArgumentException
and will show the error page. This is uglier, but more useful because it's very clear that there is a misconfiguration. When fii.rejectPublicInvocations
is false
but rejectIfNoRule
is true
you just see the "Sorry, you're not authorized to view this page." error 403 message.Note that the two settings are mutually exclusive. If rejectIfNoRule
is true
then fii.rejectPublicInvocations
is ignored because the request will transition to the login page or the error 403 page. If you want the more obvious error page, set fii.rejectPublicInvocations
to true
and rejectIfNoRule
to false
to allow that check to occur.To reject un-mapped URLs with a 403 error code, use these settings (or none since rejectIfNoRule
defaults to true
)grails.plugin.springsecurity.rejectIfNoRule = true grails.plugin.springsecurity.fii.rejectPublicInvocations = false
rejectPublicInvocations
since it defaults to true
):grails.plugin.springsecurity.rejectIfNoRule = false grails.plugin.springsecurity.fii.rejectPublicInvocations = true
rejectIfNoRule
or rejectPublicInvocations
to true
you'll need to configure the staticRules
map to include URLs that can't otherwise be guarded:grails.plugin.springsecurity.controllerAnnotations.staticRules = [ '/': ['permitAll'], '/index': ['permitAll'], '/index.gsp': ['permitAll'], '/assets/**': ['permitAll'], '/**/js/**': ['permitAll'], '/**/css/**': ['permitAll'], '/**/images/**': ['permitAll'], '/**/favicon.ico': ['permitAll'] ]
grails.plugin.springsecurity.interceptUrlMap
map in Config.groovy
you'll need to add these URLs too, and likewise when using Requestmap
instances. If you don't use annotations, you must add rules for the login and logout controllers also. You can add Requestmaps manually, or in BootStrap.groovy, for example:for (String url in [ '/', '/index', '/index.gsp', '/**/favicon.ico', '/**/js/**', '/**/css/**', '/**/images/**', '/login', '/login.*', '/login/*', '/logout', '/logout.*', '/logout/*']) { new Requestmap(url: url, configAttribute: 'permitAll').save() }
grails.plugin.springsecurity.interceptUrlMap = [ '/': ['permitAll'], '/index': ['permitAll'], '/index.gsp': ['permitAll'], '/assets/**': ['permitAll'], '/**/js/**': ['permitAll'], '/**/css/**': ['permitAll'], '/**/images/**': ['permitAll'], '/**/favicon.ico': ['permitAll'], '/login/**': ['permitAll'], '/logout/**': ['permitAll'] ]
'/j_spring_security_switch_user': ['ROLE_ADMIN'], '/j_spring_security_exit_user': ['permitAll']
URLs and Authorities
In each approach you configure a mapping for a URL pattern to the role(s) that are required to access those URLs, for example,/admin/user/**
requires ROLE_ADMIN
. In addition, you can combine the role(s) with tokens such as IS_AUTHENTICATED_ANONYMOUSLY, IS_AUTHENTICATED_REMEMBERED, and IS_AUTHENTICATED_FULLY. One or more Voters will process any tokens and enforce a rule based on them:
IS_AUTHENTICATED_ANONYMOUSLY
- signifies that anyone can access this URL. By default the
AnonymousAuthenticationFilter
ensures an 'anonymous'Authentication
with no roles so that every user has an authentication. The token accepts any authentication, even anonymous. IS_AUTHENTICATED_REMEMBERED
- requires the user to be authenticated through a remember-me cookie or an explicit login.
IS_AUTHENTICATED_FULLY
- requires the user to be fully authenticated with an explicit login.
IS_AUTHENTICATED_FULLY
you can implement a security scheme whereby users can check a remember-me checkbox during login and be auto-authenticated each time they return to your site, but must still log in with a password for some parts of the site. For example, allow regular browsing and adding items to a shopping cart with only a cookie, but require an explicit login to check out or view purchase history.For more information on IS_AUTHENTICATED_FULLY
, IS_AUTHENTICATED_REMEMBERED
, and IS_AUTHENTICATED_ANONYMOUSLY
, see the Javadoc for AuthenticatedVoterThe plugin isn't compatible with Grails<g:actionSubmit>
tags. These are used in the autogenerated GSPs that are created for you, and they enable having multiple submit buttons, each with its own action, inside a single form. The problem from the security perspective is that the form posts to the default action of the controller, and Grails figures out the handler action to use based on theaction
attribute of theactionSubmit
tag. So for example you can guard the/person/delete
with a restrictive role, but given this typical edit form:both actions will be allowed if the user has permission to access the<g:form> … <g:actionSubmit class="save" action="update" value='Update' /> <g:actionSubmit class="delete" action="delete" value="'Delete' /> </g:form>/person/index
url, which would often be the case.The workaround is to create separate forms without usingactionSubmit
and explicitly set theaction
on the<g:form>
tags, which will result in form submissions to the expected urls and properly guarded urls.
Comparing the Approaches
Each approach has its advantages and disadvantages. Annotations and theConfig.groovy
Map are less flexible because they are configured once in the code and you can update them only by restarting the application (in prod mode anyway). In practice this limitation is minor, because security mappings for most applications are unlikely to change at runtime.On the other hand, storing Requestmap
entries enables runtime-configurability. This approach gives you a core set of rules populated at application startup that you can edit, add to, and delete as needed. However, it separates the security rules from the application code, which is less convenient than having the rules defined in grails-app/conf/Config.groovy
or in the applicable controllers using annotations.URLs must be mapped in lowercase if you use the Requestmap
or grails-app/conf/Config.groovy
map approaches. For example, if you have a FooBarController, its urls will be of the form /fooBar/list, /fooBar/create, and so on, but these must be mapped as /foobar/, /foobar/list, /foobar/create. This mapping is handled automatically for you if you use annotations.
4.1 Defining Secured Annotations
You can use an@Secured
annotation (either the standard org.springframework.security.access.annotation.Secured
or the plugin's grails.plugin.springsecurity.annotation.Secured
which also works on controller closure actions) in your controllers to configure which roles are required for which actions. To use annotations, specify securityConfigType="Annotation"
, or leave it unspecified because it's the default:grails.plugin.springsecurity.securityConfigType = "Annotation"
package com.mycompany.myappimport grails.plugin.springsecurity.annotation.Securedclass SecureAnnotatedController { @Secured(['ROLE_ADMIN']) def index() { render 'you have ROLE_ADMIN' } @Secured(['ROLE_ADMIN', 'ROLE_SUPERUSER']) def adminEither() { render 'you have ROLE_ADMIN or SUPERUSER' } def anybody() { render 'anyone can see this' // assuming you're not using "strict" mode, otherwise the action is not viewable by anyone } }
ROLE_ADMIN
to see /myapp/secureAnnotated
(or /myapp/secureAnnotated/index
) and be authenticated and have ROLE_ADMIN
or ROLE_SUPERUSER
to see /myapp/secureAnnotated/adminEither
. Any user can access /myapp/secureAnnotated/anybody
if you have disabled "strict" mode (using rejectIfNoRule
), and nobody can access the action by default since it has no access rule configured.In addition, you can define a closure in the annotation which will be called during access checking. The closure must return true
or false
and has all of the methods and properties that are available when using SpEL expressions, since the closure's delegate
is set to a subclass of WebSecurityExpressionRoot
, and also the Spring ApplicationContext
as the ctx
property:@Secured(closure = { assert request assert ctx authentication.name == 'admin1' }) def someMethod() { … }
package com.mycompany.myappimport grails.plugin.springsecurity.annotation.Secured@Secured(['ROLE_ADMIN']) class SecureClassAnnotatedController { def index() { render 'index: you have ROLE_ADMIN' } def otherAction() { render 'otherAction: you have ROLE_ADMIN' } @Secured(['ROLE_SUPERUSER']) def super() { render 'super: you have ROLE_SUPERUSER' } }
ROLE_ADMIN
to see /myapp/secureClassAnnotated
(or /myapp/secureClassAnnotated/index
) or /myapp/secureClassAnnotated/otherAction
. However, you must have ROLE_SUPERUSER
to access /myapp/secureClassAnnotated/super
. The action-scope annotation overrides the class-scope annotation. Note that "strict" mode isn't applicable here since all actions have an access rule defined (either explicitly or inherited from the class-level annotation).Securing RESTful domain classes
Since Grails 2.3, domain classes can be annotated with thegrails.rest.Resource
AST transformation, which will generate
internally a controller with the default CRUD operations.You can also use the @Secured
annotation on such domain classes:@Resource
@Secured('ROLE_ADMIN')
class Thing {
String name
}
package com.mycompany.myappimport grails.plugin.springsecurity.annotation.Securedclass SecureAnnotatedController { @Secured(value = ['ROLE_ADMIN'], httpMethod = 'GET') def create() { … } @Secured(value = ['ROLE_ADMIN'], httpMethod = 'POST') def save() { … } }
create
and save
actions but create
requires a GET request (since it renders the form to create a new instance) and save
requires POST (since it's the action that the form posts to).controllerAnnotations.staticRules
You can also define 'static' mappings that cannot be expressed in the controllers, such as '/**' or for JavaScript, CSS, or image URLs. Use thecontrollerAnnotations.staticRules
property, for example:grails.plugin.springsecurity.controllerAnnotations.staticRules = [ … '/js/admin/**': ['ROLE_ADMIN'], '/someplugin/**': ['ROLE_ADMIN'] ]
SomePluginController
, which has URLs of the form /somePlugin/...
, to ROLE_ADMIN
; annotations are not an option here because you would not edit plugin code for a change like this.When mapping URLs for controllers that are mapped inUrlMappings.groovy
, you need to secure the un-url-mapped URLs. For example if you have a FooBarController that you map to/foo/bar/$action
, you must register that incontrollerAnnotations.staticRules
as/foobar/**
. This is different than the mapping you would use for the other two approaches and is necessary becausecontrollerAnnotations.staticRules
entries are treated as if they were annotations on the corresponding controller.
4.2 Simple Map in Config.groovy
To use theConfig.groovy
Map to secure URLs, first specify securityConfigType="InterceptUrlMap"
:grails.plugin.springsecurity.securityConfigType = "InterceptUrlMap"
Config.groovy
:grails.plugin.springsecurity.interceptUrlMap = [ '/': ['permitAll'], '/index': ['permitAll'], '/index.gsp': ['permitAll'], '/assets/**': ['permitAll'], '/**/js/**': ['permitAll'], '/**/css/**': ['permitAll'], '/**/images/**': ['permitAll'], '/**/favicon.ico': ['permitAll'], '/login/**': ['permitAll'], '/logout/**': ['permitAll'], '/secure/**': ['ROLE_ADMIN'], '/finance/**': ['ROLE_FINANCE', 'isFullyAuthenticated()'], ]
'/secure/**': ['ROLE_ADMIN', 'ROLE_SUPERUSER'], '/secure/reallysecure/**': ['ROLE_SUPERUSER']
/secure/reallysecure/list
to a user with ROLE_SUPERUSER
since the first URL pattern matches, so the second would be ignored. The correct mapping would be'/secure/reallysecure/**': ['ROLE_SUPERUSER'] '/secure/**': ['ROLE_ADMIN', 'ROLE_SUPERUSER'],
4.3 Requestmap Instances Stored in the Database
With this approach you use theRequestmap
domain class to store mapping entries in the database. Requestmap
has a url
property that contains the secured URL pattern and a configAttribute
property containing a comma-delimited list of required roles and/or tokens such as IS_AUTHENTICATED_FULLY
, IS_AUTHENTICATED_REMEMBERED
, and IS_AUTHENTICATED_ANONYMOUSLY
.To use Requestmap
entries, specify securityConfigType="Requestmap"
:grails.plugin.springsecurity.securityConfigType = "Requestmap"
Requestmap
entries as you create entries in any Grails domain class:for (String url in [ '/', '/index', '/index.gsp', '/**/favicon.ico', '/assets/**', '/**/js/**', '/**/css/**', '/**/images/**', '/login', '/login.*', '/login/*', '/logout', '/logout.*', '/logout/*']) { new Requestmap(url: url, configAttribute: 'permitAll').save() }new Requestmap(url: '/profile/**', configAttribute: 'ROLE_USER').save() new Requestmap(url: '/admin/**', configAttribute: 'ROLE_ADMIN').save() new Requestmap(url: '/admin/role/**', configAttribute: 'ROLE_SUPERVISOR').save() new Requestmap(url: '/admin/user/**', configAttribute: 'ROLE_ADMIN,ROLE_SUPERVISOR').save() new Requestmap(url: '/j_spring_security_switch_user', configAttribute: 'ROLE_SWITCH_USER,isFullyAuthenticated()').save()
configAttribute
value can have a single value or have multiple comma-delimited values. In this example only users with ROLE_ADMIN
or ROLE_SUPERVISOR
can access /admin/user/**
urls, and only users with ROLE_SWITCH_USER
can access the switch-user url (/j_spring_security_switch_user
) and in addition must be authenticated fully, i.e. not using a remember-me cookie. Note that when specifying multiple roles, the user must have at least one of them, but when combining IS_AUTHENTICATED_FULLY
, IS_AUTHENTICATED_REMEMBERED
, or IS_AUTHENTICATED_ANONYMOUSLY
(or their corresponding SpEL expressions) with one or more roles means the user must have one of the roles and satisty the IS_AUTHENTICATED
rule.Unlike the Config.groovy Map approach, you do not need to revise the Requestmap
entry order because the plugin calculates the most specific rule that applies to the current request.Requestmap Cache
Requestmap
entries are cached for performance, but caching affects runtime configurability. If you create, edit, or delete an instance, the cache must be flushed and repopulated to be consistent with the database. You can call springSecurityService.clearCachedRequestmaps()
to do this. For example, if you create a RequestmapController
the save
action should look like this (and the update and delete actions should similarly call clearCachedRequestmaps()
):class RequestmapController { def springSecurityService ... def save() { def requestmapInstance = new Requestmap(params) if (!requestmapInstance.save(flush: true)) { render view: 'create', model: [requestmapInstance: requestmapInstance] return } springSecurityService.clearCachedRequestmaps() flash.message = "${message(code: 'default.created.message', args: [message(code: 'requestmap.label', default: 'Requestmap'), requestmapInstance.id])}" redirect action: 'show', id: requestmapInstance.id } }
4.4 Using Expressions to Create Descriptive, Fine-Grained Rules
Spring Security uses the Spring Expression Language (SpEL), which allows you to declare the rules for guarding URLs more descriptively than does 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 (for example,IS_AUTHENTICATED_FULLY
), with Spring Security's expression support, you can instead use the embedded scripting language to define simple or complex access rules.You can use expressions with any of the previously described approaches to securing application URLs. For example, consider this annotated controller:package com.yourcompany.yourappimport grails.plugin.springsecurity.annotation.Securedclass SecureController { @Secured(["hasRole('ROLE_ADMIN')"]) def someAction() { … } @Secured(["authentication.name == 'ralph'"]) def someOtherAction() { … } }
someAction
requires ROLE_ADMIN
, and someOtherAction
requires that the user be logged in with username 'ralph'.The corresponding Requestmap
URLs would benew Requestmap(url: "/secure/someAction", configAttribute: "hasRole('ROLE_ADMIN')").save()new Requestmap(url: "/secure/someOtherAction", configAttribute: "authentication.name == 'ralph'").save()
grails.plugin.springsecurity.interceptUrlMap = [ '/secure/someAction': ["hasRole('ROLE_ADMIN')"], '/secure/someOtherAction': ["authentication.name == 'ralph'"] ]
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 |
request | the HTTP request, allowing expressions such as "isFullyAuthenticated() or request.getMethod().equals('OPTIONS')" |
hasIpAddress
. However, you may find it more convenient to separate IP restrictions from role restrictions by using the IP address filter.To help you migrate traditional configurations to expressions, this table compares various configurations and their corresponding expressions:Traditional Config | Expression |
---|---|
ROLE_ADMIN | hasRole('ROLE_ADMIN') |
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 | isAuthenticated() or isRememberMe() |
IS_AUTHENTICATED_FULLY | isFullyAuthenticated() |