Archive for the Category 'security'

ACLs in the Grails Spring Security Plugin

Sunday, December 27th, 2009

It has taken way too long, but the Grails Spring Security plugin finally has ACL support. It's not officially available yet, but people have offered to beta test an early version of the plugin with ACLs, so you can download that here and report any issues back. Once it's stable I'll do an official release.

History

Stephan February did the first work adding ACL support to the plugin. Unfortunately at the time the plugin was based on Acegi 1.0.x and I had just converted it to use Spring Security 2.0. No one did the work to convert the ACL support to the new package layout and approach, so this wasn't used.

This is a frequently requested feature, so I created a feature request as a TODO item for myself. I found some time to work on this over the summer and created an initial GORM-based implementation (the standard Spring Security implementation uses JDBC). I was fortunate to be able to use this at a client project at InnoCentive which helped to flesh out the ideas and identify a few issues.

Around the same time, Phillip Merensky mentioned on the mailing list that he was working on an implementation. He wrote about his approach here and attached his version of the plugin to the JIRA issue. Phillip's work was very helpful; I've merged his version with mine for the current implementation.

Working with ACLs in Spring Security is complex but it will be easier to understand with a sample application.

Test Application

Create a test application

grails create-app acltest
cd acltest

Download the plugin with ACL support here and install it:

grails install-plugin /path/to/grails-acegi-0.5.2-ACL.zip

As with any application using the plugin, you need to run the create-auth-domains script, plus generate-manager if you want the generated GSPs and generate-registration if you want basic registration support:

grails create-auth-domains acltest.User acltest.Role acltest.Requestmap
grails generate-manager
grails generate-registration

The ACL support uses domain classes but to allow customizing the domain classes (e.g. to enable Hibernate 2nd-level caching) there's a script that copies the domain classes into your application:

grails create-acl-domains

The script takes no parameters since the package and names aren't configurable - the plugin code imports the domain classes.

Next, switch from using Requestmap entries in the database to using annotated controllers:

  • delete grails-app/domain/acltest/Requestmap.groovy
  • delete grails-app/controllers/RequestmapController.groovy
  • delete the grails-app/views/requestmap directory and its GSPs
  • delete Requestmap import from grails-app/controllers/RoleController.groovy
  • in grails-app/conf/SecurityConfig.groovy, disable requestmaps (useRequestMapDomainClass = false) and enable annotations (useControllerAnnotations = true), and remove the requestMapClass property:
    security {
       active = true

       loginUserDomainClass = 'acltest.User'
       authorityDomainClass = 'acltest.Role'

       useRequestMapDomainClass = false
       useControllerAnnotations = true
    }

To enable ACL processing, set the useAcl attribute to true:

security {
   active = true

   loginUserDomainClass = 'acltest.User'
   authorityDomainClass = 'acltest.Role'

   useRequestMapDomainClass = false
   useControllerAnnotations = true

   useAcl = true
}

We'll need a domain class to test with, so create a Report domain class:

grails create-domain-class acltest.Report

and add a name property for testing:

package acltest

class Report {
   String name
}

Working with ACLs

Probably the most important interface for ACLs is Permission. You can implement the interface yourself, but BasePermission has READ, WRITE, CREATE, DELETE, and ADMINISTRATION instances that should be sufficient for your needs.

The plugin provides a new service, AclUtilService, to grant and revoke permissions, and to check if permissions are granted. The service methods are:

  • void addPermission(object, recipient, Permission permission) grants the specified permission to the recipient (either the login name or an Authentication) for the specified instance
  • void addPermission(Class< ?> domainClass, long id, recipient, Permission permission) grants the specified permission to the recipient (either the login name or an Authentication) for the specified instance; use this overload to avoid loading the instance
  • void deletePermission(object, recipient, Permission permission) removes the grant of the specified permission from the recipient (either the login name or an Authentication) for the specified instance
  • void deletePermission(Class< ?> domainClass, long id, recipient, Permission permission) removes the grant of the specified permission from the recipient (either the login name or an Authentication) for the specified instance; use this overload to avoid loading the instance
  • boolean hasPermission(Authentication authentication, domainObject, Permission permission) checks if the authentication has a grant of the specified permission for the specified instance
  • boolean hasPermission(Authentication authentication, domainObject, Permission[] permissions) checks if the authentication has a grant of any of the specified permissions for the specified instance; the first one that is found is used, so the order of the array matters

Creating, editing, or deleting permissions requires an authenticated user. The default required role is ROLE_ADMIN for all actions, but this can be configured in SecurityConfig.groovy. Change the acl.authority.changeOwnership property to change who can call OwnershipAcl.setOwner(). Change the acl.authority.modifyAuditingDetails property to change who can call AuditableAcl.updateAuditing(). And change acl.authority.changeAclDetails to change who can call MutableAcl.deleteAce(), MutableAcl.insertAce(), MutableAcl.setEntriesInheriting(), MutableAcl.setParent(), or MutableAcl.updateAce().

You'll probably want to create an admin UI that uses AclUtilService and is aware of your secured domain classes and business rules.

Configuration

Configuring ACL support happens in two places; you configure Voters that have one or more associated permissions and a domain class (which can be an abstract base class), and you configure which service methods use which voters. Often there will be a 1-1 relationship between these but since they're separate, you can re-use the voters for multiple service methods. And you may not even need custom voters; if you only want to secure methods with roles, or if you only need return value checking, then you wouldn't configure any voters, but you'd still configure method restrictions.

There are two types of ACL checks; method return value and method parameter. The plugin creates two voters for return value checks, one for single values (AFTER_ACL_READ) and one for collections (AFTER_ACL_COLLECTION_READ). Each requires that the authenticated user have BasePermission.READ. An optimization would be to allow access to admins (who have been granted BasePermission.ADMINISTRATION); to configure this, redefine the beans in grails-app/conf/spring/resources.groovy:

beans = {
   afterAclCollectionRead(AclEntryAfterInvocationCollectionFilteringProvider,
        ref('aclService'),
        [BasePermission.READ, BasePermission.ADMINISTRATION])

   afterAclRead(AclEntryAfterInvocationProvider,
        ref('aclService'),
        [BasePermission.READ, BasePermission.ADMINISTRATION])
}

Voters for method parameter checks (the first parameter of the specified type or a subclass is checked) can be configured either in SecurityConfig.groovy or in domain class annotations. Putting the configuration in SecurityConfig.groovy keeps everything in one place, whereas the annotations let you put the declarations where they apply, so they're self-documenting. Use whichever approach you prefer.

To configure them in SecurityConfig.groovy, use the acl.voters property, e.g.

import org.springframework.security.acls.domain.BasePermission
import acltest.Report

security {
   ...
   useAcl = true

   acl.voters = [

      aclReportWriteVoter: [
           configAttribute: 'ACL_REPORT_WRITE',
           permissions: [BasePermission.ADMINISTRATION,
                         BasePermission.WRITE],
           domainObjectClass: Report],
   
        aclReportDeleteVoter: [
           configAttribute: 'ACL_REPORT_DELETE',
           permissions: [BasePermission.ADMINISTRATION,
                         BasePermission.DELETE],
           domainObjectClass: Report]
   ]
}

