LDAP to DS: authenticate & query

Small, short code snippets that other people may find useful. Do you have a good regex that you would like to share? Share it! Even better, the code can be commented on, and improved.

Moderator: General Moderators

Post Reply
User avatar
pickle
Briney Mod
Posts: 6445
Joined: Mon Jan 19, 2004 6:11 pm
Location: 53.01N x 112.48W
Contact:

LDAP to DS: authenticate & query

Post by pickle »

Wrote this little gem to simplify my contact with NDS via LDAP. You can use this object both for verification and to search your Directory Services tree. I've only used this on Novell Directory Services, but I imagine it would work on other implementations as well. Examples follow.

Edit: Updated the code to have 'camel caps', make sure { } are surrounding code after ifs and elses, and added some more error checking to provide a clean error if the initialization variables aren't of the required type and if attributes aren't set when calling getAttributeResults()

Code: Select all

<?
//**********************
// LDAPReader class
// For connecting to Directory Services via LDAP
//
// Author: Dylan Anderson
// Date: April 21, 2006
//**********************
 
class LDAPReader
{
  //connection variables
  var $host;
  var $username;
  var $password;
  var $contexts;
  var $bound_context;
  var $connection;
 
  var $search_recursive;
 
  //results variables
  var $filter;
  var $attributes;
  var $results;
 
  //*****************************************//
  // constructor function:
  //
  // $contexts must be an array of contexts listed
  // in the order you want to use them to bind
  //*****************************************//
  function LDAP($host,$username,$password,$contexts)
  {
    if(is_string($host))
    {
      $this->host = $host;
    }
    else
    {
      $this->_dumpError('Hostname is not a string');
    }
 
    if(is_string($username))
    {
      $this->username = $username;
    }
    else
    {
      $this->_dumpError('Username is not a string');
    }
 
    if(is_string($host))
    {
      $this->password = $password;
    }
    else
    {
      $this->_dumpError('Password is not a string');
    }
 
    if(is_array($contexts))
    {
      $this->contexts = $contexts;
    }
    else
    {
      $this->_dumpError('Contexts are not in an array');
    }
 
    $this->connect();
  }
 
  //*****************************************//
  // setFilter
  //
  // sets the filter to use in the query.
  //*****************************************//
  function setFilter($p_filter)
  {
    $this->filter = $p_filter;
  }
  
  //*****************************************//
  // setFilterHr
  //
  // sets the filter to use in the query.
  // Makes the filter from an array of values, rather than 
  // just storing the sent string.
  //
  // $boolean_argument can be & (and) or | (or)
  //*****************************************//
  function setFilterHR($p_filters,$boolean_argument = "&")
  {
    if(count($p_filters) && is_array($p_filters))
    {
      $this->filter = '('.$boolean_argument;
 
      foreach($p_filters as $curr_filter)
      {
    $this->filter .= '('.$curr_filter.')';
      }
      $this->filter .= ')';
      return true;
    }
  }
 
 
  //*****************************************//
  // setAttributes
  //
  // sets the attributes we want
  //*****************************************//
  function setAttributes($p_attributes)
  {
    if(count($p_attributes) && is_array($p_attributes))
    {
      $this->attributes = $p_attributes;
    }
  }
 
  //*****************************************//
  // setRecursiveSearch
  //
  // Set whether we want to just do our query in
  // in the context in which we've bound, or if 
  // we want to search in all sub-levels as well
  //*****************************************//
  function setRecursiveSearch($search)
  {
    $this->search_recursive = $search;
  }
 
    
  //*****************************************//
  // connect
  //
  // loops through each context and tries to connect
  //*****************************************//
  function connect()
  {
    //connect to the server
    $this->connection = @ldap_connect($this->host);
 
    //dump an error message if we didn't connect
    if(!$this->connection)
    {
      $this->_dumpError('LDAP connection error #'.$this->getErrNum().': '.$this->getError());
    }
    else
    {
      //try to bind - loop through each context
      foreach($this->contexts as $context)
      {
    $ldap_bind = @ldap_bind($this->connection,'cn='.$this->username.','.$context,$this->password);
    if($ldap_bind)
    {
      $this->bound_context = $context;
      break;
    }
      }
    }
  }
  
