(Quick Reference)

4 Configuring Request Mappings to Secure URLs - Reference Documentation

Authors: Burt Beckwith, Beverley Talbott

Version: 2.0.0

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.

You can only use one method at a time. You configure it with the 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. If rejectIfNoRule 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

and to reject with the error 500 page, use these (optionally omit rejectPublicInvocations since it defaults to true ):

grails.plugin.springsecurity.rejectIfNoRule = false
grails.plugin.springsecurity.fii.rejectPublicInvocations = true

Note that if you set 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']
]

This is needed when using annotations; if you use the 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()
}

The analogous interceptUrlMap settings would be:

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']
]

In addition, when you enable the switch-user feature, you'll have to specify access rules for the associated URLs, e.g.

'/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.

With 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 AuthenticatedVoter

The 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 the action attribute of the actionSubmit tag. So for example you can guard the /person/delete with a restrictive role, but given this typical edit form:

<g:form>
   …
   <g:actionSubmit class="save" action="update"
                   value='Update' />

<g:actionSubmit class="delete" action="delete" value="'Delete' /> </g:form>

both actions will be allowed if the user has permission to access the /person/index url, which would often be the case.

The workaround is to create separate forms without using actionSubmit and explicitly set the action 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 the Config.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"

You can define the annotation at the class level, meaning that the specified roles are required for all actions, or at the action level, or both. If the class and an action are annotated then the action annotation values will be used since they're more specific.

For example, given this controller:

package com.mycompany.myapp

import grails.plugin.springsecurity.annotation.Secured

class 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 } }

you must be authenticated and have 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() {
   …
}

Often most actions in a controller require similar access rules, so you can also define annotations at the class level:

package com.mycompany.myapp

import 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' } }

Here you need to be authenticated and have 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 the grails.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
}

Additionally, you can specify the HTTP method that is required in each annotation for the access rule, e.g.

package com.mycompany.myapp

import grails.plugin.springsecurity.annotation.Secured

class SecureAnnotatedController {

@Secured(value = ['ROLE_ADMIN'], httpMethod = 'GET') def create() { … }

@Secured(value = ['ROLE_ADMIN'], httpMethod = 'POST') def save() { … } }

Here you must have ROLE_ADMIN for both the 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 the controllerAnnotations.staticRules property, for example:

grails.plugin.springsecurity.controllerAnnotations.staticRules = [
   …
   '/js/admin/**': ['ROLE_ADMIN'],
   '/someplugin/**': ['ROLE_ADMIN']
]

This example maps all URLs associated with 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 in UrlMappings.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 in controllerAnnotations.staticRules as /foobar/**. This is different than the mapping you would use for the other two approaches and is necessary because controllerAnnotations.staticRules entries are treated as if they were annotations on the corresponding controller.

4.2 Simple Map in Config.groovy

To use the Config.groovy Map to secure URLs, first specify securityConfigType="InterceptUrlMap":

grails.plugin.springsecurity.securityConfigType = "InterceptUrlMap"

Define a Map in 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()'],
]

When using this approach, make sure that you order the rules correctly. The first applicable rule is used, so for example if you have a controller that has one set of rules but an action that has stricter access rules, e.g.

'/secure/**':              ['ROLE_ADMIN', 'ROLE_SUPERUSER'],
'/secure/reallysecure/**': ['ROLE_SUPERUSER']

then this would fail - it wouldn't restrict access to /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 the Requestmap 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"

You create 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()

The 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.yourapp

import grails.plugin.springsecurity.annotation.Secured

class SecureController {

@Secured(["hasRole('ROLE_ADMIN')"]) def someAction() { … }

@Secured(["authentication.name == 'ralph'"]) def someOtherAction() { … } }

In this example, someAction requires ROLE_ADMIN, and someOtherAction requires that the user be logged in with username 'ralph'.

The corresponding Requestmap URLs would be

new Requestmap(url: "/secure/someAction",
               configAttribute: "hasRole('ROLE_ADMIN')").save()

new Requestmap(url: "/secure/someOtherAction", configAttribute: "authentication.name == 'ralph'").save()

and the corresponding static mappings would be

grails.plugin.springsecurity.interceptUrlMap = [
   '/secure/someAction':      ["hasRole('ROLE_ADMIN')"],
   '/secure/someOtherAction': ["authentication.name == 'ralph'"]
]

The Spring Security docs have a table listing the standard expressions, which is copied here for reference:

ExpressionDescription
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)
principalAllows direct access to the principal object representing the current user
authenticationAllows direct access to the current Authentication object obtained from the SecurityContext
permitAllAlways evaluates to true
denyAllAlways 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
requestthe HTTP request, allowing expressions such as "isFullyAuthenticated() or request.getMethod().equals('OPTIONS')"

In addition, you can use a web-specific expression 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 ConfigExpression
ROLE_ADMINhasRole('ROLE_ADMIN')
ROLE_USER,ROLE_ADMINhasAnyRole('ROLE_USER','ROLE_ADMIN')
ROLE_ADMIN,IS_AUTHENTICATED_FULLYhasRole('ROLE_ADMIN') and isFullyAuthenticated()
IS_AUTHENTICATED_ANONYMOUSLYpermitAll
IS_AUTHENTICATED_REMEMBEREDisAuthenticated() or isRememberMe()
IS_AUTHENTICATED_FULLYisFullyAuthenticated()