which creates a 'write' voter and a 'delete' voter. The equivalent annotations would be:

package acltest

import org.codehaus.groovy.grails.plugins.springsecurity.acl.AclVoter
import org.codehaus.groovy.grails.plugins.springsecurity.acl.AclVoters

@AclVoters([
   @AclVoter(name='aclReportWriteVoter',
             configAttribute='ACL_REPORT_WRITE',
             permissions=['ADMINISTRATION', 'WRITE']),
   @AclVoter(name='aclReportDeleteVoter',
             configAttribute='ACL_REPORT_DELETE',
             permissions=['ADMINISTRATION', 'DELETE'])
])
class Report {
   String name
}

Note that since you cannot use an annotation more than once, in a case like this where there can be multiple voter annotations for a domain class they need to be defined as attributes of a containing annotation (AclVoters). If you only have a single voter then you can annotate the class with that and omit the containing annotation.

The voter configuration should be fairly clear; there's a name parameter that's used as the Spring bean name (so it must be unique), a configAttribute parameter that's arbitrary but typically uses a naming convention where it starts with 'ACL_', and one or more permissions. The one limitation of annotations over the static configuration is that annotations cannot have Permissions as parameters, so Strings are used instead. This limits you to naming fields of the BasePermission class. If you have custom permission classes you'll need to use the static configuration.

Securing Service Methods

As with voters, there are two ways to define the access rules for service methods. You can define a static springSecurityACL property with configuration options, or annotate the class and/or individual methods.

Let's create a service to test ACLs:

grails create-service acltest.Report

and add some methods that work with Reports:

package acltest

class ReportService {

   boolean transactional = true

   Report getReport(long id) {
      Report.get(id)
   }

   Report createReport(params) {
      Report report = new Report(params)
      report.save()
      report
   }

   List getAllReports(params = [:]) { Report.list(params) }

   String getReportName(long id) { Report.get(id).name }

   Report updateReport(Report report, params) {
      report.properties = params
      if (!report.hasErrors()) {
         report.save()
      }
      report
   }

   void deleteReport(Report report) {
      report.delete()
   }
}

To configure the rules in one place, add a springSecurityACL property:

static springSecurityACL = [
   getReportName: ['ROLE_USER', 'ROLE_ADMIN'],
   getAllReports: ['ROLE_USER', 'AFTER_ACL_COLLECTION_READ'],
   getReport: ['ROLE_USER', 'AFTER_ACL_READ'],
   updateReport: ['ACL_REPORT_WRITE'],
   deleteReport: ['ACL_REPORT_DELETE']
]

and the equivalent annotated version would be:

package acltest

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

class ReportService {

   boolean transactional = true

   @Secured(['ROLE_USER', 'AFTER_ACL_READ'])
   Report getReport(long id) {
      Report.get(id)
   }

   Report createReport(params) {
      Report report = new Report(params)
      report.save()
      report
   }

   @Secured(['ROLE_USER', 'AFTER_ACL_COLLECTION_READ'])
   List getAllReports(params = [:]) { Report.list(params) }

   @Secured(['ROLE_USER', 'ROLE_ADMIN'])
   String getReportName(long id) { Report.get(id).name }

   @Secured(['ACL_REPORT_WRITE'])
   Report updateReport(Report report, params) {
      report.properties = params
      if (!report.hasErrors()) {
         report.save()
      }
      report
   }

   @Secured(['ACL_REPORT_DELETE'])
   void deleteReport(Report report) {
      report.delete()
   }
}

The configuration specifies these rules:

  • getReportName requires that the authenticated user have either ROLE_USER or ROLE_ADMIN (but no ACL rules)
  • getAllReports requires ROLE_USER and will have elements removed from the returned List that the user doesn't have an ACL grant for (thanks to AFTER_ACL_COLLECTION_READ); the user must have one of the permissions defined in the afterAclCollectionRead bean (by default BasePermission.READ) for each element in the list; elements that don't have access granted will be removed
  • getReport requires ROLE_USER and will be denied (thanks to AFTER_ACL_READ) unless the user has one of the permissions defined in the afterAclRead bean (by default BasePermission.READ).
  • updateReport has no role restrictions but must satisfy the requirements of the aclReportWriteVoter voter (which has the ACL_REPORT_WRITE config attribute), i.e. BasePermission.ADMINISTRATION or BasePermission.WRITE
  • deleteReport has no role restrictions but must satisfy the requirements of the aclReportDeleteVoter voter (which has the ACL_REPORT_DELETE config attribute), i.e. BasePermission.ADMINISTRATION or BasePermission.DELETE
  • createReport has no restrictions

To test this out we'll need some users; create those and their grants in BootStrap.groovy:

import org.springframework.security.GrantedAuthority
import org.springframework.security.GrantedAuthorityImpl
import org.springframework.security.acls.domain.BasePermission
import org.springframework.security.context.SecurityContextHolder as SCH
import org.springframework.security.providers.UsernamePasswordAuthenticationToken

import acltest.Report
import acltest.Role
import acltest.User

class BootStrap {

   def aclUtilService
   def passwordEncoder
   def sessionFactory

   def init = { servletContext ->
      createUsers()
      createReports()
      createGrants()

      sessionFactory.currentSession.flush()
   }

   private void createUsers() {
      def adminRole = new Role(description: 'Admin', authority: 'ROLE_ADMIN').save()
      def admin = new User(username: 'admin', userRealName: 'admin',
            passwd: passwordEncoder.encodePassword('admin', null),
            enabled: true, email: 'admin@admin.com').save()
      adminRole.addToPeople admin

      def userRole = new Role(description: 'User', authority: 'ROLE_USER').save()
      def user1 = new User(username: 'user1', userRealName: 'user1',
            passwd: passwordEncoder.encodePassword('user1', null),
            enabled: true, email: 'user1@user.com').save()
      userRole.addToPeople user1

      def user2 = new User(username: 'user2', userRealName: 'user2',
            passwd: passwordEncoder.encodePassword('user2', null),
            enabled: true, email: 'user2@user.com').save()
      userRole.addToPeople user2
   }

   private void createReports() {
      (1..10).each { new Report(name: "report $it").save() }
   }

   private void createGrants() {

      loginAsAdmin()

      try {
         // user1 can see reports 1-4
         def user = User.findByUsername('user1')   
         (1..4).each {
            def report = Report.findByName("report $it")
            aclUtilService.addPermission(report,
                  user.username, BasePermission.READ)
         }
         // and can edit #3
         aclUtilService.addPermission(Report.findByName('report 3'),
               user.username, BasePermission.WRITE)
         // and edit and delete #4
         aclUtilService.addPermission(Report.findByName('report 4'),
               user.username, BasePermission.WRITE)
         aclUtilService.addPermission(Report.findByName('report 4'),
               user.username, BasePermission.DELETE)

         // user2 can see reports 5, 10
         user = User.findByUsername('user2')   
         [5, 10].each {
            def report = Report.findByName("report $it")
            aclUtilService.addPermission(report,
                  user.username, BasePermission.READ)
         }
      }
      finally {
         SCH.clearContext()
      }
   }