  //*****************************************//  
  // query
  //
  // does the search
  //*****************************************//
  function query()
  {
    $search_function = ($this->search_recursive) ? "ldap_search" : "ldap_list";
 
    //if asked for particular attributes, pass them along in the query
    if(isset($this->filter))
    {
      if(is_array($this->attributes) && count($this->attributes))
      {
    $results = @$search_function($this->connection,$this->bound_context,$this->filter,$this->attributes);
      }
      else
      {
    $results = @$search_function($this->connection,$this->bound_context,$this->filter);
      }
    }
    else
    {
      $this->_dumpError('Error while conducting query: no filter set');
    }
 
    //dump an error if we get FALSE as a result
    if(!$results)
    {
      $this->_dumpError('LDAP search error #'.$this->getErrNum().': '.$this->getError());
    }
    else
    {
      $info = @ldap_get_entries($this->connection,$results);
      if(!$info)
      {
    $this->_dumpError('LDAP query results retrieval error #'.$this->getErrNum().': '.$this->getError());
      }
      else
      {
    $this->results = $info;
    $ret_val = true;
      }
    }
    return($ret_val);
  }
 
  //*****************************************//
  // isBound
  // 
  // returns whether or not a bind has been made
  // useful for checking credentials
  //*****************************************//  
  function isBound()
  {
    $ret_val = ($this->bound_context) ? true : false;
    return($ret_val);
  }
 
  //*****************************************//
  // getResults
  // 
  // returns the results generated from query()
  //*****************************************//  
  function getResults()
  {
    if(!isset($this->results))
    {
      $this->_dumpError('No results set.');
    }
    else
    {
      return($this->results);
    }
  }
 
  //*****************************************//
  // getAttributeResults
  // 
  // returns a cleaned $results array that only contains
  // values specifically pertaining to what we asked for
  //*****************************************//  
  function getAttributeResults()
  {
    if(!isset($this->results))
    {
      $this->_dumpError('No results set.');
    }
    else if(!isset($this->attributes))
    {
      $this->_dumpError('No attributes set.');
    }
    else
    {
      //loop through each record in the results
      foreach($this->results as $record)
      {
    //loop through each attribute we asked for
    foreach($this->attributes as $asked_attribute)
    {
      //if the attribute is found
      if(isset($record[$asked_attribute]))
      {
        //remove the first entry, which is the 'count' element
        array_shift($record[$asked_attribute]);
        $current_record_values[$asked_attribute] = $record[$asked_attribute];
      }
    }
    if(is_array($current_record_values))
    {
      $attribute_values[] = $current_record_values;
    }
    unset($current_record_values);
      }
      return($attribute_values);
    }
  }
 
 
  //*****************************************//  
  // _dumpError
  //
  // Dump an error
  //*****************************************//  
  function _dumpError($message)
  {
    echo $message;
    exit();
  }
 
  //*****************************************//  
  // getErrNum
  //
  // returns the last error number generated
  //*****************************************//  
  function getErrNum()
  {
    return(ldap_errno($this->connection));
  }
 
  //*****************************************//  
  // getError
  //
  // returns the last error generated
  //*****************************************//  
  function getError()
  {
    return(ldap_error($this->connection));
  }
}
?>
 
Example usage

Authentication:

Code: Select all

 
<?PHP
$username = 'username_from_form';
$password = 'password_from_form';
 
require_once('LDAP.php');//stores the object of course
$ldap_server_uri = "ldap://192.168.1.1";
$ldap_contexts = array('o=House');
$LDAP = new LDAP($ldap_server_uri,$username,$password,$contexts);
if($LDAP->bound())
{
   echo "Yay, you authenticated";
}
else
{
   echo "Get out of here you bum!";
}
 
You'll notice that the context is stored in an array. This is so that you can authenticated against multiple contexts. If this is what you need to do, just put each context as a new element in that array.





Searching
Want to retrieve all the email addresses for all employees in your tree?

Code: Select all

<?PHP
require_once('LDAP.php');
$username = "secretLDAPuser";
$password = "secretLDAPpassword";
$ldap_server_uri = "ldap://192.168.1.1";
$ldap_contexts = array('o=BigCompany');
 
$LDAP = new LDAP($ldap_server_uri,$username,$password,$ldap_contexts);
$LDAP->set_filter('groupMembership=employees');
$LDAP->set_attributes(array('mail'));
 
$LDAP->query();
 
echo '<pre>';
print_r($LDAP->get_results(););
print_r($LDAP->get_attribute_results());
echo '</pre>';
?>
 
Will print out:

Code: Select all

 
Array
(
    [count] => 2
    [0] => Array
        (
            [mail] => Array
                (
                    [count] => 1
                    [0] => joe@hardyboys.com
                )
            [0] => mail
            [count] => 1
            [dn] => cn=joe,o=HardyBoys
        )
    [1] => Array
        (
            [mail] => Array
                (
                    [count] => 1
                    [0] => frank@hardyboys.com
                )
            [0] => mail
            [count] => 1
            [dn] => cn=frank,o=HardyBoys
        )
)
Array
(
    [0] => Array
        (
            [mail] => Array
                (
                    [0] => joe@hardyboys.com
                )
        )
    [1] => Array
        (
            [mail] => Array
                (
                    [0] => frank@hardyboys.com
                )
        )
)
 
