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

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.

6 Responses to “Fixing the User/Role Many-to-Many in the Grails Spring Security Plugin”

  1. Jeremy says:

    Will these changes break the functionality of the authenticateService? Specifically ifAllGranted, ifNotGranted, ifAnyGranted and the taglib?

  2. Burt says:

    @Jeremy

    The data loading is independent of its usage – as long as the UserDetailsService implementation (GrailsDaoImpl) returns a User details instance (GrailsUserImpl), Spring Security doesn’t care how that data is loaded.

    As I described the changes here nothing is affected – there’s still a User.getAuthorities() method, but here it’s a dynamic finder instead of a mapped collection.

  3. @Burt

    You get a big thumbs-up from me! I was just beefing up my register controller and noticed that role.addToPeople(person) call. Yikes! I was adding this to my todo list and decided to do a quick search. You saved me having to do all this from scratch!

  4. rec says:

    I have a user/role implementation using a UserRole model as mentioned above. The problem is that I’m having a hard time doing queries that require a join.

    I’d like to do something like (pseudo-code):
    c = User.createCriteria()
    adminUsers = c.list(max, offset, order, sort, {
    roles {
    eq(‘roleName’, ‘admin’)
    }
    })

    In general, the issue is that the user model isn’t directly aware of the role relationship at the hibernate level and vice versa. I can get around it by using (as in the post above) User.findAll and then iterating on the models, but that is inefficient, and also prevents me from using the paged result type queries.

    Is there a way to have a dual hasMany relationship using a join table model without a belongsTo, for read-only purposes? So I can use UserRole directly to manage the objects and use the hasMany only for queries? I’ve done similar things in rails in the past.

    I ended up doing something like:
    UserRole.findAll(“from UserRole ur, User u, Role r WHERE ur.user = u AND ur.role = r AND r.roleName = ‘admin’”) and then working with the results, but it’s not as neat as working on the User or Role models directly.

    Any suggestions?

  5. rec says:

    Just for completeness, I tried to add to the following to User and Role

    hasMany = [userRoles: UserRole]

    and use

    adminUsers = c.list(max, offset, order, sort, {
    userRoles {
    role {
    eq(‘roleName’, ‘admin’)
    }
    }
    })

    hibrenate did a join to the user_role table, but did not do a join to the role table so it ended up with a bad sql statement

    • chuck says:

      Rec, did you ever figure out a solution to your problem? I would like to be able to something similar where I can use the User class to query a property (active) on the User_Role class.

      Thanks

Creative Commons License
This work is licensed under a Creative Commons Attribution 3.0 License.