Auth
[ class tree: Auth ] [ index: Auth ] [ all elements ]

Source for file LDAP.php

Documentation is available at LDAP.php

  1. <?php
  2. //
  3. // +----------------------------------------------------------------------+
  4. // | PHP Version 4                                                        |
  5. // +----------------------------------------------------------------------+
  6. // | Copyright (c) 1997-2005 The PHP Group                                |
  7. // +----------------------------------------------------------------------+
  8. // | This source file is subject to version 2.02 of the PHP license,      |
  9. // | that is bundled with this package in the file LICENSE, and is        |
  10. // | available at through the world-wide-web at                           |
  11. // | http://www.php.net/license/2_02.txt.                                 |
  12. // | If you did not receive a copy of the PHP license and are unable to   |
  13. // | obtain it through the world-wide-web, please send a note to          |
  14. // | license@php.net so we can mail you a copy immediately.               |
  15. // +----------------------------------------------------------------------+
  16. // | Authors: Jan Wagner <wagner@netsols.de>                              |
  17. // +----------------------------------------------------------------------+
  18. //
  19. // $Id: LDAP.php,v 1.25 2006/02/21 05:50:21 aashley Exp $
  20. //
  21.  
  22. require_once "Auth/Container.php";
  23. require_once "PEAR.php";
  24.  
  25. /**
  26.  * Storage driver for fetching login data from LDAP
  27.  *
  28.  * This class is heavily based on the DB and File containers. By default it
  29.  * connects to localhost:389 and searches for uid=$username with the scope
  30.  * "sub". If no search base is specified, it will try to determine it via
  31.  * the namingContexts attribute. It takes its parameters in a hash, connects
  32.  * to the ldap server, binds anonymously, searches for the user, and tries
  33.  * to bind as the user with the supplied password. When a group was set, it
  34.  * will look for group membership of the authenticated user. If all goes
  35.  * well the authentication was successful.
  36.  *
  37.  * Parameters:
  38.  *
  39.  * host:        localhost (default), ldap.netsols.de or 127.0.0.1
  40.  * port:        389 (default) or 636 or whereever your server runs
  41.  * url:         ldap://localhost:389/
  42.  *              useful for ldaps://, works only with openldap2 ?
  43.  *              it will be preferred over host and port
  44.  * version:     LDAP version to use, ususally 2 (default) or 3,
  45.  *              must be an integer!
  46.  * referrals:   If set, determines whether the LDAP library automatically
  47.  *              follows referrals returned by LDAP servers or not. Possible
  48.  *              values are true (default) or false.
  49.  * binddn:      If set, searching for user will be done after binding
  50.  *              as this user, if not set the bind will be anonymous.
  51.  *              This is reported to make the container work with MS
  52.  *              Active Directory, but should work with any server that
  53.  *              is configured this way.
  54.  *              This has to be a complete dn for now (basedn and
  55.  *              userdn will not be appended).
  56.  * bindpw:      The password to use for binding with binddn
  57.  * basedn:      the base dn of your server
  58.  * userdn:      gets prepended to basedn when searching for user
  59.  * userscope:   Scope for user searching: one, sub (default), or base
  60.  * userattr:    the user attribute to search for (default: uid)
  61.  * userfilter:  filter that will be added to the search filter
  62.  *              this way: (&(userattr=username)(userfilter))
  63.  *              default: (objectClass=posixAccount)
  64.  * attributes:  array of additional attributes to fetch from entry.
  65.  *              these will added to auth data and can be retrieved via
  66.  *              Auth::getAuthData(). An empty array will fetch all attributes,
  67.  *              array('') will fetch no attributes at all (default)
  68.  * attrformat:  The returned format of the additional data defined in the
  69.  *              'attributes' option. Two formats are available.
  70.  *              LDAP returns data formatted in a
  71.  *              multidimensional array where each array starts with a
  72.  *              'count' element providing the number of attributes in the
  73.  *              entry, or the number of values for attributes. When set
  74.  *              to this format, the only way to retrieve data from the
  75.  *              Auth object is by calling getAuthData('attributes').
  76.  *              AUTH returns data formatted in a
  77.  *              structure more compliant with other Auth Containers,
  78.  *              where each attribute element can be directly called by
  79.  *              getAuthData() method from Auth.
  80.  *              For compatibily with previous LDAP container versions,
  81.  *              the default format is LDAP.
  82.  * groupdn:     gets prepended to basedn when searching for group
  83.  * groupattr:   the group attribute to search for (default: cn)
  84.  * groupfilter: filter that will be added to the search filter when
  85.  *              searching for a group:
  86.  *              (&(groupattr=group)(memberattr=username)(groupfilter))
  87.  *              default: (objectClass=groupOfUniqueNames)
  88.  * memberattr : the attribute of the group object where the user dn
  89.  *              may be found (default: uniqueMember)
  90.  * memberisdn:  whether the memberattr is the dn of the user (default)
  91.  *              or the value of userattr (usually uid)
  92.  * group:       the name of group to search for
  93.  * groupscope:  Scope for group searching: one, sub (default), or base
  94.  * debug:       Enable/Disable debugging output (default: false)
  95.  *
  96.  * To use this storage container, you have to use the following syntax:
  97.  *
  98.  * <?php
  99.  * ...
  100.  *
  101.  * $a = new Auth("LDAP", array(
  102.  *       'host' => 'localhost',
  103.  *       'port' => '389',
  104.  *       'version' => 3,
  105.  *       'basedn' => 'o=netsols,c=de',
  106.  *       'userattr' => 'uid'
  107.  *       'binddn' => 'cn=admin,o=netsols,c=de',
  108.  *       'bindpw' => 'password'));
  109.  *
  110.  * $a2 = new Auth('LDAP', array(
  111.  *       'url' => 'ldaps://ldap.netsols.de',
  112.  *       'basedn' => 'o=netsols,c=de',
  113.  *       'userscope' => 'one',
  114.  *       'userdn' => 'ou=People',
  115.  *       'groupdn' => 'ou=Groups',
  116.  *       'groupfilter' => '(objectClass=posixGroup)',
  117.  *       'memberattr' => 'memberUid',
  118.  *       'memberisdn' => false,
  119.  *       'group' => 'admin'
  120.  *       ));
  121.  *
  122.  * This is a full blown example with user/group checking to an Active Directory
  123.  *
  124.  * $a3 = new Auth('LDAP', array(
  125.  *       'host' => 'ldap.netsols.de',
  126.  *       'port' => 389,
  127.  *       'version' => 3,
  128.  *       'referrals' => false,
  129.  *       'basedn' => 'dc=netsols,dc=de',
  130.  *       'binddn' => 'cn=Jan Wagner,cn=Users,dc=netsols,dc=de',
  131.  *       'bindpw' => 'password',
  132.  *       'userattr' => 'samAccountName',
  133.  *       'userfilter' => '(objectClass=user)',
  134.  *       'attributes' => array(''),
  135.  *       'group' => 'testing',
  136.  *       'groupattr' => 'samAccountName',
  137.  *       'groupfilter' => '(objectClass=group)',
  138.  *       'memberattr' => 'member',
  139.  *       'memberisdn' => true,
  140.  *       'groupdn' => 'cn=Users',
  141.  *       'groupscope' => 'one',
  142.  *       'debug' => true);
  143.  *
  144.  * The parameter values have to correspond
  145.  * to the ones for your LDAP server of course.
  146.  *
  147.  * When talking to a Microsoft ActiveDirectory server you have to
  148.  * use 'samaccountname' as the 'userattr' and follow special rules
  149.  * to translate the ActiveDirectory directory names into 'basedn'.
  150.  * The 'basedn' for the default 'Users' folder on an ActiveDirectory
  151.  * server for the ActiveDirectory Domain (which is not related to
  152.  * its DNS name) "win2000.example.org" would be:
  153.  * "CN=Users, DC=win2000, DC=example, DC=org'
  154.  * where every component of the domain name becomes a DC attribute
  155.  * of its own. If you want to use a custom users folder you have to
  156.  * replace "CN=Users" with a sequence of "OU" attributes that specify
  157.  * the path to your custom folder in reverse order.
  158.  * So the ActiveDirectory folder
  159.  *   "win2000.example.org\Custom\Accounts"
  160.  * would become
  161.  *   "OU=Accounts, OU=Custom, DC=win2000, DC=example, DC=org'
  162.  *
  163.  * It seems that binding anonymously to an Active Directory
  164.  * is not allowed, so you have to set binddn and bindpw for
  165.  * user searching,
  166.  *
  167.  * Example a3 shows a tested example for connection to Windows 2000
  168.  * Active Directory
  169.  *
  170.  * @author   Jan Wagner <wagner@netsols.de>
  171.  * @package  Auth
  172.  * @version  $Revision: 1.25 $
  173.  */
  174.  
  175. {
  176.     /**
  177.      * Options for the class
  178.      * @var array 
  179.      */
  180.     var $options = array();
  181.  
  182.     /**
  183.      * Connection ID of LDAP Link
  184.      * @var string 
  185.      */
  186.     var $conn_id = false;
  187.  
  188.     /**
  189.      * Constructor of the container class
  190.      *
  191.      * @param  $params, associative hash with host,port,basedn and userattr key
  192.      * @return object Returns an error object if something went wrong
  193.      */
  194.     function Auth_Container_LDAP($params)
  195.     {
  196.         if (false === extension_loaded('ldap')) {
  197.             return PEAR::raiseError('Auth_Container_LDAP: LDAP Extension not loaded'41PEAR_ERROR_DIE);
  198.         }
  199.  
  200.         $this->_setDefaults();
  201.  
  202.         if (is_array($params)) {
  203.             $this->_parseOptions($params);
  204.         }
  205.     }
  206.  
  207.     // }}}
  208.     // {{{ _connect()
  209.  
  210.     /**
  211.      * Connect to the LDAP server using the global options
  212.      *
  213.      * @access private
  214.      * @return object  Returns a PEAR error object if an error occurs.
  215.      */
  216.     function _connect()
  217.     {
  218.         // connect
  219.         if (isset($this->options['url']&& $this->options['url'!= ''{
  220.             $this->_debug('Connecting with URL'__LINE__);
  221.             $conn_params = array($this->options['url']);
  222.         else {
  223.             $this->_debug('Connecting with host:port'__LINE__);
  224.             $conn_params = array($this->options['host']$this->options['port']);
  225.         }
  226.  
  227.         if (($this->conn_id = @call_user_func_array('ldap_connect'$conn_params)) === false{
  228.             return PEAR::raiseError('Auth_Container_LDAP: Could not connect to server.'41);
  229.         }
  230.         $this->_debug('Successfully connected to server'__LINE__);
  231.  
  232.         // switch LDAP version
  233.         if (is_int($this->options['version']&& $this->options['version'> 2{
  234.             $this->_debug("Switching to LDAP version {$this->options['version']}"__LINE__);
  235.             @ldap_set_option($this->conn_idLDAP_OPT_PROTOCOL_VERSION$this->options['version']);
  236.         }
  237.  
  238.         // switch LDAP referrals
  239.         if (is_bool($this->options['referrals'])) {
  240.           $this->_debug("Switching to LDAP referrals {$this->options['referrals']}"__LINE__);
  241.           @ldap_set_option($this->conn_idLDAP_OPT_REFERRALS$this->options['referrals']);
  242.         }
  243.  
  244.         // bind with credentials or anonymously
  245.         if ($this->options['binddn'&& $this->options['bindpw']{
  246.             $this->_debug('Binding with credentials'__LINE__);
  247.             $bind_params = array($this->conn_id$this->options['binddn']$this->options['bindpw']);
  248.         else {
  249.             $this->_debug('Binding anonymously'__LINE__);
  250.             $bind_params = array($this->conn_id);
  251.         }
  252.  
  253.         // bind for searching
  254.         if ((@call_user_func_array('ldap_bind'$bind_params)) == false{
  255.             $this->_debug();
  256.             $this->_disconnect();
  257.             return PEAR::raiseError("Auth_Container_LDAP: Could not bind to LDAP server."41);
  258.         }
  259.         $this->_debug('Binding was successful'__LINE__);
  260.     }
  261.  
  262.     /**
  263.      * Disconnects (unbinds) from ldap server
  264.      *
  265.      * @access private
  266.      */
  267.     function _disconnect()
  268.     {
  269.         if ($this->_isValidLink()) {
  270.             $this->_debug('disconnecting from server');
  271.             @ldap_unbind($this->conn_id);
  272.         }
  273.     }
  274.  
  275.     /**
  276.      * Tries to find Basedn via namingContext Attribute
  277.      *
  278.      * @access private
  279.      */
  280.     function _getBaseDN()
  281.     {
  282.         if ($this->options['basedn'== "" && $this->_isValidLink()) {
  283.             $this->_debug("basedn not set, searching via namingContexts."__LINE__);
  284.  
  285.             $result_id @ldap_read($this->conn_id"""(objectclass=*)"array("namingContexts"));
  286.  
  287.             if (@ldap_count_entries($this->conn_id$result_id== 1{
  288.  
  289.                 $this->_debug("got result for namingContexts"__LINE__);
  290.  
  291.                 $entry_id @ldap_first_entry($this->conn_id$result_id);
  292.                 $attrs @ldap_get_attributes($this->conn_id$entry_id);
  293.                 $basedn $attrs['namingContexts'][0];
  294.  
  295.                 if ($basedn != ""{
  296.                     $this->_debug("result for namingContexts was $basedn"__LINE__);
  297.                     $this->options['basedn'$basedn;
  298.                 }
  299.             }
  300.             @ldap_free_result($result_id);
  301.         }
  302.  
  303.         // if base ist still not set, raise error
  304.         if ($this->options['basedn'== ""{
  305.             return PEAR::raiseError("Auth_Container_LDAP: LDAP search base not specified!"41PEAR_ERROR_DIE);
  306.         }
  307.         return true;
  308.     }
  309.  
  310.     /**
  311.      * determines whether there is a valid ldap conenction or not
  312.      *
  313.      * @accessd private
  314.      * @return boolean 
  315.      */
  316.     function _isValidLink()
  317.     {
  318.         if (is_resource($this->conn_id)) {
  319.             if (get_resource_type($this->conn_id== 'ldap link'{
  320.                 return true;
  321.             }
  322.         }
  323.         return false;
  324.     }
  325.  
  326.     /**
  327.      * Set some default options
  328.      *
  329.      * @access private
  330.      */
  331.     function _setDefaults()
  332.     {
  333.         $this->options['url']         '';
  334.         $this->options['host']        'localhost';
  335.         $this->options['port']        '389';
  336.         $this->options['version']     = 2;
  337.         $this->options['referrals']   = true;
  338.         $this->options['binddn']      '';
  339.         $this->options['bindpw']      '';
  340.         $this->options['basedn']      '';
  341.         $this->options['userdn']      '';
  342.         $this->options['userscope']   'sub';
  343.         $this->options['userattr']    'uid';
  344.         $this->options['userfilter']  '(objectClass=posixAccount)';
  345.         $this->options['attributes']  = array('')// no attributes
  346.         $this->options['attrformat']  'LDAP'// returns attribute array as PHP LDAP functions return it
  347.      // $this->options['attrformat']  = 'AUTH'; // returns attribute like other Auth containers
  348.         $this->options['group']       '';
  349.         $this->options['groupdn']     '';
  350.         $this->options['groupscope']  'sub';
  351.         $this->options['groupattr']   'cn';
  352.         $this->options['groupfilter''(objectClass=groupOfUniqueNames)';
  353.         $this->options['memberattr']  'uniqueMember';
  354.         $this->options['memberisdn']  = true;
  355.         $this->options['debug']       = false;
  356.     }
  357.  
  358.     /**
  359.      * Parse options passed to the container class
  360.      *
  361.      * @access private
  362.      * @param  array 
  363.      */
  364.     function _parseOptions($array)
  365.     {
  366.         foreach ($array as $key => $value{
  367.             if (array_key_exists($key$this->options)) {
  368.                 $this->options[$key$value;
  369.             }
  370.         }
  371.     }
  372.  
  373.     /**
  374.      * Get search function for scope
  375.      *
  376.      * @param  string scope
  377.      * @return string ldap search function
  378.      */
  379.     function _scope2function($scope)
  380.     {
  381.         switch($scope{
  382.         case 'one':
  383.             $function 'ldap_list';
  384.             break;
  385.         case 'base':
  386.             $function 'ldap_read';
  387.             break;
  388.         default:
  389.             $function 'ldap_search';
  390.             break;
  391.         }
  392.         return $function;
  393.     }
  394.  
  395.     /**
  396.      * Fetch data from LDAP server
  397.      *
  398.      * Searches the LDAP server for the given username/password
  399.      * combination.  Escapes all LDAP meta characters in username
  400.      * before performing the query.
  401.      *
  402.      * @param  string Username
  403.      * @param  string Password
  404.      * @return boolean 
  405.      */
  406.     function fetchData($username$password)
  407.     {
  408.         $this->_connect();
  409.         $this->_getBaseDN();
  410.  
  411.         // UTF8 Encode username for LDAPv3
  412.         if (@ldap_get_option($this->conn_idLDAP_OPT_PROTOCOL_VERSION$ver&& $ver == 3{
  413.             $this->_debug('UTF8 encoding username for LDAPv3'__LINE__);
  414.             $username utf8_encode($username);
  415.         }
  416.         // make search filter
  417.         $filter sprintf('(&(%s=%s)%s)',
  418.                           $this->options['userattr'],
  419.                           $this->_quoteFilterString($username),
  420.                           $this->options['userfilter']);
  421.         // make search base dn
  422.         $search_basedn $this->options['userdn'];
  423.         if ($search_basedn != '' && substr($search_basedn-1!= ','{
  424.             $search_basedn .= ',';
  425.         }
  426.         $search_basedn .= $this->options['basedn'];
  427.  
  428.         // attributes
  429.         $attributes $this->options['attributes'];
  430.  
  431.         // make functions params array
  432.         $func_params = array($this->conn_id$search_basedn$filter$attributes);
  433.  
  434.         // search function to use
  435.         $func_name $this->_scope2function($this->options['userscope']);
  436.  
  437.         $this->_debug("Searching with $func_name and filter $filter in $search_basedn"__LINE__);
  438.  
  439.         // search
  440.         if (($result_id @call_user_func_array($func_name$func_params)) == false{
  441.             $this->_debug('User not found'__LINE__);
  442.         elseif (@ldap_count_entries($this->conn_id$result_id== 1// did we get just one entry?
  443.  
  444.             $this->_debug('User was found'__LINE__);
  445.  
  446.             // then get the user dn
  447.             $entry_id @ldap_first_entry($this->conn_id$result_id);
  448.             $user_dn  @ldap_get_dn($this->conn_id$entry_id);
  449.  
  450.             // fetch attributes
  451.             if ($attributes @ldap_get_attributes($this->conn_id$entry_id)) {
  452.  
  453.                 if (is_array($attributes&& isset($attributes['count']&&
  454.                      $attributes['count'> 0{
  455.  
  456.                     // ldap_get_attributes() returns a specific multi dimensional array
  457.                     // format containing all the attributes and where each array starts
  458.                     // with a 'count' element providing the number of attributes in the
  459.                     // entry, or the number of values for attribute. For compatibility
  460.                     // reasons, it remains the default format returned by LDAP container
  461.                     // setAuthData().
  462.                     // The code below optionally returns attributes in another format,
  463.                     // more compliant with other Auth containers, where each attribute
  464.                     // element are directly set in the 'authData' list. This option is
  465.                     // enabled by setting 'attrformat' to
  466.                     // 'AUTH' in the 'options' array.
  467.                     // eg. $this->options['attrformat'] = 'AUTH'
  468.  
  469.                     if strtoupper($this->options['attrformat']== 'AUTH' {
  470.                         $this->_debug('Saving attributes to Auth data in AUTH format'__LINE__);
  471.                         unset ($attributes['count']);
  472.                         foreach ($attributes as $attributeName => $attributeValue {
  473.                             if (is_int($attributeName)) continue;
  474.                             if (is_array($attributeValue&& isset($attributeValue['count'])) {
  475.                                 unset ($attributeValue['count']);
  476.                             }
  477.                             if (count($attributeValue)<=1$attributeValue $attributeValue[0];
  478.                             $this->_auth_obj->setAuthData($attributeName$attributeValue);
  479.                         }
  480.                     }
  481.                     else
  482.                     {
  483.                         $this->_debug('Saving attributes to Auth data in LDAP format'__LINE__);
  484.                         $this->_auth_obj->setAuthData('attributes'$attributes);
  485.                     }
  486.                 }
  487.             }
  488.             @ldap_free_result($result_id);
  489.  
  490.             // need to catch an empty password as openldap seems to return TRUE
  491.             // if anonymous binding is allowed
  492.             if ($password != ""{
  493.                 $this->_debug("Bind as $user_dn"__LINE__);
  494.  
  495.                 // try binding as this user with the supplied password
  496.                 if (@ldap_bind($this->conn_id$user_dn$password)) {
  497.                     $this->_debug('Bind successful'__LINE__);
  498.  
  499.                     // check group if appropiate
  500.                     if (strlen($this->options['group'])) {
  501.                         // decide whether memberattr value is a dn or the username
  502.                         $this->_debug('Checking group membership'__LINE__);
  503.                         return $this->checkGroup(($this->options['memberisdn']$user_dn $username);
  504.                     else {
  505.                         $this->_debug('Authenticated'__LINE__);
  506.                         $this->_disconnect();
  507.                         return true; // user authenticated
  508.                     // checkGroup
  509.                 // bind
  510.             // non-empty password
  511.         // one entry
  512.         // default
  513.         $this->_debug('NOT authenticated!'__LINE__);
  514.         $this->_disconnect();
  515.         return false;
  516.     }
  517.  
  518.     /**
  519.      * Validate group membership
  520.      *
  521.      * Searches the LDAP server for group membership of the
  522.      * authenticated user.  Quotes all LDAP filter meta characters in
  523.      * the user name before querying the LDAP server.
  524.      *
  525.      * @param  string Distinguished Name of the authenticated User
  526.      * @return boolean 
  527.      */
  528.     function checkGroup($user)
  529.     {
  530.         // make filter
  531.         $filter sprintf('(&(%s=%s)(%s=%s)%s)',
  532.                           $this->options['groupattr'],
  533.                           $this->options['group'],
  534.                           $this->options['memberattr'],
  535.                           $this->_quoteFilterString($user),
  536.                           $this->options['groupfilter']);
  537.  
  538.         // make search base dn
  539.         $search_basedn $this->options['groupdn'];
  540.         if ($search_basedn != '' && substr($search_basedn-1!= ','{
  541.             $search_basedn .= ',';
  542.         }
  543.         $search_basedn .= $this->options['basedn'];
  544.  
  545.         $func_params = array($this->conn_id$search_basedn$filter,
  546.                              array($this->options['memberattr']));
  547.         $func_name $this->_scope2function($this->options['groupscope']);
  548.  
  549.         $this->_debug("Searching with $func_name and filter $filter in $search_basedn"__LINE__);
  550.  
  551.         // search
  552.         if (($result_id @call_user_func_array($func_name$func_params)) != false{
  553.             if (@ldap_count_entries($this->conn_id$result_id== 1{
  554.                 @ldap_free_result($result_id);
  555.                 $this->_debug('User is member of group'__LINE__);
  556.                 $this->_disconnect();
  557.                 return true;
  558.             }
  559.         }
  560.         // default
  561.         $this->_debug('User is NOT member of group'__LINE__);
  562.         $this->_disconnect();
  563.         return false;
  564.     }
  565.  
  566.     /**
  567.      * Outputs debugging messages
  568.      *
  569.      * @access private
  570.      * @param string Debugging Message
  571.      * @param integer Line number
  572.      */
  573.     function _debug($msg ''$line = 0)
  574.     {
  575.         if ($this->options['debug'=== true{
  576.             if ($msg == '' && $this->_isValidLink()) {
  577.                 $msg 'LDAP_Error: ' @ldap_err2str(@ldap_errno($this->_conn_id));
  578.             }
  579.             print("$line$msg <br />");
  580.         }
  581.     }
  582.  
  583.     /**
  584.      * Escapes LDAP filter special characters as defined in RFC 2254.
  585.      *
  586.      * @access private
  587.      * @param string Filter String
  588.      */
  589.     function _quoteFilterString($filter_str)
  590.     {
  591.         $metas        = array(  '\\',  '*',  '(',  ')',   "\x00");
  592.         $quoted_metas = array('\\\\''\*''\(''\)'"\\\x00");
  593.         return str_replace($metas$quoted_metas$filter_str);
  594.     }
  595. }
  596.  
  597. ?>

Documentation generated on Mon, 11 Mar 2019 14:36:46 -0400 by phpDocumentor 1.4.4. PEAR Logo Copyright © PHP Group 2004.