As you can see, the get_results() function gives you absolutely everything returned by your query. get_attribute_results() only returns you the information you asked for. Please note that the get_attribute_results() function is fine tuned to work in NDS - your implementation might arrange information differently.



Another important example is the use of set_filter() and set_filter_hr(). The difference is that set_filter() just uses the string you pass as the filter, whereas set_filter_hr() builds it from what you pass. For example:

Code: Select all

$LDAP->set_filter_hr(array('cn=joe','cn=frank'),'|');
Will generate the filter:

Code: Select all

(|(cn=joe)(cn=frank))
Use this function whenever you want of course, but it was intended to simplify the process of creating the filter. If you don't pass the 2nd argument, it defaults to boolean and (&).


Finally, there's the $search_recursive variable. Setting this to true will direct the class to search all parent contexts rather then just the one in which you are bound. This has obvious applications.


I hope others find this class useful - let me know if you have any improvements/tweaks/code cleanup.
Last edited by pickle on Fri Apr 21, 2006 5:27 pm, edited 2 times in total.
Real programmers don't comment their code. If it was hard to write, it should be hard to understand.
User avatar
Christopher
Site Administrator
Posts: 13596
Joined: Wed Aug 25, 2004 7:54 pm
Location: New York, NY, US

Post by Christopher »

Really niice! :) I only have one code comment, and that is about this code:

Code: Select all

$search_function = ($this->search_recursive) ? "ldap_search" : "ldap_list";

    //if asked for particular attributes, pass them along in the query
    if(is_array($this->attributes) && count($this->attributes))
      $results = @$search_function($this->connection,$this->bound_context,$this->filter,$this->attributes);
    else
      $results = @$search_function($this->connection,$this->bound_context,$this->filter);
I know this is a PHP way to do things, but nested ifs would probably be clearer even if it is little more code.

My other comment is about naming. I would rename the class LDAPReader because it only reads. I would suggest using camel caps for method names as that is the PHP norm. And I would rename search_recusive() to setRecursiveSearch() and bound() to isBound() and the error methods could just be getError() and geErrno().
(#10850)
User avatar
pickle
Briney Mod
Posts: 6445
Joined: Mon Jan 19, 2004 6:11 pm
Location: 53.01N x 112.48W
Contact:

Post by pickle »

All very good points and all ones I'll institute later today. I'm not sure what you mean by 'camel caps' though.
Real programmers don't comment their code. If it was hard to write, it should be hard to understand.
User avatar
n00b Saibot
DevNet Resident
Posts: 1452
Joined: Fri Dec 24, 2004 2:59 am
Location: Lucknow, UP, India
Contact:

Post by n00b Saibot »

camelCasing it is called - like in setErrorHandler() or myFriendPickle() ;)

first letter of first word is small case, rest are Title Case.
User avatar
pickle
Briney Mod
Posts: 6445
Joined: Mon Jan 19, 2004 6:11 pm
Location: 53.01N x 112.48W
Contact:

Post by pickle »

Updated the code with your changes. See the 'Edit' note in the original post.
Real programmers don't comment their code. If it was hard to write, it should be hard to understand.
User avatar
Christopher
Site Administrator
Posts: 13596
Joined: Wed Aug 25, 2004 7:54 pm
Location: New York, NY, US

Post by Christopher »

Looks great. You should probably have default values for all the variables that are not set in the constructor.

There is probably other little stuff like:

Code: Select all

function isBound()
  {
    return $this->bound_context ? true : false;
  }
You can avoid a temp var and as I recall there is a reason in PHP why you should avoid parens if they are not needed (anybody remember why?).
(#10850)
alex.barylski
DevNet Evangelist
Posts: 6267
Joined: Tue Dec 21, 2004 5:00 pm
Location: Winnipeg

Post by alex.barylski »

[quote="arborint"]Looks great. You should probably have default values for all the variables that are not set in the constructor.

There is probably other little stuff like:

Code: Select all

function isBound()
  {
    return $this->bound_context ? true : false;
  }
You can avoid a temp var and as I recall there is a reason in PHP why you should avoid parens if they are not needed (anybody remember why?).[/quote]

Remember why, not so much...understand why...I can take a guess...

Likely due to recursion while parsing, as this rule of thumb applies to any interpreted language...in SQL you should avoid using unnessecary parens also...as recursion is generally a bad idea for performance reasons unless it required.

Without getting right into it, I think thats why...

Cheers :)
Post Reply