(Quick Reference)

3 Domain Classes - Reference Documentation

Authors: Burt Beckwith, Beverley Talbott

Version: 2.0.0

3 Domain Classes

By default the plugin uses regular Grails domain classes to access its required data. It's easy to create your own user lookup code though, which can access the database or any other source to retrieve user and authority data. See Custom UserDetailsService for how to implement this.

To use the standard user lookup you'll need at a minimum a 'person' and an 'authority' domain class. In addition, if you want to store URL<->Role mappings in the database (this is one of multiple approaches for defining the mappings) you need a 'requestmap' domain class. If you use the recommended approach for mapping the many-to-many relationship between 'person' and 'authority,' you also need a domain class to map the join table.

To use the user/group lookup you'll also need a 'group' domain class. If you are using the recommended approach for mapping many-to-many relationship between 'person' and 'group' and between 'group' and 'authority' you'll need a domain class for each to map the join tables. You can still additionally use 'requestmap' with this approach.

The s2-quickstart script creates initial domain classes for you. You specify the package and class names, and it creates the corresponding domain classes. After that you can customize them as you like. You can add unlimited fields, methods, and so on, as long as the core security-related functionality remains.

3.1 Person Class

Spring Security uses an Authentication object to determine whether the current user has the right to perform a secured action, such as accessing a URL, manipulating a secured domain object, accessing a secured method, and so on. This object is created during login. Typically overlap occurs between the need for authentication data and the need to represent a user in the application in ways that are unrelated to security. The mechanism for populating the authentication is completely pluggable in Spring Security; you only need to provide an implementation of UserDetailsService and implement its one method, loadUserByUsername().

By default the plugin uses a Grails 'person' domain class to manage this data. username, enabled, password are the default names of the core required properties. You can easily plug in your own implementation, and rename the class, package, and fields. In addition, you should define an authorities property to retrieve roles; this can be a public field or a getAuthorities() method, and it can be defined through a traditional GORM many-to-many or a custom mapping.

Assuming you choose com.mycompany.myapp as your package, and User as your class name, you'll generate this class:

package com.mycompany.myapp

import groovy.transform.EqualsAndHashCode import groovy.transform.ToString

@EqualsAndHashCode(includes='username') @ToString(includes='username', includeNames=true, includePackage=false) class User implements Serializable {

private static final long serialVersionUID = 1

transient springSecurityService

String username String password boolean enabled = true boolean accountExpired boolean accountLocked boolean passwordExpired

User(String username, String password) { this() this.username = username this.password = password }

Set<Role> getAuthorities() { UserRole.findAllByUser(this)*.role }

def beforeInsert() { encodePassword() }

def beforeUpdate() { if (isDirty('password')) { encodePassword() } }

protected void encodePassword() { password = springSecurityService?.passwordEncoder ? springSecurityService.encodePassword(password) : password }

static transients = ['springSecurityService']

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

static mapping = { password column: '`password`' } }

Optionally, add other properties such as email, firstName, lastName, and convenience methods, and so on:

package com.mycompany.myapp

import groovy.transform.EqualsAndHashCode import groovy.transform.ToString

@EqualsAndHashCode(includes='username') @ToString(includes='username', includeNames=true, includePackage=false) class User implements Serializable {

private static final long serialVersionUID = 1

transient springSecurityService

String username String password boolean enabled = true String email String firstName String lastName boolean accountExpired boolean accountLocked boolean passwordExpired

User(String username, String password) { this() this.username = username this.password = password }

def someMethod { … }

Set<Role> getAuthorities() { UserRole.findAllByUser(this)*.role }

def beforeInsert() { encodePassword() }

def beforeUpdate() { if (isDirty('password')) { encodePassword() } }

protected void encodePassword() { password = springSecurityService?.passwordEncoder ? springSecurityService.encodePassword(password) : password }

static transients = ['springSecurityService']

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

static mapping = { password column: '`password`' } }

