Description
Rick Jensen (Migrated from SEC-1823) said:
With Active Directory (AD), groups can be nested within each other. In fact, in larger organizations, it is quite common to use nested groups to manage users.
The default AD authorities populator only looks at a user's memberOf property to populate the list of granted authorities, using the group name as the authority name. The memberOf property is only populated with groups that the user is directly a member of, and does not include groups that the user is a member of through nesting of groups.
For example, you have the following group structure:
MyApplicationAdmins (group)
Members: DomainAdmins
DomainAdmins (group)
Members: User1
User1 (user)
Currently, the ActiveDirectoryLdapAuthenticationProvider will only populate the user's GrantedAuthorities with DomainAdmins, since that is the only group the user is directly a member of. Instead, the user should have a GrantedAuthorities list that contains both DomainAdmins and MyApplicationAdmins, since the user is in both groups through nesting.
With generic LDAP, there is no way to get nested groups other than by walking the LDAP tree, which requires multiple calls to the LDAP server and is very expensive. With AD however, there is a special matching rule object identifier that will walk a chain of ancestry objects. This page (http://msdn.microsoft.com/en-us/library/aa746475%28VS.85%29.aspx) describes it in more detail.
An example of how to use the filter to get all groups a user is in would be this filter:
(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={0}))
where {0} is the DN of the user.
To enable this, the loadUserAuthorities(...) method of the ActiveDirectoryLdapAuthenticationProvider needs to be modified. Since the full nested group information is not available in the DirContextOperations object, another AD server call is required, but it is only a single call and it fully populates the GrantedAuthorities with all of the groups a user is in, both directly and via nesting.
Here is an example of the updated method: (note: this was put together hastily and likely has issues, but it serves as a concrete starting point)
/**
* Creates the user authority list from the values of the {@code memberOf} attribute obtained from the user's
* Active Directory entry in a nested fashion.
* @throws NamingException
*/
public Collection<? extends GrantedAuthority> loadUserAuthorities(DirContextOperations userData, String username, String password) throws NamingException {
SearchControls searchControls = new SearchControls();
searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
String filter = "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={0}))";
final String bindPrincipal = createBindPrincipal(username, domain);
String searchRoot = rootDn != null ? rootDn : searchRootFromPrincipal(bindPrincipal);
DirContext ctx = bindAsUser(username, password);
final DistinguishedName ctxBaseDn = new DistinguishedName(ctx.getNameInNamespace());
final DistinguishedName searchBaseDn = new DistinguishedName(searchRoot);
final NamingEnumeration<SearchResult> resultsEnum = ctx.search(searchBaseDn, filter, new Object[]{userData.getDn()}, searchControls);
if (logger.isDebugEnabled()) {
logger.debug("Searching for entry under DN '" + ctxBaseDn
+ "', base = '" + searchBaseDn + "', filter = '" + filter + "'");
}
ArrayList<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
try {
while (resultsEnum.hasMore()) {
SearchResult searchResult = resultsEnum.next();
// Work out the DN of the matched entry
DistinguishedName dn = new DistinguishedName(new CompositeName(searchResult.getName()));
if (searchRoot.length() > 0) {
dn.prepend(searchBaseDn);
}
if (logger.isDebugEnabled()) {
logger.debug("Found DN: " + dn);
}
authorities.add(new SimpleGrantedAuthority(dn.removeLast().getValue()));
}
} catch (PartialResultException e) {
org.springframework.security.ldap.LdapUtils.closeEnumeration(resultsEnum);
logger.info("Ignoring PartialResultException");
}
return authorities;
}