   // have to be authenticated as an admin to create ACLs
   private void loginAsAdmin() {
      SCH.context.authentication = new UsernamePasswordAuthenticationToken(
            'admin', 'password',
            [new GrantedAuthorityImpl('ROLE_ADMIN')] as GrantedAuthority[])
   }

   def destroy = {}
}

And to have a UI to test with, let's create a Report controller and GSPs:

grails generate-all acltest.Report

But to use the controller, it will have to be reworked to use ReportService. It's a good idea to put all create/edit/delete code in a transactional service, but in this case we need to move all database access to the service to ensure that appropriate access checks are made:

package acltest

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

import org.springframework.dao.DataIntegrityViolationException

@Secured(['ROLE_ADMIN', 'ROLE_USER'])
class ReportController {

   static allowedMethods = [delete: 'POST', save: 'POST', update: 'POST']

   static defaultAction = 'list'

   def reportService

   def list = {
      params.max = Math.min(params.max ? params.max.toInteger() : 10, 100)

      [reportInstanceList: reportService.getAllReports(params),
       reportInstanceTotal: Report.count()]
   }

   def show = {
      def reportInstance = reportService.getReport(params.id?.toLong())
      if (!reportInstance) {
         flash.message = "Report not found with id $params.id"
         redirect action: list
         return
      }
      [reportInstance: reportInstance]
   }

   def delete = {
      def reportInstance = reportService.getReport(params.id?.toLong())
      if (!reportInstance) {
         flash.message = "Report not found with id $params.id"
         redirect action: list
         return
      }

      try {
         reportService.deleteReport(reportInstance)
         flash.message = "Report $params.id deleted"
         redirect action: list
      }
      catch (DataIntegrityViolationException e) {
         flash.message = "Report $params.id could not be deleted"
         redirect action: show, id: params.id
      }
   }

   def edit = {
      def reportInstance = reportService.getReport(params.id?.toLong())
      if (!reportInstance) {
         flash.message = "Report not found with id $params.id"
         redirect action: list
         return
      }

      [reportInstance: reportInstance]
   }

   def update = {
      def reportInstance = reportService.getReport(params.id?.toLong())
      if (!reportInstance) {
         flash.message = "Report not found with id $params.id"
         redirect action: list
         return
      }

      if (params.version) {
         long version = params.version.toLong()
         if (reportInstance.version> version) {
            reportInstance.errors.rejectValue('version',
               'report.optimistic.locking.failure',
               'Another user has updated this Report while you were editing.')
            render view:'edit',model: [reportInstance: reportInstance]
            return
         }
      }

      reportService.updateReport(reportInstance, params)
      if (reportInstance.hasErrors()) {
         render view: 'edit', model: [reportInstance: reportInstance]
         return
      }

      flash.message = "Report $params.id updated"
      redirect action: show, id: reportInstance.id
   }

   def create = {
      [reportInstance: new Report(params)]
   }

   def save = {

      def reportInstance = reportService.createReport(params)
      if (reportInstance.hasErrors()) {
         render view: 'create', model: [reportInstance: reportInstance]
         return
      }

      flash.message = "Report $reportInstance.id created"
      redirect action: show, id: reportInstance.id
   }
}

Note that the controller is annotated to require either ROLE_USER or ROLE_ADMIN. Since services have nothing to do with HTTP, when access is blocked you cannot be redirected to the login page as when you try to access a URL that requires an authentication. So you need to configure URLs with similar role requirements to give the user a chance to attempt a login before calling secured service methods.


Start the app:

grails run-app

and open http://localhost:8080/acltest/report/list

Login as user2/user2 and you should only see reports #5 and #10. Logout via http://localhost:8080/acltest/logout and open the list page again, this time logging in as user1/user1. Now you should be able to see instances #1-4.

Verify that you can view #3 directly by clicking the id or opening http://localhost:8080/acltest/report/show/3. Also verify that you can edit #3 but cannot delete it.

Verify that you can view #4 directly by clicking the id or opening http://localhost:8080/acltest/report/show/4 and that can edit and delete it.

Verify that you can't view #7 directly by opening http://localhost:8080/acltest/report/show/7.


You can download the full sample app here and the plugin with ACL support here.

There isn't much documentation available for Spring Security ACLs, but I found this blog post to be very informative and thorough.

If you have questions or issues with the code, please email the Grails User mailing list so others who might be having similar problems can partipate in the conversation.

Hierarchical Roles in the Grails Spring Security Plugin

Monday, December 21st, 2009

I was looking at a non-Grails Spring Security application that used hierarchical roles and wondered what it'd take to get this working with the Grails plugin. Turns out it's pretty simple.

Non-hierarchical roles are checked by a RoleVoter but to use hierarchical roles you need a RoleHierarchyVoter. Replacing the roleVoter bean in resources.groovy is all it takes.

RoleHierarchyVoter needs an implementation of RoleHierarchy and the default implementation in Spring Security is RoleHierarchyImpl which parses a String defining the hierarchy. For example, this configuration defines the hierarchy ROLE_SUPERADMIN > ROLE_ADMIN > ROLE_USER:

import org.springframework.security.userdetails.hierarchicalroles.RoleHierarchyImpl
import org.springframework.security.vote.RoleHierarchyVoter

