Wednesday, August 25, 2010

Grails Secured Link for Acegi plugin and controller annotations

In my project I use Grails 1.3.3 and Acegi security plugin 0.5.3. There's cool stuff to set action-level permission inside controller:
@Secured(['ROLE_ADMIN', 'ROLE_CHIEF_EDITOR', 'ROLE_EDITOR'])
class AdminController {

    def index = { }

    @Secured(['ROLE_ADMIN'])
    def status = {}

    @Secured(['ROLE_ADMIN'])
    def controllers = {}

}
This works great but forces you to duplicate permission check in .gsp if you want to hide links from not authorized viewers. I created link taglib which checks if current user have enough permissions to access resulting action and hides . Guys who wrote Acegi plugin provided no link check and I found a way to simulate filter invocation.


package site.taglib

import org.grails.plugins.springsecurity.service.AuthenticateService
import org.springframework.security.ConfigAttributeDefinition

import org.springframework.util.StringUtils

class SecutiryTagLib {

    protected static final ConfigAttributeDefinition DENY = new ConfigAttributeDefinition(Collections.emptyList());
   
    AuthenticateService authenticateService

    def securedLink = { attrs, body ->
        def writer = getOut()
        def elementId = attrs.remove('elementId')

        def callController = attrs.controller
        def callAction = attrs.action
        if (!callAction) callAction = "index"
       
        def controllerUri = "/$callController/$callAction/".toLowerCase()

        ConfigAttributeDefinition configAttribute = null
        Object configAttributePattern = null

        def compiled = authenticateService.objectDefinitionSource.getConfigAttributeMap()
        def urlMatcher = authenticateService.objectDefinitionSource.urlMatcher

        def rejectIfNoRule = false
        if (authenticateService.securityConfig.controllerAnnotationsRejectIfNoRule instanceof Boolean) {
            rejectIfNoRule = authenticateService.securityConfig.controllerAnnotationsRejectIfNoRule
        }

        for (Map.Entry<Object, ConfigAttributeDefinition> entry: compiled.entrySet()) {
            Object pattern = entry.getKey();
            if (urlMatcher.pathMatchesUrl(pattern, controllerUri)) {
                if (configAttribute == null || urlMatcher.pathMatchesUrl(configAttributePattern, (String) pattern)) {
                    configAttribute = entry.getValue();
                    configAttributePattern = pattern;
                }
            }
        }

        if (configAttribute == null && rejectIfNoRule) {
            configAttribute = DENY;
        }

        def linkRoles = StringUtils.collectionToDelimitedString(configAttribute?.getConfigAttributes(), ',')

        if (authenticateService.ifAnyGranted(linkRoles)) {
            def link = createLink(attrs)
            writer << "<a href=\"${link}\""
            if (elementId) {
                writer << " id=\"${elementId}\""
            }
            writer << "${attrs.collect {k, v -> " $k=\"$v\"" }.join('')}>"
            writer << "${body()}</a>"
        }
    }
}

 After this you can simply rename g:link to g:securedLink
<g:securedLink controller="admin" action="status ">Status</g:securedLink>