(Quick Reference)
4 Required and Optional Domain Classes - Reference Documentation
Authors: Burt Beckwith, Beverley Talbott
Version: 1.2.7.3
4 Required and Optional 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.
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.
4.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. The class name is
Person, and
username,
enabled,
password are the default names of the 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.myappclass User { String username
String password
boolean enabled
boolean accountExpired
boolean accountLocked
boolean passwordExpired static constraints = {
username blank: false, unique: true
password blank: false
} static mapping = {
password column: '`password`'
} Set<Role> getAuthorities() {
UserRole.findAllByUser(this).collect { it.role } as Set
}
}Optionally, add other properties such as
email,
firstName,
lastName, and convenience methods, and so on:
package com.mycompany.myappclass User { String username
String password
boolean enabled
String email
String firstName
String lastName static constraints = {
username blank: false, unique: true
password blank: false
} Set<Role> getAuthorities() {
UserRole.findAllByUser(this).collect { it.role } as Set
} def someMethod {
…
}
}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:
| Property | Default Value | Meaning |
|---|
| userLookup.userDomainClassName | 'Person' | User 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 |
4.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.myappclass Role { String authority static mapping = {
cache true
} static constraints = {
authority blank: false, unique: true
}
}The class and property names are configurable using these configuration attributes:
| Property | Default Value | Meaning |
|---|
| authority.className | 'Authority' | Role class name |
| authority.nameField | 'authority' | Role class role name field |
4.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.testappimport org.apache.commons.lang.builder.HashCodeBuilderclass UserRole implements Serializable { User user
Role role boolean equals(other) {
if (!(other instanceof UserRole)) {
return false
} other.user?.id == user?.id &&
other.role?.id == role?.id
} 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) {
find 'from UserRole where user.id=:userId and role.id=:roleId',
[userId: userId, roleId: roleId]
} 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 instance = UserRole.findByUserAndRole(user, role)
if (!instance) {
return false
} instance.delete(flush: flush)
true
} static void removeAll(User user) {
executeUpdate 'DELETE FROM UserRole WHERE user=:user', [user: user]
} static mapping = {
id composite: ['role', 'user']
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:
| Property | Default Value | Meaning |
|---|
| userLookup.authorityJoinClassName | 'PersonAuthority' | User/Role many-many join class name |
4.4 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.
| Property | Default Value | Meaning |
|---|
| requestMap.className | 'Requestmap' | requestmap class name |
| requestMap.urlField | 'url' | URL pattern field name |
| requestMap.configAttributeField | 'configAttribute' | authority pattern field name |
Assuming you choose
com.mycompany.myapp as your package, and
Requestmap as your class name, you'll generate this class:
package com.testappclass Requestmap { String url
String configAttribute static mapping = {
cache true
} static constraints = {
url blank: false, unique: true
configAttribute blank: false
}
}To use Requestmap entries to guard URLs, see
Requestmap Instances Stored in the Database.