Preliminary note: although this article is about handling nested groups from Active Directory in your Grails application, the solution for a Plain Old Java Application with Spring Security is the same, so keep reading.

For authenticating and authorising your users against an LDAP or an Active Directory (AD) in your Grails application, the SpringSecurity plugin and its LDAP minion is your one-stop shop.

It's simply matter of configuring the various properties, as described in the plugin documentation. Our configuration looks like this:

grails.plugins.springsecurity.ldap.context.server = 'ldap://some.ip:389'
grails.plugins.springsecurity.ldap.authorities.groupSearchBase =
        'OU=Security Groups,OU=MyBusiness,DC=ugroup,DC=local'
grails.plugins.springsecurity.ldap.authorities.groupSearchFilter = '(member={0})'
grails.plugins.springsecurity.ldap.authorities.groupRoleAttribute = 'cn'
grails.plugins.springsecurity.ldap.search.base = 'OU=SBSUsers,OU=Users,OU=MyBusiness,DC=ugroup,DC=local'
grails.plugins.springsecurity.ldap.search.filter = '(sAMAccountName={0})'
grails.plugins.springsecurity.ldap.search.attributesToReturn =
        ['proxyAddresses', 'cn', 'sn', 'givenName', 'mail']
grails.plugins.springsecurity.ldap.authenticator.attributesToReturn =
        ['proxyAddresses', 'cn', 'sn', 'givenName', 'mail']

We've been using it happily for about a year. Until the System Administrator decided to re-organize the AD groups to better reflect the company structure by introducing nested groups.

Until now, any user would belong directly to one or several groups. For instance:

Alicia
    memberOf "Research"
    memberOf "Research Management"
Jonathan
    memberOf "Research"

Relying on the default group-to-role mapping provided by the plugin, our users have the following roles:

Alicia: ROLE_RESEARCH_MANAGEMENT, ROLE_RESEARCH
Jonathan: ROLE_RESEARCH

If we have a controller and we want to grant access to employees in the research department, we annotate it:

@Secured('ROLE_RESEARCH')
public ResearchController {
    ...
}

Works for both Alicia and Jonathan. Easy for us developers but less convenient for the sysadmin. There is an obvious redundancy in the AD data. In order to minimize maintenance and better reflect the corporate hierarchy, IT decided to re-organize the AD as follows:

Alicia
    memberOf "Research Management"
Jonathan
    memberOf "Research"
Research Management
    memberOf "Research"

Alicia can still be considered a member of both "Research Management" and "Research", but with our existing mapping, we're out of luck: only the "direct" group "Research Management" is mapped, leaving the nested group "Research" unmapped.

Alicia: ROLE_RESEARCH_MANAGEMENT
Jonathan: ROLE_RESEARCH

Googling for it, I found this very interesting issue in the S2 bug tracker. It contains a reference to MSDN documentation. Special filters allow you to tweak your search, including for our nested groups:

1.2.840.113556.1.4.1941
LDAP_MATCHING_RULE_IN_CHAIN
This rule is limited to filters that apply to the DN. This is a special "extended match operator that walks the chain of ancestry in objects all the way to the root until it finds a match.

Ah, magic search string... Nevermind. I was first tricked by the Jira issue mentioned above and first thought I had to roll my own AuthorityPopulator to feed the Grails plugin. But no, it's actually just matter of copy-paste!

I changed my group search filter from:

grails.plugins.springsecurity.ldap.authorities.groupSearchFilter = '(member={0})'

to

grails.plugins.springsecurity.ldap.authorities.groupSearchFilter =
        '(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={0}))'

And that's it, Alicia has retrieved her previous roles:

Alicia: ROLE_RESEARCH_MANAGEMENT, ROLE_RESEARCH

All is well. This little trick was worth a blog post. Thanks to Rick Jensen, the author of the precious S2 issue, for the source of inspiration and the pointers.