Felipe Cypriano

You're about to read

Customizing SpringSecurity to Protect Each Button of a Page Using Grails Acegi Plugin

I’m very happy with grails acegi plugin, aka Spring Security Plugin, but on my newest project I needed a finner grained way to do control access than using simple urls filters and roles.

I wanted a way to control which button, link, action of the current page the user can access. If the user has only read access to a page than the page is shown but edit action isn’t, because of this requirement using only roles to grant access isn’t enough and could easily became a mess if I create one role per action. Use urls filters won’t work because most urls are generated by ZK framework and hence are non predictable.

The solution is fully based on SpringSecurity capabilities and should work on every project that uses it independent of plugins or frameworks that I use. Since spring security plugin does the hard work for us, we just need to create two more classes besides acegi’s default user and role and extends UserDetailsService interface. This is based on zk_sample_project and is a database implementation of this article by Oleg Zhurakousky.

The database schema

The plugin needs User and Role class by default, to extend (not meaning inheritance) them I create two new classes to add more control over which resource can be accessed namely: Group and Access. The Group class purpose is only to organize the Access, since there will be a lot of them. It’s important to notice that the access’ name and the role’s authority must be unique.

With this classes we can create roles that contains a list of granted accesses, for instance:

  • ROLE_ADMIN

    • pagePolicy.btnNew
    • pagePolicy.btnSave
    • pagePolicy.btnDelete
  • ROLE_ORDINARY

    • pagePolicy.btnNew

With our granted access setup in database we need to teach spring security how to authorize an access based on the access name:

1
2
authenticateService.ifAllGranted "ROLE_ADMIN" //  works out of the box
authenticateService.ifAllGranted "pagePolicy.btnNew" //  don't work

The goal is to make both options works, to do this we need to provide a custom implementation of UserDetailsService which will load to the UserDetails object both roles and accesses as GrantedAuthority. Let’s customize spring security and extend GrailsDAOImpl to add our logic:

src/groovy/HierachyUserDetailsService.groovy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class HierachyUserDetailsService extends GrailsDaoImpl {
    protected GrantedAuthority[] loadAuthorities(user, String username, boolean loadRoles) {
        if (!loadRoles) {
            return []
        }

        def accessCriteria = Access.createCriteria()
        // get all the accesses for this user
        def accesses = accessCriteria.list {
            authorizedGroups {
                authorizedRoles {
                    users {
                        eq("login", username)
                    }
                }
            }
        }

        // now we iterate over the accesses to get the roles associated to each access
        def allAuthorities = [] as Set
        accesses.each { access ->
            def roles = [] as Set
            access. authorizedGroups*. authorizedRoles*.each { role ->
                roles << new GrantedAuthorityImpl(role.authorityValue)
            }

            allAuthorities.addAll(roles)
            allAuthorities << new AccessGrantedAuthority(access.name, roles)
        }
        log.debug "Authorities for user ${username}: ${allAuthorities}"

        return allAuthorities as GrantedAuthority[]
    }
}

GrailsDaoImpl already implements UserDetailsService interface and provide a very good set of methods we just need to override one of them to achieve our goal. Great. The AccessGrantedAuthority (line 28) is just to make it easier to debug from which role the access came from. See the log debug output using this class:

Authorities for user admin: [ROLE_ADMIN, ROLE_ORDINARY, pagePolicy.btnNew[ROLE_ADMIN, ROLE_ORDINARY], pagePolicy.btnSave[ROLE_ADMIN], pagePolicy.btnDelete[ROLE_ADMIN]]

If don’t want this information you could use the default implementation, GrantedAuthorityImpl as in line 24.

Now that we have our implementation the last step is to configure our implementation as the current implementation used by Spring Security:

grails-app/conf/spring/resources.groovy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import org.codehaus.groovy.grails.plugins.springsecurity.AuthorizeTools
beans = {
    // Spring Security
    def securityConf = AuthorizeTools.securityConfig.security
    userDetailsService(br.com.litoraltextil.vc.springsecurity.AuthorityHierachyUserDetailsService) {
        usernameFieldName = securityConf.userName
        passwordFieldName = securityConf.password
        enabledFieldName = securityConf.enabled
        authorityFieldName = securityConf.authorityField
        loginUserDomainClass = securityConf.loginUserDomainClass
        relationalAuthoritiesField = securityConf.relationalAuthorities
        authoritiesMethodName = securityConf.getAuthoritiesMethod
        sessionFactory = ref('sessionFactory')
        authenticateService = ref("authenticateService")
    }
}

As a full configuration of spring security plugin isn’t the focus of this post I suggest you to read how to customize the plugin using SecurityConfig. Finally we can use all the artifacts using both the role name or the access name, some examples:

1
2
<g:ifAllGranted role="ROLE_ADMIN">secure stuff here</g:ifAllGranted>
<g:ifAllGranted role="ROLE_ADMIN, aAccess.name">secure stuff using both</g:ifAllGranted>