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

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