The getAuthorities() method is analagous to defining static hasMany = [authorities: Authority] in a traditional many-to-many mapping. This way GormUserDetailsService can call user.authorities during login to retrieve the roles without the overhead of a bidirectional many-to-many mapping.

The class and property names are configurable using these configuration attributes:

PropertyDefault ValueMeaning
userLookup.userDomainClassNamenoneUser class name
userLookup.usernamePropertyName'username'User class username field
userLookup.passwordPropertyName'password'User class password field
userLookup.authoritiesPropertyName'authorities'User class role collection field
userLookup.enabledPropertyName'enabled'User class enabled field
userLookup.accountExpiredPropertyName'accountExpired'User class account expired field
userLookup.accountLockedPropertyName'accountLocked'User class account locked field
userLookup.passwordExpiredPropertyName'passwordExpired'User class password expired field
userLookup.authorityJoinClassName'PersonAuthority'User/Role many-many join class name

3.2 Authority Class

The Spring Security plugin also requires an 'authority' class to represent a user's role(s) in the application. In general this class restricts URLs to users who have been assigned the required access rights. A user can have multiple roles to indicate various access rights in the application, and should have at least one. A basic user who can access only non-restricted resources but can still authenticate is a bit unusual. Spring Security usually functions fine if a user has no granted authorities, but fails in a few places that assume one or more. So if a user authenticates successfully but has no granted roles, the plugin grants the user a 'virtual' role, ROLE_NO_ROLES. Thus the user satisfies Spring Security's requirements but cannot access secure resources, as you would not associate any secure resources with this role.

Like the 'person' class, the 'authority' class has a default name, Authority, and a default name for its one required property, authority. If you want to use another existing domain class, it simply has to have a property for name. As with the name of the class, the names of the properties can be whatever you want - they're specified in grails-app/conf/Config.groovy.

Assuming you choose com.mycompany.myapp as your package, and Role as your class name, you'll generate this class:

package com.mycompany.myapp

import groovy.transform.EqualsAndHashCode import groovy.transform.ToString

@EqualsAndHashCode(includes='authority') @ToString(includes='authority', includeNames=true, includePackage=false) class Role implements Serializable {

private static final long serialVersionUID = 1

String authority

Role(String authority) { this() this.authority = authority }

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

static mapping = { cache true } }

The class and property names are configurable using these configuration attributes:

PropertyDefault ValueMeaning
authority.classNamenoneRole class name
authority.nameField'authority'Role class role name field

Role names must start with "ROLE_". This is configurable in Spring Security, but not in the plugin. It would be possible to allow different prefixes, but it's important that the prefix not be blank as the prefix is used to differentiate between role names and tokens such as IS_AUTHENTICATED_FULLY, IS_AUTHENTICATED_ANONYMOUSLY, etc., and SpEL expressions.

The role names should be primarily an internal implementation detail; if you want to display friendlier names in a UI, it's simple to remove the prefix first.

3.3 PersonAuthority Class

The typical approach to mapping the relationship between 'person' and 'authority' is a many-to-many. Users have multiple roles, and roles are shared by multiple users. This approach can be problematic in Grails, because a popular role, for example, ROLE_USER, will be granted to many users in your application. GORM uses collections to manage adding and removing related instances and maps many-to-many relationships bidirectionally. Granting a role to a user requires loading all existing users who have that role because the collection is a Set. So even though no uniqueness concerns may exist, Hibernate loads them all to enforce uniqueness. The recommended approach in the plugin is to map a domain class to the join table that manages the many-to-many, and using that to grant and revoke roles to users.

Like the other domain classes, this class is generated for you, so you don't need to deal with the details of mapping it. Assuming you choose com.mycompany.myapp as your package, and User and Role as your class names, you'll generate this class:

package com.mycompany.myapp

import grails.gorm.DetachedCriteria import groovy.transform.ToString

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