beans = {

   roleHierarchy(RoleHierarchyImpl) {
      hierarchy = '''
         ROLE_SUPERADMIN > ROLE_ADMIN
         ROLE_ADMIN > ROLE_USER
      '
''
   }

   roleVoter(RoleHierarchyVoter, ref('roleHierarchy'))
}

You can download a small demo app here that shows how it works. Unpack the app and run grails run-app, and then open http://localhost:8080/hierarchical/secure/. The app creates three users in BootStrap:

Username Password Role
user user ROLE_USER
admin admin ROLE_ADMIN
superadmin superadmin ROLE_SUPERADMIN

so you can login as each user to test the secured actions:

class SecureController {

   def index = {}

   @Secured(['ROLE_USER'])
   def user = {
   ...
   }

   @Secured(['ROLE_ADMIN'])
   def admin = {
   ...
   }

   @Secured(['ROLE_SUPERADMIN'])
   def superadmin = {
   ...
   }
}

Logout in between by navigating to http://localhost:8080/hierarchical/logout. Although only one role is defined for each action, as the super admin you can access all three, as the admin you can access admin and user, and as the user you can only access user.


I'll make this part of the plugin at some point to make configuration simpler, but for now it's not much work to do it explicitly.

SpringOne 2GX Sample Apps – Spring Security LDAP Login

Tuesday, December 01st, 2009

This is the second in a series of posts making the demo applications that I used for my SpringOne 2GX presentations available. I'll describe here how to create a Grails application using the Spring Security plugin that authenticates users from LDAP. This is based on topics from the Demystifying Spring Security in Grails talk (you can download the presentation here) but wasn't shown there since I ran out of time.

Also refer to the plugin documentation for other tutorials here.


To create an application that authenticates users from LDAP, run

grails create-app springone2gx_ldap
cd springone2gx_ldap

To make classpath management simpler in Eclipse/STS I create a grails-app/conf/BuildConfig.groovy (in Grails 1.1 apps; in 1.2 this is done for you) with the line

grails.project.plugins.dir='plugins'

to keep plugins in the project root like in 1.0.x but this is optional.

Next install the plugin:

grails install-plugin acegi

Run the create-auth-domains script to generate the person, authority, and request map domain classes and also grails-app/conf/SecurityConfig.groovy:

grails create-auth-domains com.burtbeckwith.springone2gx.User com.burtbeckwith.springone2gx.Role com.burtbeckwith.springone2gx.Requestmap

The other two scripts that the plugin provides are optional and create CRUD pages (generate-manager) and basic user registration (generate-registration). It's a good idea to run generate-manager; run generate-registration if it's useful to you.

grails generate-manager

As with the previous post we'll use annotated controllers, so we'll need to configure that, and we can delete the request map class and CRUD pages. The plugin scripts currently asssume you'll be using request maps, so we have to run generate-manager and generate-registration before deleting these.

  • delete grails-app/domain/com/burtbeckwith/springone2gx/Requestmap.groovy
  • delete grails-app/controller/RequestmapController.groovy
  • delete the grails-app/views/requestmap directory and its GSPs
  • remove the com.burtbeckwith.springone2gx.Requestmap import from grails-app/controller/RoleController.groovy
  • in grails-app/conf/SecurityConfig.groovy, disable requestmaps (useRequestMapDomainClass = false) and enable annotations (useControllerAnnotations = true), and remove the requestMapClass property:
    security {

       active = true

       loginUserDomainClass = "com.burtbeckwith.springone2gx.User"
       authorityDomainClass = "com.burtbeckwith.springone2gx.Role"

       useRequestMapDomainClass = false
       useControllerAnnotations = true
    }

In Eclipse or STS the steps to configure the classpath are:

  • add PLUGIN_DIR/src/groovy as a source folder
  • add PLUGIN_DIR/src/java as a source folder
  • add PLUGIN_DIR/grails-app/services as a source folder
  • add these jars from PLUGIN_DIR/lib
    • facebook-java-api-2.0.4.jar
    • jcifs-1.2.25.jar
    • spring-ldap-1.2.1.jar
    • spring-ldap-tiger-1.2.1.jar
    • spring-security-core-2.0.4.jar
    • spring-security-core-tiger-2.0.4.jar
    • spring-security-ntlm-2.0.4.jar
    • spring-security-openid-2.0.4.jar

Having done all that, let's create a secured controller to test annotations:

grails create-controller secure

and add the import for the annotation, and annotate at the class level that you must be an admin to access this controller:

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

@Secured(['ROLE_ADMIN'])
class SecureController {

   def index = {
      redirect action: foo
   }

   def foo = {
      render 'OK'
   }

   def bar = {
      render 'also OK'
   }
}

The controller has a redirect from the default action and a second action so we can test that all methods inherit the class-level annotation.

Next lets configure LDAP. To make this a self-contained demo, we'll use the excellent LDAP server plugin but obviously you'll need to configure the application to connect to your LDAP server.

Install the plugin by running

grails install-plugin ldap-server

and add the necessary LDAP configuration option to grails-app/conf/SecurityConfig.groovy (at a minimum useLdap = true)

security {
   ...
   useLdap = true
   ldapServer = 'ldap://localhost:10389'
   ldapManagerDn = 'uid=admin,ou=system'
   ldapManagerPassword = 'secret'
   ldapSearchBase = 'dc=d1,dc=example,dc=com'
   ldapSearchFilter = '(uid={0})'
   ldapGroupSearchBase = 'ou=groups,dc=d1,dc=example,dc=com'
   ldapGroupSearchFilter = 'uniquemember={0}'
   ldapUsePassword = false
}

The LDAP plugin requires one or more configured LDAP servers in grails-app/conf/Config.groovy and we'll need just one:

ldapServers {
   d1 {
      base = 'dc=d1,dc=example,dc=com'
      port = 10389
      indexed = ['objectClass', 'uid', 'mail', 'userPassword', 'description']
   }
}

The plugin will auto-load .ldif data files with user information, so put these records in grails-app/ldap-servers/d1/data/users.ldif:

dn: ou=groups,dc=d1,dc=example,dc=com
objectclass: organizationalUnit
objectclass: top
ou: groups

dn: cn=USER,ou=groups,dc=d1,dc=example,dc=com
objectclass: groupOfUniqueNames
cn: USER
objectclass: top
uniqueMember: cn=person1,dc=d1,dc=example,dc=com
uniqueMember: cn=person2,dc=d1,dc=example,dc=com
uniqueMember: cn=person3,dc=d1,dc=example,dc=com

dn: cn=ADMIN,ou=groups,dc=d1,dc=example,dc=com
objectclass: groupOfUniqueNames
objectclass: top
cn: ADMIN
uniqueMember: cn=person2,dc=d1,dc=example,dc=com

dn: cn=person1,dc=d1,dc=example,dc=com
objectClass: uidObject
objectClass: person
objectClass: top
objectClass: organizationalPerson
uid: person1
userPassword: {SHA}44rSFJQ9qtHWTBAvrsKd5K/p2j0=
cn: person1
sn: jones

dn: cn=person2,dc=d1,dc=example,dc=com
objectClass: uidObject
objectClass: person
objectClass: top
objectClass: organizationalPerson
uid: person2
userPassword: {SHA}KqYKj/f81HPTIeAUav2eJt85UUc=
cn: person2
sn: jones

dn: cn=person3,dc=d1,dc=example,dc=com
objectClass: uidObject
objectClass: person
objectClass: top
objectClass: organizationalPerson
uid: person3
userPassword: {SHA}ERnP037iRzV+A0oI2ETuol9v0g8=
cn: person3
sn: jones

Spring Security will by default convert LDAP groups ('groupOfUniqueNames') to roles, prefixing the group names with ROLE_, so this data creates three users; person1, person2, and person3 (with passwords 'password1', 'password2', and 'password3' respectively), all with ROLE_USER and person2 with ROLE_ADMIN.

Since LDAP is only managing authentication details we need local data in the database; create the corresponding entries in BootStrap:

import com.burtbeckwith.springone2gx.User

class BootStrap {

   def init = { servletContext ->
      new User(username: 'person1', enabled: true).save()
      new User(username: 'person2', enabled: true).save()
      new User(username: 'person3', enabled: true).save(flush: true)
   }

   def destroy = {}
}

Since LDAP is handling authentication, we can (partially) remove password-related fields from User.groovy along with other unused fields:

class User {

   static hasMany = [authorities: Role]
   static belongsTo = Role

   String username
   String passwd = 'notused'
   boolean enabled

   static constraints = {
      username blank: false, unique: true
   }
}

We need to leave in the passwd property since GrailsDaoImpl expects it but its value isn't important so we'll just hard-code it in the domain class. A custom subclass GrailsDaoImpl or a new implementation of UserDetailsService would remove this requirement.

Start the app using

grails run-app

and open http://localhost:8080/springone2gx_ldap/secure/ in a browser and it should prompt you to login. If you login as person1 or person3 you'll be denied access since those users only have ROLE_USER but person2 has ROLE_ADMIN and will be allowed.

After successful login it'll redirect to http://localhost:8080/springone2gx_ldap/secure/foo - verify that http://localhost:8080/springone2gx_ldap/secure/bar is also secured by going to that in your browser.


You can download a finished application based on this discussion here

SpringOne 2GX Sample Apps – Spring Security Form Login

Tuesday, December 01st, 2009

This is the first in a series of posts making the demo applications that I used for my SpringOne 2GX presentations available. I'll describe here how to create a standard Grails application using the Spring Security plugin that authenticates users from a database. This was used in the Demystifying Spring Security in Grails talk (you can download the presentation here)

Also refer to the plugin documentation for other tutorials here.


To create a standard application that loads users from a database, run

grails create-app springone2gx
cd springone2gx

To make classpath management simpler in Eclipse/STS I create a grails-app/conf/BuildConfig.groovy (in Grails 1.1 apps; in 1.2 this is done for you) with the line

grails.project.plugins.dir='plugins'

to keep plugins in the project root like in 1.0.x but this is optional.

Next install the plugin:

grails install-plugin acegi

Run the create-auth-domains script to generate the person, authority, and request map domain classes and also grails-app/conf/SecurityConfig.groovy:

grails create-auth-domains com.burtbeckwith.springone2gx.User com.burtbeckwith.springone2gx.Role com.burtbeckwith.springone2gx.Requestmap

The other two scripts that the plugin provides are optional and create CRUD pages (generate-manager) and basic user registration (generate-registration). It's a good idea to run generate-manager; run generate-registration if it's useful to you.

grails generate-manager

There are three methods for securing URLs in the plugin and for this demo we'll use controller annotations, so we'll need to configure that. We can delete the request map class, controller, and CRUD pages since they won't be needed. The plugin scripts currently assume you'll be using request maps, so we have to run generate-manager and generate-registration before deleting these.

  • delete grails-app/domain/com/burtbeckwith/springone2gx/Requestmap.groovy
  • delete grails-app/controller/RequestmapController.groovy
  • delete the grails-app/views/requestmap directory and its GSPs
  • remove the com.burtbeckwith.springone2gx.Requestmap import from grails-app/controller/RoleController.groovy
  • in grails-app/conf/SecurityConfig.groovy, disable requestmaps (useRequestMapDomainClass = false) and enable annotations (useControllerAnnotations = true), and remove the requestMapClass property:
    security {

       active = true

       loginUserDomainClass = "com.burtbeckwith.springone2gx.User"
       authorityDomainClass = "com.burtbeckwith.springone2gx.Role"

       useRequestMapDomainClass = false
       useControllerAnnotations = true
    }


The current plugin is monolithic - it has support for several authentication mechanisms (OpenID, Facebook, etc.) If you're not using these you can delete that code and associated jar files. This is completely optional, and you can always add them back by extracting them from the plugin zip.

If you're not using OpenID, here are the steps to remove that code:

  • delete the org.codehaus.groovy.grails.plugins.springsecurity.openid package: PLUGIN_DIR/src/groovy/org/codehaus/groovy/grails/plugins/springsecurity/openid
  • delete these jars
    • openid4java-0.9.2.jar
    • spring-security-openid-2.0.4.jar
    • xmlsec-1.3.0.jar
    • htmlparser-1.6.jar
    • commons-httpclient-3.0.1.jar
    • openxri-client.jar
    • openxri-syntax.jar
  • in LoginController.groovy
    • delete def openIDConsumer
    • delete def openIDAuthenticationProcessingFilter
    • delete the openIdAuthenticate() action
    • remove the config.useOpenId part in the auth() action

If you're not using Facebook, here are the steps to remove that code:

  • delete the org.codehaus.groovy.grails.plugins.springsecurity.facebook package: PLUGIN_DIR/src/java/org/codehaus/groovy/grails/plugins/springsecurity/facebook
  • delete these jars
    • facebook-java-api-2.0.4.jar
    • json-20070829.jar
  • in LoginController.groovy
    • remove the config.useFacebook part in the auth() action

If you're not using Kerberos, here are the steps to remove that code:

  • delete the org.codehaus.groovy.grails.plugins.springsecurity.kerberos package: PLUGIN_DIR/src/groovy/org/codehaus/groovy/grails/plugins/springsecurity/kerberos

If you're not using LDAP, here are the steps to remove that code:

  • delete the org.codehaus.groovy.grails.plugins.springsecurity.ldap package: PLUGIN_DIR/src/groovy/org/codehaus/groovy/grails/plugins/springsecurity/ldap and PLUGIN_DIR/src/java/org/codehaus/groovy/grails/plugins/springsecurity/ldap
  • delete these jars
    • spring-ldap-1.2.1.jar
    • spring-ldap-tiger-1.2.1.jar
  • in LoginController.groovy
    • remove the config.useFacebook part in the auth() action

If you're not using CAS, here are the steps to remove that code:

  • delete these jars

    • cas-client-core-3.1.1.jar
    • spring-security-cas-client-2.0.4.jar

Even if you don't use NTLM it's best to leave it in - the jars and code aren't large and it's more complicated to remove than just deleting.

In Eclipse or STS the steps to configure the classpath are:

  • add PLUGIN_DIR/src/groovy as a source folder
  • add PLUGIN_DIR/src/java as a source folder
  • add PLUGIN_DIR/grails-app/services as a source folder
  • add PLUGIN_DIR/lib/spring-security-core-2.0.4.jar
  • add PLUGIN_DIR/lib/spring-security-core-tiger-2.0.4.jar
  • add PLUGIN_DIR/lib/spring-security-ntlm-2.0.4.jar
  • add PLUGIN_DIR/lib/jcifs-1.2.25.jar

Having done all that, let's create a secured controller to test annotations:

grails create-controller secure

and add the import for the annotation, and annotate at the class level that you must be an admin to access this controller:

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

@Secured(['ROLE_ADMIN'])
class SecureController {

   def index = {
      redirect action: foo
   }

   def foo = {
      render 'OK'
   }

   def bar = {
      render 'also OK'
   }
}

The controller has a redirect from the default action and a second action so we can test that all methods inherit the class-level annotation.

Hitting this controller will require a login, so lets add code to BootStrap to create a user:

import com.burtbeckwith.springone2gx.Role
import com.burtbeckwith.springone2gx.User

class BootStrap {

   def passwordEncoder

   def init = { servletContext ->

      def adminRole = new Role(description: 'Admin role',
                  authority: 'ROLE_ADMIN').save()

      String password = passwordEncoder.encodePassword('p4ssw0rd', null)
      def me = new User(username: 'admin',
                  passwd: password, enabled: true).save()
      adminRole.addToPeople(me)
      adminRole.save(flush: true)
   }

   def destroy = {}
}

Start the app using

grails run-app

and open http://localhost:8080/springone2gx/secure/ in a browser and it should prompt you to login - use the username and password from the user created in BootStrap.

After successful login it'll redirect to http://localhost:8080/springone2gx/secure/foo - verify that http://localhost:8080/springone2gx/secure/bar is also secured by going to that in your browser.


The 'secure' controller blocks access, but anyone can access the role or user controller - open http://localhost:8080/springone2gx/user/list to verify that. So let's finish locking down the app.

Add the same annotation to UserController and RoleController that you added to SecureController:

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

@Secured(['ROLE_ADMIN'])
class UserController {
...
}

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

@Secured(['ROLE_ADMIN'])
class RoleController {
...
}

Up to now we've assumed that all access should be allowed unless it's explicitly blocked. This is appropriate for many applications, but you might want to take the pessimistic approach of denying access unless it's allowed. That's simple to do.

Set controllerAnnotationsRejectIfNoRule to true in grails-app/conf/SecurityConfig.groovy, and since we can't annotate CSS/JavaScript/images/etc., we'll need to allow access to those using the controllerAnnotationStaticRules property:

security {

   active = true

   loginUserDomainClass = 'com.burtbeckwith.springone2gx.User'
   authorityDomainClass = 'com.burtbeckwith.springone2gx.Role'

   useRequestMapDomainClass = false
   useControllerAnnotations = true

   controllerAnnotationsRejectIfNoRule = true
   controllerAnnotationStaticRules = [
      '/**/js/**': ['IS_AUTHENTICATED_ANONYMOUSLY'],
      '/**/css/**': ['IS_AUTHENTICATED_ANONYMOUSLY'],
      '/**/images/**': ['IS_AUTHENTICATED_ANONYMOUSLY'],
      '/*': ['IS_AUTHENTICATED_ANONYMOUSLY']
   ]
}

Users won't be able to log in now, since you're now allowing access to LoginController. That's easy to do: add an annotation:

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

@Secured(['IS_AUTHENTICATED_ANONYMOUSLY'])
class LoginController {
...
}

IS_AUTHENTICATED_ANONYMOUSLY means that any access is allowed - "anonymous" logins as well as real logins. Do the same for LogoutController, and CaptchaController and RegisterController if you're using the registration features of the plugin.

Using this approach, new controllers will be inaccessible until you annotate them. If you need the pessimistic approach, this is preferred to the alternative that controllers accidentally allow full access because you forgot to lock them down.


You can download a finished application based on this discussion here

Fixing the User/Role Many-to-Many in the Grails Spring Security Plugin

Sunday, October 25th, 2009

The User/Role many-to-many relationship in the Grails Spring Security plugin is modeled using the standard GORM mapping approach, i.e. using hasMany and belongsTo. As I pointed out here this is a performance concern when you have a large number of users, since granting a new user a popular role (e.g. ROLE_USER) will cause all other users with that role to be loaded from the database.

To fix this in the current plugin would be a breaking change, but I'm planning on creating a new plugin that will use Spring Security 3 once it's released, so I thought I'd write up some notes on how to fix the many-to-many mapping for current users. It's only a few steps.

The first is to map the join table, so you'll need to create a UserRole domain class (I'm assuming that your person class is named User and your authority class is named Role - translate as appropriate):

import org.apache.commons.lang.builder.HashCodeBuilder

class UserRole implements Serializable {

   User user
   Role role

   boolean equals(other) {
      if (!(other instanceof UserRole)) {
         return false
      }

      return other.user.id == user.id && other.role.id == role.id
   }

   int hashCode() {
      return new HashCodeBuilder().append(user.id).append(role.id).toHashCode()
   }

   static UserRole create(User user, Role role, boolean flush = false) {
      new UserRole(user: user, role: role).save(flush: flush, insert: true)
   }

   static boolean remove(User user, Role role, boolean flush = false) {
      UserRole userRole = UserRole.findByUserAndRole(user, role)
      return userRole ? userRole.delete(flush: flush) : false
   }

   static void removeAll(User user) {
      executeUpdate("DELETE FROM UserRole WHERE user=:user", [user: user])
   }

   static mapping = {
      id composite: ['role', 'user']
      version false
      table 'role_people'
   }
}

Some notes on this class:

  • it has to implement Serializable since it's a Hibernate composite primary key class
  • the mapping block settings ensure that the table DDL is the same as that for the autogenerated join table, so you won't need to update your database
  • the hashCode and equals methods are just suggestions; feel free to re-implement

Next remove static hasMany = [people: User] from Role and static hasMany = [authorities: Role] and static belongsTo = Role from User.

While we don't want to map the Role's User collection, we still need convenient access to the User's roles, so next add a utility method to User to mimic what we removed when deleting the hasMany. While we're here let's add a hasRole method:

Set<Role> getAuthorities() {
   UserRole.findAllByUser(this).collect { it.role } as Set
}

boolean hasRole(Role role) {
   UserRole.countByUserAndRole(this, role)> 0
}

If you're using the plugin-generated CRUD pages (created via grails generate-manager) you'll want to remove the User listings from views/role/show.gsp:

<tr class="prop">
   <td valign="top" class="name">People:</td>
   <td valign="top" class="value">${authority.people}</td>
</tr>

and views/role/edit.gsp:

<tr class="prop">
  <td valign="top" class="name"><label for="people">People:</label></td>
  <td valign="top" class="value ${hasErrors(bean:authority,field:'people','errors')}">
  <ul>
  <g :each var="p" in="${authority.people?}">
     <li>${p}</li>
  </g>
  </ul>
  </td>
</tr>

Then in RegisterController.groovy change

role.addToPeople(person)

to

UserRole.create(person, role)

and finally in UserController.groovy, change (in two places)

Role.findAll().each { it.removeFromPeople(person) }

to

UserRole.removeAll(person)

and

Role.findByAuthority(key).addToPeople(person)

to

UserRole.create(person, Role.findByAuthority(key))


And that's it. You shouldn't need to make any database changes, since the new code will map to the existing tables just like the old code. If you've used the addToPeople and removeFromPeople dynamic many-to-many methods elsewhere in your code you'll need to convert those to use the UserRole helper methods, but otherwise the impact should be fairly minor.

Fixing Grails 1.0 Many-to-Many Mappings in 1.1

Sunday, March 01st, 2009

One of the more frequently asked questions about Grails involves its "backwards" Many-to-Many mapping. Given domain classes Foo and Bar, the join table foo_bar that's generated for them will have a 'foo' column that points to the bar table and a 'bar' column that points to the foo table. In the comments of this bug it's explained that this was intentional. But in 1.1 the Grails team decided to listen to the users and rework the approach.

Unfortunately this is a stealth fix that's listed in the release notes along with many other changes but not in the breaking changes and it is very much a breaking change - unless you make changes your many-to-many relationships will not work after a 1.0 -> 1.1 upgrade. This is of particular interest to me as a developer on the Spring Security (Acegi) plugin since Role < -> User relationships are modeled as a many-to-many.

The plugin uses three domain classes, User, Role, and Requestmap. You can use whatever names you want but I'll assume those names here; note that Requestmap isn't affected by this issue. In Grails 1.0 your database would have two tables, user and role, and a many-to-many join table role_user, with backwards mappings, i.e. people_id refers to role and authorities_id refers to user:

CREATE TABLE role (
   id BIGINT NOT NULL AUTO_INCREMENT,
   version BIGINT NOT NULL,
   authority VARCHAR(255) NOT NULL UNIQUE,
   description VARCHAR(255) NOT NULL,
   PRIMARY KEY (id)
) ENGINE=InnoDB;

CREATE TABLE user (
   id BIGINT NOT NULL AUTO_INCREMENT,
   version BIGINT NOT NULL,
   description VARCHAR(255) NOT NULL,
   email VARCHAR(255) NOT NULL,
   email_show bit NOT NULL,
   enabled bit NOT NULL,
   passwd VARCHAR(255) NOT NULL,
   user_real_name VARCHAR(255) NOT NULL,
   username VARCHAR(255) NOT NULL UNIQUE,
   PRIMARY KEY (id)
) ENGINE=InnoDB;

CREATE TABLE role_user (
   people_id BIGINT NOT NULL,
   authorities_id BIGINT NOT NULL,
   PRIMARY KEY (people_id, authorities_id)
) ENGINE=InnoDB;

ALTER TABLE role_user ADD INDEX FK1407FDF48F01F561 (people_id),
ADD CONSTRAINT FK1407FDF48F01F561 FOREIGN KEY (people_id) REFERENCES role (id);

ALTER TABLE role_user ADD INDEX FK1407FDF4CF6CDEE4 (authorities_id),
ADD CONSTRAINT FK1407FDF4CF6CDEE4 FOREIGN KEY (authorities_id) REFERENCES user (id);

Here the name of the mapping table is the owning end ('Role') followed by the owned end ('User'). Note that the foreign key and index names ('FK1407FDF48F01F561', etc.) will probably be different for your tables.

However in Grails 1.1 the join table looks like this:

CREATE TABLE role_people (
    role_id BIGINT NOT NULL,
    user_id BIGINT NOT NULL,
    PRIMARY KEY (role_id, user_id)
) ENGINE=InnoDB;

ALTER TABLE role_people ADD INDEX FK28B75E7852388A1A (role_id),
ADD CONSTRAINT FK28B75E7852388A1A FOREIGN KEY (role_id) REFERENCES role (id);

ALTER TABLE role_people ADD INDEX FK28B75E78F7634DFA (user_id),
ADD CONSTRAINT FK28B75E78F7634DFA FOREIGN KEY (user_id) REFERENCES user (id);

and the mappings are correct - role_id references role and user_id references user. The name is the owning end ('role') followed by the collection name of the owned end ('people').

This means that your many-to-many relationships are completely broken, and in particular it means that you won't be able to log in to your application if you're using the Spring Security plugin. You'll see errors like this in your logs:

ERROR springsecurity.GrailsDaoImpl  - User [foo] has no GrantedAuthority

So there are two options - migrate your data to the new table, or use the GORM mapping closure to get Grails to work in '1.0 mode'. The first option is cleaner, but if you're accessing your join tables explicitly, e.g. for reporting queries using SQL, then this approach will break those.

For MySQL, the migration command is:

INSERT INTO role_people (role_id, user_id)
SELECT people_id, authorities_id FROM role_user;

and you'll want to create the missing foreign keys and associated indexes:

ALTER TABLE role_people ADD INDEX FK28B75E7852388A1A (role_id),
ADD CONSTRAINT FK28B75E7852388A1A FOREIGN KEY (role_id) REFERENCES role (id);

ALTER TABLE role_people ADD INDEX FK28B75E78F7634DFA (user_id),
ADD CONSTRAINT FK28B75E78F7634DFA FOREIGN KEY (user_id) REFERENCES user (id);

Once you verify that this worked, you should drop role_user and its foreign keys:

ALTER TABLE role_user DROP FOREIGN KEY FK1407FDF48F01F561;
ALTER TABLE role_user DROP FOREIGN KEY FK1407FDF4CF6CDEE4;
DROP TABLE role_user;

Alternatively, to continue to use "1.0 mode", you just need to add a mapping closure to each of your domain classes:

In Role.groovy:

static mapping = {
   people column: 'people_id', joinTable: 'role_user'
}

and in User.groovy:

static mapping = {
   authorities column: 'authorities_id', joinTable: 'role_user'
}

and your app will continue to work - and still be backwards ;)

Grails Acegi (Spring Security) Plugin v0.5 Released

Monday, January 12th, 2009

Version 0.5 went out yesterday. This was an interesting one to work on. There were a few relatively minor bug fixes, but there are four new features/enhancements and the plugin now works with Grails 1.1.


Probably the coolest new feature is being able to define security rules directly in the Controllers. Previously there were two ways to define the URL->Role mappings, in a static string (the standard Spring Security approach) or using Requestmap entries in the database. Now there's a third - using annotations in controllers.

I'd written about using annotations previously and I used that approach. Annotations can be ugly, but I think here they're great here since they'll only ever have a fairly short list of values (roles plus special tokens like IS_AUTHENTICATED_FULLY, etc.). You can annotate individual actions and/or put an annotation at the class level and then all of the actions in the controller share those mappings, and if needed you can override that for individual actions. And they're inheritable, so if you use controller base classes you can define them there and share rules throughout the hierarchy. For example, if you have an AbstractAdminController that all administrative controllers extend you could annotate just the base class with @Secured(['ROLE_ADMIN']) and restrict access to the entire admin hierarchy.

There are some notes here describing the new approach and the original two.

To better support stuff like administrative sections of a site, I added in an IP address filter so you can specify IP ranges (using Ant patterns or masks) for URLs. This way you can restrict access for your admin area to '10.**' or '192.168.**' in addition to requiring specific roles to ensure that only users in the intranet or VPN have access.

I also added in support for ChannelProcessingFilter to allow defining which URLs require HTTPS and which require HTTP. With this addition there aren't many Spring Security filters left that haven't been mapped in the plugin - just ConcurrentSessionFilter, SessionFixationProtectionFilter, X509PreAuthenticatedProcessingFilter, and RequestHeaderPreAuthenticatedProcessingFilter.

Someone on the Grails user mailing list was talking about Facebook logins, and I'd been wanting an excuse to play with their API so I wired up support for that. It's of limited use (like the OpenID support) since you don't get much information from the authentication, so there would be a lot of manual work required in the app when setting up user information in the database. For example, you don't even get the login name/email, only the numeric ID, so that has to be the username attribute in the User table.

Getting things going with Grails 1.1 was interesting. There was one real bug that I'd introduced in AuthenticatedVetoableDecisionManager - I think it's a Groovy bug but I worked around it. I had named both a variable and a method deny, and it was trying to invoke deny() on the variable instead of the method. I changed it to denyCount which is more self-documenting and it was fine.

The plugin generates its own controllers and CRUD pages, so I needed to update those to use new 1.1 features. The allowedMethods map is now static so I changed those to avoid warnings at startup. Optimistic locking is now checked in controllers by adding an <input type='hidden'> form element in the GSP with the current domain instance version and checking it against the database after submitting updates, so I wired that up so the User/Role/Requestmap management pages work like non-plugin pages.

I also had to rework my automated testing due to the changes in the way plugins are installed in 1.1. I have an Ant task that creates a test project, installs the plugin, and configures the app for testing. Then I run a suite of automated tests using the WebTest plugin. Plugins are now global by default, so installing a plugin for a second app will share the plugin that the first app installed, which will save a bunch of space both locally and in source control. But I need the plugin to be local to the test app, and this is possible by overriding the default behavior and reverting to the 1.0 approach by setting grails.plugins.dir='plugins' in grails-app/conf/BuildConfig.groovy. And this doesn't affect 1.0 since BuildConfig.groovy is ignored.

So check out the new release. There are no backwards compatibility issues, so upgrading is simple. Check out the fixes and enhancements here and the docs here.

An updated Grails Acegi plugin

Monday, April 14th, 2008

Note - this is out of date now since these changes have been merged into the Acegi Plugin. To get the latest features install the standard plugin, i.e. "grails install-plugin acegi"


I've implemented Acegi Security (now Spring Security) in a few Spring apps so I greatly appreciate how simple the Grails Acegi plugin makes securing an application. It only takes a few minutes to install and configure and you get to avoid working witht Acegi's notoriously large XML config (and steep learning curve).

A coworker was asking if it'd be possible to use LDAP instead of a database and this got me thinking about how a lot of the default configuration in the plugin isn't modifyable. In addition to alternate data stores I could see needing to add in a LogoutHandler or even one or more extra Filters but you'd need to edit the plugin code, which makes upgrading a pain.

Also, I've been following the progress of Acegi migrating to Spring Security 2.0 and their new simpler configuration options. I've only implemented a small Grails app so far but we're working on a much more extensive one and I want to avoid having to downgrade to traditional Acegi, so I spent a weekend upgrading and extending the plugin's configurability and sent it off to the original developers to see if they'd like to incorporate the changes. They're pretty busy but are reviewing the new code.

Along the way I found and fixed a couple of Hibernate Session-handling bugs
and it turned out to be good timing - just after I fixed them a couple of people complained about them on the user mailing list. If you're affected by the bugs but don't want to risk using this update you can replace the 3 fixed files attached to the Jira issue.

It may take a while to get things incorporated into the official plugin, so if any of this is interesting to you, feel free to download the updated plugin (I called it spring-security-0.1 but it'll most likely be acegi-security-0.3 for consistency), and of course let me know if there are any issues.


The plugin is configured using a default script (DefaultAcegiConfig) and a user-defined script (AcegiConfig). Many options are configurable, and I added a few more:

  • filterNames - a list of filter bean names. If the list is specified in the
    user's config, the specified filters will be used in the requested order,
    otherwise the standard filters will be used
  • logoutHandlerNames - a list of logout handler bean names. If the list is
    specified in the user's config, the specified logout handlers will be used in
    the requested order, otherwise the standard handlers will be used
  • decisionVoterNames - a list of voter bean names. If the list is specified in
    the user's config, the specified voters will be used in the requested order,
    otherwise the standard voters will be used
  • providerNames - a list of authentication provider bean names. If the list is
    specified in the user's config, the specified authentication providers will
    be used in the requested order, otherwise the standard authentication
    providers will be used

So for example, if you wanted to integrate an SSO solution, you could replace one or more of the default filters and/or add extra filters by overriding the list of filter names, and defining the SSO-specific filters in resources.groovy or resources.xml. The same goes for logout handlers, voters, and providers.

I also made a few string properties customizable:

  • realmName - allows the user to choose the realm name instead of the
    default 'Grails Realm' (no idea if this is useful)
  • rememberMeKey - allows the user to choose the
    rememberMeServices/rememberMeAuthenticationProvider key instead of the
    default 'grailsRocks'
  • afterLogoutUrl - allows the user to choose the logoutFilter
    logoutSuccessUrl instead of the default '/'

I tried to keep things backwards compatible for the users, but made a few small
changes. One was to change the 'show_mail' parameter
to 'showMail' (in the user domain class) and another was to change the 'loadAcegi' config attribute to 'active' (in AcegiConfig). Also, I renamed DefaultAcegiConfig and AcegiConfig to DefaultSecurityConfig and SecurityConfig respectively.

To make working with LDAP or other authentication stores easier, I reworked GrailsUser and GrailsDaoImpl a bit. GrailsUser is now an interface (extending UserDetails and adding a getter for the user domain object) and GrailsUserImpl is the default concrete implementation. So you can use your own implementation of GrailsUser by subclassing GrailsDaoImpl and overriding createUserDetails() or replace the whole bean by defining your own implementation of UserDetailsService in resources.groovy/resources.xml (it turns out that any beans that you define replace plugins' beans with the same name/id).

I also moved the artifacts (AuthBase, AuthenticateService, and AuthorizeTagLib) into packages so projects that use packages can access them.


There were some relatively minor issues:

  • org.acegisecurity.annotation.SecurityAnnotationAttributes was apparently
    moved to org.springframework.security.annotation.SecurityAnnotationAttributes
    but is no longer available in the code. I couldn't figure out how to
    implement annotation-based security without this (there's no 2.0 documenation
    yet) so I re-implemented the Acegi code in the plugin
    (org.codehaus.groovy.grails.plugins.springsecurity.SecurityAnnotationAttributes)
    and it works fine. This should be replaced with whatever approach is typical
    in 2.0
  • I'm using Java 5 features since the existing annotation support had already broken support for
    Java 1.4. If this is a problem it would be simple to revert
  • I tested as much as I could but haven't used the email, Ajax login, or
    Switch User features so those might be broken (unlikely though)
  • apparently there's a bug in the Grails plugin.xml generation code when a
    plugin uses packages, so the elements are broken (I doubt this
    affects anything)

Additionally, Stephan February emailed the Grails user list
around the same time announcing that he'd made a custom version of the plugin with ACL support added, so there's plenty of merging to be done :)

Creative Commons License
This work is licensed under a Creative Commons Attribution-ShareAlike 2.5 License.