(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.myappimport 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.myappimport 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:
Property | Default Value | Meaning |
---|
userLookup.userDomainClassName | none | 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 |
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.myappimport 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:
Property | Default Value | Meaning |
---|
authority.className | none | Role 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.myappimport grails.gorm.DetachedCriteria
import groovy.transform.ToStringimport 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:
Property | Default Value | Meaning |
---|
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.myappimport 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
.
Property | Default Value | Assigned Value Using s2QuickstartGroups | Meaning |
---|
useRoleGroups | false | true | Use 'authority group' implementation when loading user authorities |
authority. groupAuthorityNameField | null | '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.myappimport grails.gorm.DetachedCriteria
import groovy.transform.ToStringimport 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.myappimport grails.gorm.DetachedCriteria
import groovy.transform.ToStringimport 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.
Property | Default Value | Meaning |
---|
requestMap.className | none | requestmap 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.myappimport org.springframework.http.HttpMethodimport 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.