(Quick Reference)

5 Configuring Request Mappings to Secure URLs - Reference Documentation

Authors: Burt Beckwith, Beverley Talbott

Version: 1.2.7.3

5 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

Most applications are mostly public, with some pages only accessible to authenticated users with various roles. In this case, it makes more 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 configuration.

To use the pessimistic approach, add this line to grails-app/conf/Config.groovy:

grails.plugins.springsecurity.rejectIfNoRule = true

Any requested URL that does not have a corresponding rule will be denied to all users.

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.

5.1 Defining Secured Annotations

You can use an @Secured annotation (either the standard org.springframework.security.access.annotation.Secured or the plugin's grails.plugins.springsecurity.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.plugins.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.plugins.springsecurity.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' } }

you need to 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.

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.plugins.springsecurity.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.

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.plugins.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.

5.2 Simple Map in Config.groovy

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

grails.plugins.springsecurity.securityConfigType = "InterceptUrlMap"

Define a Map in Config.groovy:

grails.plugins.springsecurity.interceptUrlMap = [
   '/secure/**':    ['ROLE_ADMIN'],
   '/finance/**':   ['ROLE_FINANCE', 'IS_AUTHENTICATED_FULLY'],
   '/js/**':        ['IS_AUTHENTICATED_ANONYMOUSLY'],
   '/css/**':       ['IS_AUTHENTICATED_ANONYMOUSLY'],
   '/images/**':    ['IS_AUTHENTICATED_ANONYMOUSLY'],
   '/*':            ['IS_AUTHENTICATED_ANONYMOUSLY'],
   '/login/**':     ['IS_AUTHENTICATED_ANONYMOUSLY'],
   '/logout/**':    ['IS_AUTHENTICATED_ANONYMOUSLY']
]

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

5.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.plugins.springsecurity.securityConfigType = "Requestmap"

You create Requestmap entries as you create entries in any Grails domain class:

new Requestmap(url: '/js/**', configAttribute: 'IS_AUTHENTICATED_ANONYMOUSLY').save()
new Requestmap(url: '/css/**', configAttribute: 'IS_AUTHENTICATED_ANONYMOUSLY').save()
new Requestmap(url: '/images/**', configAttribute: 'IS_AUTHENTICATED_ANONYMOUSLY').save()
new Requestmap(url: '/login/**', configAttribute: 'IS_AUTHENTICATED_ANONYMOUSLY').save()
new Requestmap(url: '/logout/**', configAttribute: 'IS_AUTHENTICATED_ANONYMOUSLY').save()
new Requestmap(url: '/*', configAttribute: 'IS_AUTHENTICATED_ANONYMOUSLY').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,IS_AUTHENTICATED_FULLY').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 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 } }

5.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.plugins.springsecurity.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.plugins.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

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_USER')
ROLE_USER,ROLE_ADMINhasAnyRole('ROLE_USER','ROLE_ADMIN')
ROLE_ADMIN,IS_AUTHENTICATED_FULLYhasRole('ROLE_ADMIN') and isFullyAuthenticated()
IS_AUTHENTICATED_ANONYMOUSLYpermitAll
IS_AUTHENTICATED_REMEMBEREDisAnonymous() or isRememberMe()
IS_AUTHENTICATED_FULLYisFullyAuthenticated()