Last updated by dhpye 4 years ago
There's a potential security risk due to the way Grails handles URLs with extensions that you should be aware of. If someone requests a URL that ends in a '.' - e.g. /admin/user/list. - a security mapping of
/admin/user/list=ROLE_ADMIN
will allow unauthenticated users to access the action since the URL doesn't match the rule, but will have the dot stripped off for rendering.


This only applies to single controller actions and the 'index' action (if it exists), and it only affects the static string approach and Requestmap approach - the annotation handler automatically handles this for you.

The fix for single urls is to add a * to the end of the action name:

/admin/user/list*=ROLE_ADMIN

and the fix for controllers that have a mapping for all actions is to double-map them to guard the index action:

/admin/user*=ROLE_ADMIN
/admin/user/**=ROLE_ADMIN

This will be fixed in Grails 1.1 but until then you should either use annotations or double-map these entries.

Securing URLs

There are three ways to configure the request mappings to secure application URLs using the Spring Security plugin. Additionally, @Secured annotations can be used on service methods and work is being done to integrate ACLs, but these won't be discussed here.


The goal is to create a mapping of URLs and URL patterns to the roles required to access those URLs.

requestMapString

Typically this is done by defining a string entry in SecurityConfig.groovy, following the form:

requestMapString = '''CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON 
			  PATTERN_TYPE_APACHE_ANT 
			  /login/**=IS_AUTHENTICATED_ANONYMOUSLY 
			  /admin/**=ROLE_USER
                          /book/test/**=IS_AUTHENTICATED_FULLY
                          /book/**=ROLE_SUPERVISOR
			'''
To enable this configuration, set 'useRequestMapDomainClass' to false in SecurityConfig.groovy to enable fallback to the requestMapString configuration.


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 only ROLE_ADMIN 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

Note that in the example above, the "/book/test/**" rule appears before "/book/**", but the attributes are of different types. This rule combination specifies that to access any BookController URL you need ROLE_SUPERVISOR, but in addition, to access test/** you must have authenticated with a password and not using a cookie - it will redirect to the login page if necessary.

Requestmap

Another supported mechanism stores mapping entries in the database, using the Requestmap domain class. Requestmap has a 'url' property which 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 this approach add this to SecurityConfig.groovy:

useRequestMapDomainClass = true

Creation of Requestmap entries is the same as for any Grails domain class:

new Requestmap(url: '/login/**', configAttribute: 'IS_AUTHENTICATED_ANONYMOUSLY').save()
new Requestmap(url: '/admin/**', configAttribute: 'ROLE_USER').save()
new Requestmap(url: '/book/test/**', configAttribute: 'IS_AUTHENTICATED_FULLY').save()
new Requestmap(url: '/book/**', configAttribute: 'ROLE_SUPERVISOR').save()

Unlike the static string approach above, you don't need to worry about Requestmap entry order since the plugin calculates the most specific rule that applies to the current request.

Requestmap entries are cached for performance, but this has an impact on runtime configurability. If you create, edit, or delete an instance, the cache must be flushed and repopulated to be consistent with the database. If you use the generated RequestmapController, this is handled for you - "authenticateService.clearCachedRequestmaps()" is called where appropriate. But if you have custom code, you'll need to follow this convention and flush the cache after any update.

If you run the generate-manager script (described here) you can use the CRUD GSPs generated at grails-app/views/requestmap to manage Requestmap entries, or you can create your own admin UI.

Annotations

A newer mechanism uses annotations to define what roles are required for various URLs. 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:

import org.codehaus.groovy.grails.plugins.springsecurity.Secured

class SecureAnnotatedController {

@Secured(['ROLE_ADMIN']) def index = { render 'you have ROLE_ADMIN' }

@Secured(['ROLE_ADMIN', 'ROLE_ADMIN2']) def adminEither = { render 'you have ROLE_ADMIN or ROLE_ADMIN2' }

def anybody = { render 'anyone can see this' } }

you'd need to be authenticated and have ROLE_ADMIN to see /yourapp/secureAnnotated (or /yourapp/secureAnnotated/index) and be authenticated and have ROLE_ADMIN or ROLE_ADMIN2 to see /yourapp/secureAnnotated/adminEither. Any user can access /yourapp/secureAnnotated/anybody.

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

import org.codehaus.groovy.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_ADMIN2']) def admin2 = { render 'admin2: you have ROLE_ADMIN2' } }

Here you'd need to be authenticated and have ROLE_ADMIN to see /yourapp/secureClassAnnotated (or /yourapp/secureClassAnnotated/index) or /yourapp/secureClassAnnotated/otherAction. However you must have ROLE_ADMIN2 to access /yourapp/secureClassAnnotated/admin2 - the action-scope annotation overrides the class-scope annotation.

To use this approach add this to SecurityConfig.groovy:

useRequestMapDomainClass = false
useControllerAnnotations = true

You can also define 'static' mappings that cannot be expressed in the controllers, such as '/**' or for .js/.css/image urls. Use the 'controllerAnnotationStaticRules' property, e.g.

controllerAnnotationStaticRules = ['/js/admin/**': ['ROLE_ADMIN']]

In addition, you can use a pessimistic 'lockdown' approach if you like. Most applications are mostly public, with some pages only accessible to authenticated users with various roles. Here it makes more sense to leave URLs open by default and restrict access one a case-by-case basis. But if your app is primarily secure, you can deny access to all URLs that don't have an applicable URL-Role configuration.

To use the pessimistic approach, add this to SecurityConfig.groovy:

controllerAnnotationsRejectIfNoRule = true

Like the Requestmap approach above, you don't need to worry about the order of the URL rules since the plugin calculates the most specific rule that applies to the current request.

Advantages/disadvantages:

Each approach has its advantages and disadvantages. The static string is less flexible since it's configured once in the code and can only be updated by restarting the application. In practice this isn't that serious a concern since for most applications, security mappings are unlikely to change at runtime.

If you want runtime-configurability then storing Requestmap entries enables this. This allows you to have a core set of rules populated at application startup and to edit, add, and delete them whenever you like. But it separates the security rules from the application code, which is less convenient than having the rules defined in a static string in SecurityConfig.groovy or in the applicable controllers using annotations.

Some notes:

  • to understand the meaning of IS_AUTHENTICATED_FULLY, IS_AUTHENTICATED_REMEMBERED, and IS_AUTHENTICATED_ANONYMOUSLY, see the Javadoc for AuthenticatedVoter
  • URLs must be mapped in lowercase if using the Requestmap or requestMapString approaches, so for example if you have a FooBarController, its urls will be of the form /fooBar/list, /fooBar/create, etc. but these must be mapped as /foobar/, /foobar/list, /foobar/create. This is handled automatically for you if you use annotations.