'Bug in Grails / Spring Security when using user groups and roles - can't authenticate
I think I have found a bug in Grails Spring Security 3.1.1, and latest Grails 3.2.6.
I have installed the Spring Security plugin.
From the command line console I did the following:
grails s2-quickstart org.softwood.security User Role --groupClassName=UserGroup
to create a user, role, and UserGroup table as I want to use the allocate roles to groups feature. I then configured the domain classes, and added a few users in the bootstrap to test it out like this:
def loadSecurityUserAndRoles () {
//plugin requires ROLE_ prefix see section 4.2/p18
Role adminRole = new Role(authority: 'ROLE_ADMIN').save(failOnError:true)
Role userRole = new Role(authority: 'ROLE_USER').save(failOnError:true)
Role xtraRole = new Role(authority: 'ROLE_XTRA').save(failOnError:true)
UserGroup adminGroup = new UserGroup (name:"GROUP_ADMIN").save(failOnError:true)
UserGroup userGroup = new UserGroup (name:"GROUP_USERS").save(failOnError:true)
User userWill = new User(username: 'will', password: 'password').save(failOnError:true)
User userMaz = new User(username: 'maz', password: 'password').save(failOnError:true)
User userMeg = new User(username: 'meg', password: 'password').save(failOnError:true)
//give adminGroup admin and user roles
UserGroupToRole sgr = UserGroupToRole.create(adminGroup, adminRole)
sgr = UserGroupToRole.create(adminGroup, userRole)
sgr = UserGroupToRole.create(userGroup, userRole)
assert UserGroupToRole.count() == 3
def auth2 = adminGroup.getAuthorities()
println "adminGroup authorities returned $auth2 "
//assign test user to adminGroup, and maz+meg to user group, inherit all group roles
UserToUserGroup su2g = UserToUserGroup.create (userWill, adminGroup, true)
su2g = UserToUserGroup.create (userMaz, userGroup, true)
su2g = UserToUserGroup.create (userMeg, userGroup, true)
//assign individual 'xtra' role to user
UserToRole sxtra = UserToRole.create(userWill, xtraRole, true)
assert UserToRole.count() == 1
def auth = userWill.getAuthorities()
assert auth.collect{it.authority}.sort() == ['ROLE_ADMIN', 'ROLE_USER', 'ROLE_XTRA']
println "userWill authorities returned $auth "
def mazAuth = userMaz.getAuthorities()
def megAuth = userMeg.getAuthorities()
println "user authorities returned maz: '$mazAuth', and meg: '$megAuth' "
def groups = userWill.getUserGroups()
assert groups.collect{it.name}.sort() == ['GROUP_ADMIN']
assert UserGroup.count() == 2
assert User.count() == 3
assert Role.count() == 3
assert UserToUserGroup.count() == 3
assert UserGroupToRole.count() == 3
assert UserToRole.count() == 1
}
This all seems to work as id expect and the basic asserts return the right numbers of roles for each user when I assert the <userInst>.getAuthorities()
:
I then setup a controller secureTest
with open action and secured one
class SecureTestController {
def index() {
render "hello Will you passed the permit_any"
}
@Secured ('ROLE_ADMIN')
def secure () {
render "hello Will you passed the ROLE_ADMIN"
}
}
I run the app - it starts, I point the browser in secureTest/index
- works fine as open url.
When I point the browser at secureTest/secure, it throws default login page. I fill in will/password at it throws stacktrace and fails to login.
The key part of that trace is here I think:
Caused by: groovy.lang.MissingPropertyException: No such property: authorities for class: org.softwood.security.Role
Possible solutions: authority
at org.grails.datastore.gorm.GormInstanceApi.propertyMissing(GormInstanceApi.groovy:55)
at org.grails.datastore.gorm.GormEntity$Trait$Helper.propertyMissing(GormEntity.groovy:57)
at org.grails.datastore.gorm.GormEntity$Trait$Helper$propertyMissing$9.call(Unknown Source)
at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:48)
at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:113)
at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:133)
at org.softwood.security.Role.propertyMissing(Role.groovy)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.codehaus.groovy.reflection.CachedMethod.invoke(CachedMethod.java:93)
at groovy.lang.MetaClassImpl.invokeMissingProperty(MetaClassImpl.java:880)
at groovy.lang.MetaClassImpl.getProperty(MetaClassImpl.java:1861)
at groovy.lang.MetaClassImpl.getProperty(MetaClassImpl.java:3735)
at org.softwood.security.Role.getProperty(Role.groovy)
at org.codehaus.groovy.runtime.InvokerHelper.getProperty(InvokerHelper.java:172)
at org.codehaus.groovy.runtime.ScriptBytecodeAdapter.getProperty(ScriptBytecodeAdapter.java:456)
at grails.plugin.springsecurity.userdetails.GormUserDetailsService$_loadAuthorities_closure2.doCall(GormUserDetailsService.groovy:92)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.codehaus.groovy.reflection.CachedMethod.invoke(CachedMethod.java:93)
at groovy.lang.MetaMethod.doMethodInvoke(MetaMethod.java:325)
at org.codehaus.groovy.runtime.metaclass.ClosureMetaClass.invokeMethod(ClosureMetaClass.java:294)
at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1024)
at groovy.lang.Closure.call(Closure.java:414)
at groovy.lang.Closure.call(Closure.java:430)
The method fails really here I think (GormUserDetailsService.groovy:92).
When you click that link the editor takes you to this in the plugin.
protected Collection<GrantedAuthority> loadAuthorities(user, String username, boolean loadRoles) {
if (!loadRoles) {
return []
}
def conf = SpringSecurityUtils.securityConfig
String authoritiesPropertyName = conf.userLookup.authoritiesPropertyName
String authorityPropertyName = conf.authority.nameField
boolean useGroups = conf.useRoleGroups
String authorityGroupPropertyName = conf.authority.groupAuthorityNameField
Collection<?> userAuthorities = user."$authoritiesPropertyName"
def authorities
if (useGroups) {
if (authorityGroupPropertyName) {
authorities = userAuthorities.collect { it."$authorityGroupPropertyName" }.flatten().unique().collect { new SimpleGrantedAuthority(it."$authorityPropertyName") }
}
else {
log.warn 'Attempted to use group authorities, but the authority name field for the group class has not been defined.'
}
}
else {
authorities = userAuthorities.collect { new SimpleGrantedAuthority(it."$authorityPropertyName") }
}
authorities ?: [NO_ROLE]
}
The key part here is this call sequence:
if (useGroups) {
if (authorityGroupPropertyName) {
authorities = userAuthorities.collect { it."$authorityGroupPropertyName" }.flatten().unique().collect { new SimpleGrantedAuthority(it."$authorityPropertyName") }
}
useGroups is true. I have a authorityGroupPropertyName that was set in application.groovy file by quick install script:
grails.plugin.springsecurity.authority.groupAuthorityNameField = 'authorities'
so this code line above calls:
userAuthorities.collect { it."$authorityGroupPropertyName" }.flatten().unique()
this returns a hashSet of role.authority
names as string and the flatten/unique just makes sure there are no nested structure and strings are unique. So far so good.
The last bit is the bug I think.
<hashSet of role Names>.collect { new SimpleGrantedAuthority(it."$authorityPropertyName") }
In this bit the collect method is called on the set of strings but the string passed to 'SimpleGrantedAuthority' should just be the string. instead its calling
it."$authorityPropertyName"
where it is a string and has no such property.
The key bits set up in application.groovy are:
grails.plugin.springsecurity.userLookup.userDomainClassName = 'org.softwood.security.User'
grails.plugin.springsecurity.userLookup.authoritiesPropertyName = 'authorities'
grails.plugin.springsecurity.userLookup.authorityJoinClassName = 'org.softwood.security.UserToUserGroup'
grails.plugin.springsecurity.authority.className = 'org.softwood.security.Role'
grails.plugin.springsecurity.authority.groupAuthorityNameField = 'authorities' //'authority'
grails.plugin.springsecurity.useRoleGroups = true
As you can see I tried to change authorities to 'authority' as that's the property name in the role class. That fails with missing property message also.
I think this is a bug and the code should just have passed 'it':
.collect {new SimpleGrantedAuthority(it)}
to generate the hashSet of <SimpleGrantedAuthority>
types.
Has any one else had this problem with Spring Security? I can't believe I'm the first to have fallen over it, or maybe no one is trying to use groups?
Solution 1:[1]
ok just before i go to bed - i copied the code out of the GormUserDetailsService into my bootstrap so i could expand expand/play in my own file space.
I modified the if block and expanded in out like this
if (useGroups) {
if (authorityGroupPropertyName) {
//userAuthorities returns Set<Role>
println """ debug
authoritiesPropertyName = $authoritiesPropertyName
authorityPropertyName = $authorityPropertyName
authorityGroupPropertyName = $authorityGroupPropertyName
userAuthorities returns $userAuthorities of type ${userAuthorities.getClass()}
"""
def roles = userAuthorities.collect { it."$authorityGroupPropertyName" }.flatten().unique()
authorities = roles.collect { new SimpleGrantedAuthority(it."$authorityPropertyName") }
my debug string shows this on the console
debug
authoritiesPropertyName = authorities
authorityPropertyName = authority
authorityGroupPropertyName = authority
userAuthorities returns [Role(authority:ROLE_XTRA), Role(authority:ROLE_USER), Role(authority:ROLE_ADMIN)] of type class java.util.HashSet
the first result returned into variable 'roles' is an ArrayList of String (each of the role.authority name instances. with correct values for this user as in earlier bootstrap setup for userWill.
the next of code now fails as i have an ArrayList of string, and it tries to access a property
authorities = roles.collect { new SimpleGrantedAuthority(it."$authorityPropertyName") }
which fails with
groovy.lang.MissingPropertyException: No such property: authority for class: java.lang.String
at org.codehaus.groovy.runtime.ScriptBytecodeAdapter.unwrap(ScriptBytecodeAdapter.java:53)
at org.codehaus.groovy.runtime.ScriptBytecodeAdapter.getProperty(ScriptBytecodeAdapter.java:458)
at coffeeshopapp.BootStrap$_loadSecurityUserAndRoles_closure6.doCall(BootStrap.groovy:115)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.codehaus.groovy.reflection.CachedMethod.invoke(CachedMethod.java:93)
at groovy.lang.MetaMethod.doMethodInvoke(MetaMethod.java:325)
at org.codehaus.groovy.runtime.metaclass.ClosureMetaClass.invokeMethod(ClosureMetaClass.java:294)
at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1024)
at groovy.lang.Closure.call(Closure.java:414)
at groovy.lang.Closure.call(Closure.java:430)
at org.codehaus.groovy.runtime.DefaultGroovyMethods.collect(DefaultGroovyMethods.java:3170)
at org.codehaus.groovy.runtime.DefaultGroovyMethods.collect(DefaultGroovyMethods.java:3140)
at org.codehaus.groovy.runtime.dgm$66.invoke(Unknown Source)
at org.codehaus.groovy.runtime.callsite.PojoMetaMethodSite$PojoMetaMethodSiteNoUnwrapNoCoerce.invoke(PojoMetaMethodSite.java:274)
at org.codehaus.groovy.runtime.callsite.PojoMetaMethodSite.call(PojoMetaMethodSite.java:56)
at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:48)
at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:113)
at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:125)
at coffeeshopapp.BootStrap.loadSecurityUserAndRoles(BootStrap.groovy:115)
whichever way i cut this this single line in the original source just doesn't work. original line reads again
authorities = userAuthorities.collect { it."$authorityGroupPropertyName" }.flatten().unique().collect { new SimpleGrantedAuthority(it."$authorityPropertyName") }
if it had read instead like this - the code would work
authorities = userAuthorities.collect { it."$authorityGroupPropertyName" }.flatten().unique().collect { new SimpleGrantedAuthority(it) }
surely the code in the v3.1.1 plugin cant work ?
Solution 2:[2]
I have configured users role and role group too, this is how mine is working:
In user class:
Set<RoleGroup> getAuthorities() {
UserRoleGroup.findAllByUser(this)*.roleGroup
}
//not used
// Set<RoleGroup> getAuthoritiesNames() {
// UserRoleGroup.findAllByUser(this)*.roleGroup?.name
// }
Set<Role> getAuthority() {
UserRole.findAllByUser(this).collect { it.role } as Set
}
In RoleGroup.groovy I have: (think I changed these unsure)
Set<Role> getAuthorities() {
RoleGroupRole.findAllByRoleGroup(this)*.role
}
def getAuthority() {
RoleGroupRole.withTransaction {
RoleGroupRole.findAllByRoleGroup(this)*.role.authority[0]
}
}
In my bootstrap something like this creates default admin account bound to role and role group:
private void addAdminUser(String username) {
User adminUser = User.findByUsername(username)
if (!adminUser) {
User.withTransaction {
adminUser = new User(username: username, password: 'PASSWORD'
)
adminUser.save(flush: true)
}
}
def adminRole
Role.withTransaction {
adminRole= Role.findByAuthority('ROLE_ADMIN')
if (!adminRole) {
adminRole = new Role(authority: 'ROLE_ADMIN').save(flush: true)
UserRole.create adminUser, adminRole,true
}
}
def adminRoleGroup
RoleGroup.withTransaction {
adminRoleGroup = RoleGroup.findByName('ADMINS')
if (!adminRoleGroup) {
adminRoleGroup = new RoleGroup(name: 'ADMINS').save(flush: true)
}
}
UserRoleGroup.withTransaction {
def adminRoleGroupRole = RoleGroupRole.findByRole(adminRole)
if (!adminRoleGroupRole) {
adminRoleGroupRole = new RoleGroupRole(role: adminRole, roleGroup: adminRoleGroup).save(flush: true)
new UserRoleGroup(user: adminUser, roleGroup: adminRoleGroup).save(flush: true)
}
}
}
Solution 3:[3]
I think I've unpeeled the onion and found the issue.
Step 1. I went back to beginning and created a new project from scratch and added the grails-security plugin on empty project. From Grails console used this as per oneline guide grails s2-quickstart org.softwood User Role --groupClassName=RoleGroup
.
This worked, so I compared what it had generated with what I'd got myself into and I think I understand where the problem for me was.
My role class was the same as empty test one - no difference.
Then I got to the User class, and somehow I'd got the getAuthorities() returning Set<Role>
(this seemed sensible as the getAuthorites() in my UserGroup (aka RoleGroup in fresh start) returns Set<Role>
. And basic User/Role returns Set<Role>
.
If you generate a project with just User and Role, then User.getAuthorities() returns set<Role>
. But when using a group the template is changed and Set<UserGroup>
was being returned.
That difference is crucial, as the code that adjusts its de-referencing strategy is in code in GormUserDetailsService.loadAuthorities() where it can't be seen - see my 'tweaked version I'd been trying to fix.
if (useGroups) {
if (authorityGroupPropertyName) {
//authorities = userAuthorities.collect { it."$authorityGroupPropertyName" }.flatten().unique().collect { new SimpleGrantedAuthority(it."$authorityPropertyName") }
//ww edit to stop gpf.. userAuthorities is Set<Role>, so first collect gets the names and produces ArrayList of string
//second collect builds the authorities as Set<SimpleGrantedAuthority>
authorities = userAuthorities.collect { it."$authorityGroupPropertyName" }.flatten().unique().collect { new SimpleGrantedAuthority(it) }
}
else {
log.warn 'Attempted to use group authorities, but the authority name field for the group class has not been defined.'
}
}
else {
authorities = userAuthorities.collect { new SimpleGrantedAuthority(it."$authorityPropertyName") }
}
The problem with this is that the def authorities
variable changes type depending on whether using user/role or user/group/role model.
I fell bang into the middle of this; I was trying to use groups (default allocation model) but permit individual Role assignments (individual granted Roles, as override facility).
When I read the User.authorities
property - I assumed this would return the Roles it was linked with (via groups, and via my individual assignment override) (like in single user/role model). I'd sorted this out in my domain class code like this in class user.
Set<Role> getAuthorities() {
//orig UserUserGroupBroken.findAllByUser(this)*.userGroup
Set<Role> individualRoles = UserToRole.findAllByUser(this)*.role
Set<UserGroup> groups = UserToUserGroup.findAllByUser(this)*.group
Set<Role> groupRoles = groups.collect{it.getAuthorities() }
Set<Role> aggregateRoles = new HashSet()
aggregateRoles.addAll (groupRoles.flatten())
aggregateRoles.addAll (individualRoles.flatten())
aggregateRoles
}
So that all seemed sensible and did what I wanted (my tests showed the the correct Roles assigned via the superposition). However the GormUserDetailsService wasn't expecting that and broke as if useGroups is true it de-references two sets to get a Set<Role>
, and use that to build the SimpleGrantedAuthority Set from.
The upshot of this is my purpose is defeated: it's too far a deviation from the assumed model.
I'll make it as a suggestion on the Git site, as it would just be so much cleaner and less opaque, and I think you intuitively assume that the authorities
property on users will give you a Set<Role>
.
For now just going to have to stick with just the basic user/group/role model and live with that.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | WILLIAM WOODMAN |
Solution 2 | V H |
Solution 3 | halfer |