LDAP or Active Directory integration

Submitted by mijalis on 2012-03-05

Are there any hacks or plans for LDAP authentication together with own database?

Yes, but it's quite far down in the todo list.

Regards,
Michał

Here's what I came up with. If you use this solution please understand that there is absolutely no support or warranty and this might break everything that's ever been connected to your network ever.

Limitations:

  • Your users and groups must exist inside of an OU. I'm not really sure why, but if you don't include and OU in the search path ldap_search can't seem to find anything. This could probably be fixed by using adLDAP instead of my home brew solution, but I'm too lazy.
  • You can't have a mix of LDAP users and standard users (except for admin).
  • Users are created automatically and their permissions are assigned from LDAP... so that makes the user manager pretty useless.
  • Users are not assigned to any projects when they login the first time.
  • Your web server must be able to communicate with your LDAP server.

I hope this is a good starting point for other people interested in implementing LDAP authentication. If you do improve this solution please post it back to this thread so that everyone can benefit.

New Helper Class System_Api_LDAPHelper

<?php

//	File Location: /system/api/ldaphelper.inc.php

if ( !defined( 'WI_VERSION' ) ) die( -1 );

class System_Api_LDAPHelper
{

	private $_params;
	private $_ldap_connection = false;
	
	public function __construct( $params = array() )
	{
		$this->setParams( $params );
	}
	
	//	set params
	public function setParams( $params = array() )
	{
		//	default ldap parameters
		$default_params = array(
			'ldap_domain_suffix' => "domain.local",
			'ldap_server' => "pdc.domain.local",
			'ldap_user_ou' => "DC=domain,DC=local",
			'ldap_group_ou' => "DC=domain,DC=local",
			'ldap_user_filter' => "(&(objectclass=person)(samaccountname=%s))",
			'ldap_group_filter' => "(&(objectclass=group)(cn=%s))"
		);
		
		//	merge provided params into defaults
		foreach ( $params as $key => $value )
			if ( isset( $default_params[$key] ) )
				$default_params[$key] = $value;
		
		//	update the object
		$this->_params = $default_params;
	}
	
	//	connect to ldap
	public function connect()
	{
		if ( $this->_ldap_connection ) return true;
		$this->_ldap_connection = @ldap_connect( $this->_params['ldap_server'] );
		if ( ! $this->_ldap_connection ) throw new Exception("Could not connect to LDAP server.");
	}
	
	//	bind to ldap server
	public function bind( $user, $password )
	{
		if ( ! $this->_ldap_connection ) $this->connect();
		if ( @ldap_bind( $this->_ldap_connection, "{$user}@{$this->_params['ldap_domain_suffix']}", $password ) ) return true;
		else return false;
	}
	
	//	get user info from ldap
	public function getUserInfo( $user )
	{
		// search for the user
		if ( ! $results = @ldap_search(
			$this->_ldap_connection,
			$this->_params['ldap_user_ou'],
			sprintf( $this->_params['ldap_user_filter'], str_replace( array( "(", ")", "*" ), array( "\(", "\)", "\*"), $user ) ),
			array( 'mail', 'dn', 'sn', 'givenName', 'displayName')
		) ) return false; // user is not found!
		
		// get the user details
		$user = @ldap_get_entries( $this->_ldap_connection, $results );
		
		// if there are more than one users returned throw and exception
		if ($user['count'] > 1) throw new Exception("Too many users returned from LDAP search.");
		
		$return = array(
			'dn' => $user[0]['dn']
		);
		for ( $i = 0; $i < $user[0]['count']; $i++ )
		{
			$name = $user[0][$i];
			$return[$name] = $user[0][$name][0];
		}
		
		// otherwise return the user
		return $return;
	}
	
