Current development on JAMWiki is primarily focused on maintenance rather than new features due to a lack of developer availability. If you are interested in working on JAMWiki please join the jamwiki-devel mailing list.

Tech:Acegi Ldap

ktip.png This page (and all pages in the Tech: namespace) is a developer discussion about a feature that is either proposed for inclusion in JAMWiki or one that has already been implemented. This page is NOT documentation of JAMWiki functionality - for a list of documentation, see Category:JAMWiki.
Status of this feature: IMPLEMENTED. Improved Spring Security integration was included as part of the JAMWiki 0.7.0 release.
Contents

Some configuration for Acegi and Ldap[edit]

After some trial and error, I finally got a working configuration with only some restrictions (take a look at the bottom of this page...) --hp 18-Mar-2008 04:46 PDT

First of all I made an custom configuration for all Ldap-Settings:


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN/EN" "http://www.springframework.org/dtd/spring-beans.dtd" >

<beans>
 
    <bean id="initialDirContextFactory"
        class="org.acegisecurity.ldap.DefaultInitialDirContextFactory">
        <constructor-arg value="ldap://${ldap_host}"/>
        <property name="managerDn" value="${ldap_user}" />
        <property name="managerPassword" value="${ldap_password}" />
         <property name="extraEnvVars">
            <map>
            	<entry key="java.naming.referral" value="follow" />
            	<entry key="java.naming.security.authentication" value="simple" />
            </map>
        </property>        
    </bean>

    <bean id="userSearch" class="org.acegisecurity.ldap.search.FilterBasedLdapUserSearch">
        <constructor-arg index="0" value="${ldap_base_dn}" />
        <constructor-arg index="1" value="(sAMAccountName={0})" />
        <constructor-arg index="2" ref="initialDirContextFactory" />
        <property name="searchSubtree" value="true" />
	<property name="derefLinkFlag" value="true" />
    </bean>

    <bean id="ldapAuthProvider" class="org.acegisecurity.providers.ldap.LdapAuthenticationProvider">
        <constructor-arg>
            <bean class="org.acegisecurity.providers.ldap.authenticator.BindAuthenticator">
                <constructor-arg><ref local="initialDirContextFactory"/></constructor-arg>
                <property name="userSearch" ref="userSearch"/>
            </bean>
        </constructor-arg>
        <constructor-arg>
          <bean class="my.JAMWikiLdapAuthoritiesPopulator">
            <constructor-arg>
                 <ref local="initialDirContextFactory"/>
            </constructor-arg>
            <constructor-arg value="${ldap_group_base_dn}" />
            <property name="groupRoleAttribute" value="cn" />
            <property name="additionalRoles">
            	<list>
		  <bean class="org.acegisecurity.GrantedAuthorityImpl">
		    <constructor-arg value="ROLE_USER"/>
		  </bean>
		  <bean class="org.acegisecurity.GrantedAuthorityImpl">
		    <constructor-arg value="ROLE_NO_ACCOUNT"/>
		  </bean>
	        </list>
	    </property>
	    <property name="roleMap">
		<map>
		  <entry key="exampleGroup1">
		    <list>
		      <value>ROLE_EDIT_NEW</value>
		      <value>ROLE_EDIT_EXISTING</value>
		    </list>
		  </entry>
		  <entry key="exampleAdminGroup">
		    <list>
		      <value>ROLE_ADMIN</value>
		    </list>
		  </entry>
		</map>
	    </property>
	    <property name="searchSubtree" value="true" />
          </bean>
        </constructor-arg>
    </bean>
</beans>

In applicationContext-acegi-security.xml I imported this config and made only few changes on the existing lines, to use the ldapAuthProvider:


	<bean id="authenticationManager" class="org.acegisecurity.providers.ProviderManager">
		<property name="providers">
			<list>
				<ref bean="ldapAuthProvider"/>
				<!-- <ref local="daoAuthenticationProvider" />-->
				<ref local="anonymousAuthenticationProvider" />
				<ref local="rememberMeAuthenticationProvider" />
			</list>
		</property>
	</bean>

Of course the Implementation of my.JAMWikiLdapAuthoritiesPopulator is necessary:


package my;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.naming.directory.SearchControls;

import org.acegisecurity.GrantedAuthority;
import org.acegisecurity.GrantedAuthorityImpl;
import org.acegisecurity.ldap.InitialDirContextFactory;
import org.acegisecurity.ldap.LdapDataAccessException;
import org.acegisecurity.ldap.LdapTemplate;
import org.acegisecurity.providers.ldap.LdapAuthoritiesPopulator;
import org.acegisecurity.userdetails.ldap.LdapUserDetails;
import org.jamwiki.utils.WikiLogger;

public class JAMWikiLdapAuthoritiesPopulator implements LdapAuthoritiesPopulator {

	private static final WikiLogger logger = WikiLogger.getLogger(JAMWikiLdapAuthoritiesPopulator.class.getName());
	
	private final String groupSearchBase;

	private String groupSearchFilter = "(member={0})";
	private String groupRoleAttribute = "cn";
	
    private LdapTemplate ldapTemplate;
    private SearchControls searchControls = new SearchControls();

    private String rolePrefix = "ROLE_";
    private boolean convertToUpperCase = true;
    
	private List additionalRoles = new ArrayList();
	private Map roleMap;
	
	public JAMWikiLdapAuthoritiesPopulator(
			InitialDirContextFactory initialDirContextFactory,
			String groupSearchBase) {
        this.ldapTemplate = new LdapTemplate(initialDirContextFactory);
        this.ldapTemplate.setSearchControls(searchControls);
		this.groupSearchBase = groupSearchBase;
	}

	public GrantedAuthority[] getGrantedAuthorities(LdapUserDetails userDetails)
		throws LdapDataAccessException {
        String userDn = userDetails.getDn();

        logger.finest("Getting authorities for user " + userDn);

        Set roles = getGroupMembershipRoles(userDn, userDetails.getUsername());
        List extraRoles = getAdditionalRoles();
        if (extraRoles != null) {
            roles.addAll(extraRoles);
        }

        return (GrantedAuthority[]) roles.toArray(new GrantedAuthority[roles.size()]);

	}
	
	protected List getMappedRolesFor(String role) {
		List ret = new ArrayList();
		
		List list = (List) roleMap.get(role);
		if (list != null) {
			for (Iterator it = list.iterator(); it.hasNext(); ) {
				ret.add(new GrantedAuthorityImpl((String) it.next()));
			}
		}
		
		return ret;
	}
	
    public Set getGroupMembershipRoles(String userDn, String username) {
        Set authorities = new HashSet();

        if (groupSearchBase == null) { 
            return authorities;
        }

        logger.finest("Searching for roles for user '" + username + "', DN = " + "'" + userDn + "', with filter "
                + groupSearchFilter + " in search base '" + getGroupSearchBase() + "'");

        Set userRoles = ldapTemplate.searchForSingleAttributeValues(getGroupSearchBase(), groupSearchFilter,
                new String[]{userDn, username}, groupRoleAttribute);

        logger.fine("Roles from search: " + userRoles);

        for (Iterator it = userRoles.iterator(); it.hasNext(); ) {
            String role = (String) it.next();
            if (convertToUpperCase) {
                role = role.toUpperCase();
            }
            authorities.add(new GrantedAuthorityImpl(rolePrefix + role));
            authorities.addAll(getMappedRolesFor(role.toUpperCase()));
        }

        return authorities;
    }

    public String getGroupSearchBase() {
		return groupSearchBase;
	}
    
	public void setAdditionalRoles(List additionalRoles) {
		this.additionalRoles.addAll(additionalRoles);
	}
	public List getAdditionalRoles() {
		return additionalRoles;
	}
	
	public void setGroupSearchFilter(String groupSearchFilter) {
		this.groupSearchFilter = groupSearchFilter;
	}
	
	public void setGroupRoleAttribute(String groupRoleAttribute) {
		this.groupRoleAttribute = groupRoleAttribute;
	}
	
    public void setSearchSubtree(boolean searchSubtree) {
        int searchScope = searchSubtree ? SearchControls.SUBTREE_SCOPE : SearchControls.ONELEVEL_SCOPE;
        searchControls.setSearchScope(searchScope);
    }	
    