@ToString(cache=true, includeNames=true, includePackage=false) class UserRole implements Serializable {

private static final long serialVersionUID = 1

User user Role role

UserRole(User u, Role r) { this() user = u role = r }

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

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

@Override int hashCode() { def builder = new HashCodeBuilder() if (user) builder.append(user.id) if (role) builder.append(role.id) builder.toHashCode() }

static UserRole get(long userId, long roleId) { criteriaFor(userId, roleId).get() }

static boolean exists(long userId, long roleId) { criteriaFor(userId, roleId).count() }

private static DetachedCriteria criteriaFor(long userId, long roleId) { UserRole.where { user == User.load(userId) && role == Role.load(roleId) } }

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

static boolean remove(User u, Role r, boolean flush = false) { if (u == null || r == null) return false

int rowCount = UserRole.where { user == u && role == r }.deleteAll()

if (flush) { UserRole.withSession { it.flush() } }

rowCount }

static void removeAll(User u, boolean flush = false) { if (u == null) return

UserRole.where { user == u }.deleteAll()

if (flush) { UserRole.withSession { it.flush() } } }

static void removeAll(Role r, boolean flush = false) { if (r == null) return

UserRole.where { role == r }.deleteAll()

if (flush) { UserRole.withSession { it.flush() } } }

static constraints = { role validator: { Role r, UserRole ur -> if (ur.user == null || ur.user.id == null) return boolean existing = false UserRole.withNewSession { existing = UserRole.exists(ur.user.id, r.id) } if (existing) { return 'userRole.exists' } } }

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

The helper methods make it easy to grant or revoke roles. Assuming you have already loaded a user and a role, you grant the role to the user as follows:

User user = …
Role role = …
UserRole.create user, role

Or by using the 3-parameter version to trigger a flush:

User user = …
Role role = …
UserRole.create user, role, true

Revoking a role is similar:

User user = …
Role role = …
UserRole.remove user, role

Or:

User user = …
Role role = …
UserRole.remove user, role, true

The class name is the only configurable attribute:

PropertyDefault ValueMeaning
userLookup.authorityJoinClassName'PersonAuthority'User/Role many-many join class name

3.4 Group Class

This Spring Security plugin provides you the option of creating an access inheritance level between 'person' and 'authority': the 'group'. The next three classes you will read about (including this one) are only used in a 'person'/'group'/'authority' implementation. Rather than giving a 'person' authorities directly, you can create a 'group', map authorities to it, and then map a 'person' to that 'group'. For applications that have a one or more groups of users who need the same level of access, having one or more 'group' instances makes managing changes to access levels easier because the authorities that make up that access level are encapsulated in the 'group', and a single change will affect all of the users.

If you run the s2-quickstart script with the group name specified and use com.mycompany.myapp as your package and RoleGroup and Role as your class names, you'll generate this class:

package com.mycompany.myapp

import groovy.transform.EqualsAndHashCode import groovy.transform.ToString

@EqualsAndHashCode(includes='name') @ToString(includes='name', includeNames=true, includePackage=false) class RoleGroup implements Serializable {

private static final long serialVersionUID = 1

String name

RoleGroup(String name) { this() this.name = name }

Set<Role> getAuthorities() { RoleGroupRole.findAllByRoleGroup(this)*.role }

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

static mapping = { cache true } }

When running the s2-quickstart script with the group name specified, the 'person' class will be generated differently to accommodate the use of groups. Assuming you use com.mycompany.myapp as your package and User and RoleGroup as your class names, the getAuthorities() method will be generated like so:

Set<RoleGroup> getAuthorities() {
   UserRoleGroup.findAllByUser(this).collect { it.roleGroup }
}

The plugin assumes the attribute authorities will provide the 'authority' collection for each class, but you can change the field names in grails-app/conf/Config.groovy. You also must ensure that the property useRoleGroups is set to true in order for GormUserDetailsService to properly attain the authorities.

PropertyDefault ValueAssigned Value Using s2QuickstartGroupsMeaning
useRoleGroupsfalsetrueUse 'authority group' implementation when loading user authorities
authority. groupAuthorityNameFieldnull'authorities'AuthorityGroup class role collection field

3.5 PersonGroup Class

The typical approach to mapping the relationship between 'person' and 'group' is a many-to-many. In a standard implementation, users have multiple roles, and roles are shared by multiple users. In a group implementation, users have multiple groups, and groups are shared by multiple users. For the same reason we would use a join class between 'person' and 'authority', we should use one between 'person' and 'group'. Please note that when using groups, there should not be a join class between 'person' and 'authority', since 'group' resides between the two.

If you run the s2-quickstart script with the group name specified, this class will be generated for you, so you don't need to deal with the details of mapping it. Assuming you choose com.mycompany.myapp as your package, and User and RoleGroup as your class names, you'll generate this class:

package com.mycompany.myapp

import grails.gorm.DetachedCriteria import groovy.transform.ToString

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

@ToString(cache=true, includeNames=true, includePackage=false) class UserRoleGroup implements Serializable {

private static final long serialVersionUID = 1

User user RoleGroup roleGroup

UserRoleGroup(User u, RoleGroup rg) { this() user = u roleGroup = rg }

@Override boolean equals(other) { if (!(other instanceof UserRoleGroup)) { return false }

other.user?.id == user?.id && other.roleGroup?.id == roleGroup?.id }

@Override int hashCode() { def builder = new HashCodeBuilder() if (user) builder.append(user.id) if (roleGroup) builder.append(roleGroup.id) builder.toHashCode() }

static UserRoleGroup get(long userId, long roleGroupId) { criteriaFor(userId, roleGroupId).get() }

static boolean exists(long userId, long roleGroupId) { criteriaFor(userId, roleGroupId).count() }

private static DetachedCriteria criteriaFor(long userId, long roleGroupId) { UserRoleGroup.where { user == User.load(userId) && roleGroup == RoleGroup.load(roleGroupId) } }

static UserRoleGroup create(User user, RoleGroup roleGroup, boolean flush = false) { def instance = new UserRoleGroup(user: user, roleGroup: roleGroup) instance.save(flush: flush, insert: true) instance }

static boolean remove(User u, RoleGroup rg, boolean flush = false) { if (u == null || rg == null) return false

int rowCount = UserRoleGroup.where { user == u && roleGroup == rg }.deleteAll()

if (flush) { UserRoleGroup.withSession { it.flush() } }

rowCount }

static void removeAll(User u, boolean flush = false) { if (u == null) return

UserRoleGroup.where { user == u }.deleteAll()

if (flush) { UserRoleGroup.withSession { it.flush() } } }

static void removeAll(RoleGroup rg, boolean flush = false) { if (rg == null) return

UserRoleGroup.where { roleGroup == rg }.deleteAll()

if (flush) { UserRoleGroup.withSession { it.flush() } } }

static constraints = { user validator: { User u, UserRoleGroup ug -> if (ug.roleGroup == null || ug.roleGroup.id == null) return boolean existing = false UserRoleGroup.withNewSession { existing = UserRoleGroup.exists(u.id, ug.roleGroup.id) } if (existing) { return 'userGroup.exists' } } }

static mapping = { id composite: ['roleGroup', 'user'] version false } }

3.6 GroupAuthority Class

The typical approach to mapping the relationship between 'group' and 'authority' is a many-to-many. In a standard implementation, users have multiple roles, and roles are shared by multiple users. In a group implementation, groups have multiple roles and roles are shared by multiple groups. For the same reason we would use a join class between 'person' and 'authority', we should use one between 'group' and 'authority'.

If you run the s2-quickstart script with the group name specified, this class will be generated for you, so you don't need to deal with the details of mapping it. Assuming you choose com.mycompany.myapp as your package, and RoleGroup and Role as your class names, you'll generate this class:

package com.mycompany.myapp

import grails.gorm.DetachedCriteria import groovy.transform.ToString

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

@ToString(cache=true, includeNames=true, includePackage=false) class RoleGroupRole implements Serializable {

private static final long serialVersionUID = 1

RoleGroup roleGroup Role role

RoleGroupRole(RoleGroup g, Role r) { this() roleGroup = g role = r }

@Override boolean equals(other) { if (!(other instanceof RoleGroupRole)) { return false }

other.role?.id == role?.id && other.roleGroup?.id == roleGroup?.id }

@Override int hashCode() { def builder = new HashCodeBuilder() if (roleGroup) builder.append(roleGroup.id) if (role) builder.append(role.id) builder.toHashCode() }

static RoleGroupRole get(long roleGroupId, long roleId) { criteriaFor(roleGroupId, roleId).get() }

static boolean exists(long roleGroupId, long roleId) { criteriaFor(roleGroupId, roleId).count() }

private static DetachedCriteria criteriaFor(long roleGroupId, long roleId) { RoleGroupRole.where { roleGroup == RoleGroup.load(roleGroupId) && role == Role.load(roleId) } }

static RoleGroupRole create(RoleGroup roleGroup, Role role, boolean flush = false) { def instance = new RoleGroupRole(roleGroup: roleGroup, role: role) instance.save(flush: flush, insert: true) instance }

static boolean remove(RoleGroup rg, Role r, boolean flush = false) { if (rg == null || r == null) return false

int rowCount = RoleGroupRole.where { roleGroup == rg && role == r }.deleteAll()

if (flush) { RoleGroupRole.withSession { it.flush() } }

rowCount }

static void removeAll(Role r, boolean flush = false) { if (r == null) return

RoleGroupRole.where { role == r }.deleteAll()

if (flush) { RoleGroupRole.withSession { it.flush() } } }

static void removeAll(RoleGroup rg, boolean flush = false) { if (rg == null) return

RoleGroupRole.where { roleGroup == rg }.deleteAll()

if (flush) { RoleGroupRole.withSession { it.flush() } } }

static constraints = { role validator: { Role r, RoleGroupRole rg -> if (rg.roleGroup == null || rg.roleGroup.id == null) return boolean existing = false RoleGroupRole.withNewSession { existing = RoleGroupRole.exists(rg.roleGroup.id, r.id) } if (existing) { return 'roleGroup.exists' } } }

static mapping = { id composite: ['roleGroup', 'role'] version false } }

3.7 Requestmap Class

Optionally, use this class to store request mapping entries in the database instead of defining them with annotations or in Config.groovy. This option makes the class configurable at runtime; you can add, remove and edit rules without restarting your application.

PropertyDefault ValueMeaning
requestMap.classNamenonerequestmap class name
requestMap.urlField'url'URL pattern field name
requestMap. configAttributeField'configAttribute'authority pattern field name
requestMap. httpMethodField'httpMethod'HTTP method field name (optional, does not have to exist in the class if you don't require URL/method security)

Assuming you choose com.mycompany.myapp as your package, and Requestmap as your class name, you'll generate this class:

package com.mycompany.myapp

import org.springframework.http.HttpMethod

import groovy.transform.EqualsAndHashCode import groovy.transform.ToString

@EqualsAndHashCode(includes=['configAttribute', 'httpMethod', 'url']) @ToString(includes=['configAttribute', 'httpMethod', 'url'], cache=true, includeNames=true, includePackage=false) class Requestmap implements Serializable {

private static final long serialVersionUID = 1

String configAttribute HttpMethod httpMethod String url

Requestmap(String url, String configAttribute, HttpMethod httpMethod = null) { this() this.configAttribute = configAttribute this.httpMethod = httpMethod this.url = url }

static constraints = { configAttribute blank: false httpMethod nullable: true url blank: false, unique: 'httpMethod' }

static mapping = { cache true } }

To use Requestmap entries to guard URLs, see Requestmap Instances Stored in the Database.