Tuesday, February 12, 2013

CAS - nested group memberships

In order to do the authorisation on the application side the single application needs to know to which groups the user is assigned. In active directory (AD) this groups could be nested, therefore the query could be fairly difficult.

Fortunately the query at the end was not that difficult... at least if you know what you are searching for :-)

member:1.2.840.113556.1.4.1941:={0} 
//where {0} needs to be replaced by distinguishedName in our case

Now we need to integrate this query into our jasig CAS system. The main idea how this can be done comes from this blog, so if you need some more details try to read the post there. The main idea there is to use the spring-securitys DefaultLdapAuthoritiesPopulator to fetch the Roles and fill them into an authorities-list. So I modified it's definition to use our query and some other customisations to fit into my deployerConfigContext.xml

<bean id="ldapAuthoritiesPopulator"
class="org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator">
<constructor-arg ref="contextSource" /> 
<constructor-arg value="${ldap.searchBase}" /> 
<property name="groupRoleAttribute" value="cn" />
<property name="groupSearchFilter" value="(member:1.2.840.113556.1.4.1941:={0})" />
<property name="IgnorePartialResultException" value="true" />
<property name="searchSubtree" value="true" />
<property name="rolePrefix" value="" />
<property name="convertToUpperCase" value="false" />
</bean>

So now we need to put this new bean into relation with the others in order to make use of it. As we are working on getting more attributes we need to add it to the bean that's in charge of retrieving all the attributes.

<bean id="attributeRepository"
class="org.jasig.services.persondir.support.ldap.LdapPersonAttributeAndRoleDao">
<property name="ldapTemplate" ref="ldapTemplate" />
<property name="queryTemplate" value="{0}"/>
<property name="baseDN" value="${ldap.searchBase}" />
<property name="requireAllQueryAttributes" value="false" />
<property name="queryAttributeMapping">
<map>
<entry key="username" value="${ldap.username}" />
</map>
</property>
<property name="resultAttributeMapping">
<map>
<entry key="distinguishedName" value="distinguishedName" />
<entry key="member" value="member" />
</map>
</property>
<property name="ldapAuthoritiesPopulator" ref="ldapAuthoritiesPopulator" />
</bean>

That's quite a straight forward definition, except that the used class is not existing, so we need to define it. Basically its just similar to the one suggested in the other blog, with small modification to fit into our system. (The following just contains the modified method)

public class LdapPersonAttributeAndRoleDao extends LdapPersonAttributeDao {

private DefaultLdapAuthoritiesPopulator ldapAuthoritiesPopulator;

private String groupAttributeName = "member";

@Override
protected List getPeopleForQuery(LogicalFilterWrapper queryBuilder, String queryUserName) {
List attribs = super.getPeopleForQuery(queryBuilder, queryUserName);
final List peopleWithRoles = new ArrayList(attribs.size());
Collection authorities = null;
try {
IPersonAttributes person = attribs.get(0);
if (person.getAttributes().get("distinguishedName") != null) {
authorities = ldapAuthoritiesPopulator.getGrantedAuthorities(new DirContextAdapter((String) person
.getAttributes().get("distinguishedName").get(0)), queryUserName);
}
} catch (Exception nnfe) {
logger.error("error looking up authorities", nnfe);
}
List authoritiesList;
if (null != authorities && authorities.size() > 0) {
authoritiesList = new ArrayList();
for (GrantedAuthority auth : authorities) {
authoritiesList.add(auth);
}
for (IPersonAttributes person : attribs) {
Map> attrs = new HashMap>();
attrs.putAll(person.getAttributes());
attrs.put(getGroupAttributeName(), authoritiesList);
peopleWithRoles.add(new CaseInsensitiveAttributeNamedPersonImpl(this.getConfiguredUserNameAttribute(),
attrs));
}
} else {
peopleWithRoles.addAll(attribs);
}
return peopleWithRoles;
}
}

And that's it basically... but as the casServiceValidationSuccess.jsp mentioned in the blog looks simpler than my proposed solution I changed it to that one.

<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>
<cas:authenticationSuccess>
<cas:user>${fn:escapeXml(assertion.chainedAuthentications[fn:length(assertion.chainedAuthentications)-1].principal.id)}</cas:user>
<c:if test="${not empty pgtIou}">
<cas:proxyGrantingTicket>${pgtIou}</cas:proxyGrantingTicket>
</c:if>
<c:if test="${fn:length(assertion.chainedAuthentications) > 1}">
<cas:proxies>
<c:forEach var="proxy" items="${assertion.chainedAuthentications}"
varStatus="loopStatus" begin="0"
end="${fn:length(assertion.chainedAuthentications)-2}" step="1">
<cas:proxy>${fn:escapeXml(proxy.principal.id)}</cas:proxy>
</c:forEach>
</cas:proxies>
</c:if>
<c:if test="${fn:length(assertion.chainedAuthentications) > 0}">
<cas:attributes>
<c:forEach var="auth" items="${assertion.chainedAuthentications}">
<c:forEach var="attr" items="${auth.principal.attributes}" >
<cas:${fn:escapeXml(attr.key)}>${fn:escapeXml(attr.value)}</cas:${fn:escapeXml(attr.key)}>
</c:forEach>
</c:forEach>
</cas:attributes>
</c:if>
</cas:authenticationSuccess>
</cas:serviceResponse>

(That's of course not the first modification we implemented into our CAS, so if you don't understand some of the code try to read my other posts, or drop me a comment)