    public void setConvertToUpperCase(boolean convertToUpperCase) {
		this.convertToUpperCase = convertToUpperCase;
	}
    
    public void setRolePrefix(String rolePrefix) {
		this.rolePrefix = rolePrefix;
	}
    
    public void setRoleMap(Map roleMap) {
    	this.roleMap = new HashMap();
    	for (Iterator it = roleMap.entrySet().iterator(); it.hasNext(); ) {
    		Map.Entry entry = (Map.Entry) it.next();
    		String key = (String) entry.getKey();
    		String upperCase = key.toUpperCase();
    		this.roleMap.put(upperCase, entry.getValue());
    	}
    }
}

The Implementation is quite similar to DefaultLdapAuthoritiesPopulator from Acegi with slight modifications:

* it allows a list of additionalRoles instead of a defaultRole
* it allows a map<string, list<string>> for mapping ldap-groups to roles

With this configuration I was able to log-in and out again, because of Authentication.getPrincipal() is no instance of WikiUserAuth anymore (as with daoAuthenticationProvider), the Username is null.

By adding just a few lines to WikiUserAuth.initWikiUserAuth, the Username is displayed after Login:


public static WikiUserAuth initWikiUserAuth(Authentication auth) throws AuthenticationCredentialsNotFoundException {
	if (auth == null) {
		throw new AuthenticationCredentialsNotFoundException("No authentication credential available");
	}
	if (auth.getPrincipal() instanceof WikiUserAuth) {
		// logged-in user
		return (WikiUserAuth)auth.getPrincipal();
	}
	WikiUserAuth user = new WikiUserAuth();
        // -- new from here
	try {
		user = new WikiUserAuth(auth.getName());
	} catch (Exception e) {
		logger.severe(e.getMessage());
		user = new WikiUserAuth();
	}		
        // ... to here
	user.setAuthorities(auth.getAuthorities());
	return user;
}

Some Limitations[edit]

  • Special:Watchlist doesn't work: The user has no userid (int). To manage this issue, the database must be changed (jam_watchlist should accept userlogins instead of ids (of course ids are more beautiful...)) and the DataHandler/QueryHandler-interfaces. The alternative solution would be adding each ldap-user at first logon to the database... I think this wouldn't be beautiful too.
  • Special:History works, but displays the ip-address instead of the username.

Comments[edit]

  • Because of using Acegi for Ldap-Integration, the org.jamwiki.ldap.LdapUserHandler isn't used
  • Of course a properties-file for the placeholders must be supplied (${ldap_user},...)
  • Perhaps there are a few more restrictions I can't see at the moment. I'm quite new to JAMWiki and Acegi...
Thanks for investigating this! The LdapUserHandler was implemented prior to the conversion to Acegi, so it is definitely something that can go away. As to getting rid of the user_id column from the watchlist, there has been some debate about getting rid of that field altogether and simply using login as the primary key. It would be somewhat painful to ensure that upgrades worked with that kind of change, but it might still be worth pursuing.
I'll have more time later this week to look over the work you've done, but hopefully in the mean time there might be some others who can comment. Let me know how you'd like to continue with this work - if you need Subversion access please just let me know your Sourceforge ID. Thanks again for investigating! -- Ryan 18-Mar-2008 08:00 PDT
Thanks, I allready have access to the repository on sourceforge, but I am new to maven and I still need some time to get familiar with the eclipse-plugins (m2eclipse, subclipse)... That's why I put the thing here... --hp 18-Mar-2008 11:16 PDT

Status[edit]

As part of the Spring Security 2.0 update a new filter was added to the security configuration so that authenticated users will automatically have a JAMWiki user record created for them, thus allowing integration with LDAP, CAS, OpenID, etc. In addition, Spring Security 2.0 provides vastly simplified configuration for integrating with LDAP, CAS, OpenID, etc. Basic LDAP support was tested and verified as part of the JAMWiki 0.7.0 development process. -- Ryan • (comments) • 28-Feb-2009 07:47 PST