	//	check user memberships
	public function isMember( $groupname, $userdn, &$checkedgroups=array() )
	{
		// check to see if group has already been checked, if it has return false
		// otherwise add it to the array of checked groups, this will prevent
		// getting stuck in a check loop
		if ( in_array( $groupname, $checkedgroups ) ) return false;
		$checkedgroups[] = $groupname;
		
		// search for group to get dn and members
		if ( ! $results = @ldap_search(
			$this->_ldap_connection,
			$this->_params['ldap_group_ou'],
			sprintf( $this->_params['ldap_group_filter'], str_replace( array( "(", ")", "*" ), array( "\(", "\)", "\*" ), $groupname ) ),
			array( 'dn', 'cn', 'member' )
		)) return false; // no GROUP with that name found
		
		// get the requested attributes from the query
		$group = @ldap_get_entries( $this->_ldap_connection, $results );
		
		// check to make sure we haven't found two groups
		if ( $group['count'] > 1 ) throw new exception("Too many groups returned by LDAP.");
		
		// if this is true the user is a direct member of the group
		if ( @ldap_compare( $this->_ldap_connection, $group[0]['dn'], 'member', $userdn ) === true ) return true;
		
		// check to see if any other members were returned
		if ( ! isset( $group[0]['member'] ) ) return false;
		
		// otherwise we need to search any member groups
		for ( $i=0; $i < $group[0]['member']['count']; $i++ )
		{
			// don't bother checking groups that have already been checked
			$groupname = preg_replace( "/CN=([^,]+),.*/i", "$1", $group[0]['member'][$i] );
			if ( $this->isMember( $groupname, $userdn, $checkedgroups ) ) return true;
		}
		
		// if we never find it return false
		return false;
	}

}
?>

Then modify /system/api/sessionmanager.inc.php and add the following code after line 63 (the beginning of the login function):

<?php
		//	LDAP AUTH START ***********************************************************************
		
		//	options for System_Api_LDAPHelper
		$ldap_params = array(
			'ldap_domain_suffix'	=> 'yourdomain.local',
			'ldap_server'			=> 'yourdomainconroller.yourdomain.local',
			'ldap_user_ou'			=> 'OU=Users,OU=YOURDOMAIN,DC=gsnwgl,DC=local',
			'ldap_group_ou'			=> 'OU=YOURDOMAIN,DC=gsnwgl,DC=local'
		);
		
		//	user groups in Acitive Directory for auth levels in WebIssues
		//	if these groups do not exist or users are not assigned to them you won't be able to login
		$ldap_user_group = 'WebIssues Users';
		$ldap_admin_group = 'WebIssues Administrators';
		
		//	**** don't modify below this line ****
		
		$ldap = new System_Api_LDAPHelper( $ldap_params );
		$userManager = new System_Api_UserManager;
		
		//	user is authenticated to LDAP
		if ( $ldap->bind( $login, $password ) )
		{
			$ldap_user = $ldap->getUserInfo( $login );
			
			//	setup or update user password
			$query = 'SELECT user_id FROM {users} WHERE user_login = %s OR user_name = %s';
			if ( ! ( $userId = $this->connection->queryScalar( $query, $login, $ldap_user['displayname'] ) ) )
			{
				$userId = $userManager->addUser( $login, $ldap_user['displayname'], $password, 0 );
			}
			else
			{
				$passwordHash = new System_Core_PasswordHash();
				$newHash = $passwordHash->hashPassword( $password );
				$query = 'UPDATE {users} SET user_passwd = %s, passwd_temp = %d WHERE user_id = %d';
				$this->connection->execute( $query, $newHash, 0, $userId );
			}
			
			//	set access level
			$accessLevel = System_Const::NoAccess;
			if ( $ldap->isMember( $ldap_user_group, $ldap_user['dn'] ) ) $accessLevel = System_Const::NormalAccess;
			if ( $ldap->isMember( $ldap_admin_group, $ldap_user['dn'] ) ) $accessLevel = System_Const::AdministratorAccess;
			$query = 'UPDATE {users} SET user_access = %d WHERE user_login = %s';
			$this->connection->execute( $query, $accessLevel, $login );
			
			//	update email address
			if ( isset( $ldap_user['mail'] ) && ! empty( $ldap_user['mail']) )
			{
				$query = 'SELECT COUNT(*) FROM {preferences} WHERE pref_key = %s AND user_id = %d';
				if ( ! $this->connection->queryScalar( $query, 'email', $userId) )
				{
					$query = 'INSERT INTO {preferences} ( user_id, pref_key, pref_value ) VALUES ( %d, %s, %s )';
					$this->connection->execute( $query, $userId, 'email', $ldap_user['mail'] );
				}
				else
				{
					$query = 'UPDATE {preferences} SET pref_value = %s WHERE user_id = %d AND pref_key = %s';
					$this->connection->execute( $query, $ldap_user['mail'], $userId, 'email' );
				}
			}
			
		}
		//	user is not authenticated to LDAP and not the admin user
		elseif ( $login != 'admin' )
		{
			$query = 'UPDATE {users} SET user_access = %d WHERE user_login = %s';
			$this->connection->execute( $query, System_Const::NoAccess, $login );
		}
		//	LDAP AUTH END *************************************************************************/

?>

Note: don't include the PHP tags into sessionmanager.inc.php