Rev 1173 | Rev 1987 | Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | RSS feed
<?php/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4 foldmethod=marker: *//*** Storage driver for use against an LDAP server** PHP versions 4 and 5** LICENSE: This source file is subject to version 3.01 of the PHP license* that is available through the world-wide-web at the following URI:* http://www.php.net/license/3_01.txt. If you did not receive a copy of* the PHP License and are unable to obtain it through the web, please* send a note to license@php.net so we can mail you a copy immediately.** @category Authentication* @package Auth* @author Jan Wagner <wagner@netsols.de>* @author Adam Ashley <aashley@php.net>* @author Hugues Peeters <hugues.peeters@claroline.net>* @copyright 2001-2006 The PHP Group* @license http://www.php.net/license/3_01.txt PHP License 3.01* @version CVS: $Id: LDAP.php,v 1.3 2007-11-19 15:11:00 jp_milcent Exp $* @link http://pear.php.net/package/Auth*//*** Include Auth_Container base class*/require_once "Auth/Container.php";/*** Include PEAR package for error handling*/require_once "PEAR.php";/*** Storage driver for fetching login data from LDAP** This class is heavily based on the DB and File containers. By default it* connects to localhost:389 and searches for uid=$username with the scope* "sub". If no search base is specified, it will try to determine it via* the namingContexts attribute. It takes its parameters in a hash, connects* to the ldap server, binds anonymously, searches for the user, and tries* to bind as the user with the supplied password. When a group was set, it* will look for group membership of the authenticated user. If all goes* well the authentication was successful.** Parameters:** host: localhost (default), ldap.netsols.de or 127.0.0.1* port: 389 (default) or 636 or whereever your server runs* url: ldap://localhost:389/* useful for ldaps://, works only with openldap2 ?* it will be preferred over host and port* version: LDAP version to use, ususally 2 (default) or 3,* must be an integer!* referrals: If set, determines whether the LDAP library automatically* follows referrals returned by LDAP servers or not. Possible* values are true (default) or false.* binddn: If set, searching for user will be done after binding* as this user, if not set the bind will be anonymous.* This is reported to make the container work with MS* Active Directory, but should work with any server that* is configured this way.* This has to be a complete dn for now (basedn and* userdn will not be appended).* bindpw: The password to use for binding with binddn* basedn: the base dn of your server* userdn: gets prepended to basedn when searching for user* userscope: Scope for user searching: one, sub (default), or base* userattr: the user attribute to search for (default: uid)* userfilter: filter that will be added to the search filter* this way: (&(userattr=username)(userfilter))* default: (objectClass=posixAccount)* attributes: array of additional attributes to fetch from entry.* these will added to auth data and can be retrieved via* Auth::getAuthData(). An empty array will fetch all attributes,* array('') will fetch no attributes at all (default)* If you add 'dn' as a value to this array, the users DN that was* used for binding will be added to auth data as well.* attrformat: The returned format of the additional data defined in the* 'attributes' option. Two formats are available.* LDAP returns data formatted in a* multidimensional array where each array starts with a* 'count' element providing the number of attributes in the* entry, or the number of values for attributes. When set* to this format, the only way to retrieve data from the* Auth object is by calling getAuthData('attributes').* AUTH returns data formatted in a* structure more compliant with other Auth Containers,* where each attribute element can be directly called by* getAuthData() method from Auth.* For compatibily with previous LDAP container versions,* the default format is LDAP.* groupdn: gets prepended to basedn when searching for group* groupattr: the group attribute to search for (default: cn)* groupfilter: filter that will be added to the search filter when* searching for a group:* (&(groupattr=group)(memberattr=username)(groupfilter))* default: (objectClass=groupOfUniqueNames)* memberattr : the attribute of the group object where the user dn* may be found (default: uniqueMember)* memberisdn: whether the memberattr is the dn of the user (default)* or the value of userattr (usually uid)* group: the name of group to search for* groupscope: Scope for group searching: one, sub (default), or base* start_tls: enable/disable the use of START_TLS encrypted connection* (default: false)* debug: Enable/Disable debugging output (default: false)* try_all: Whether to try all user accounts returned from the search* or just the first one. (default: false)** To use this storage container, you have to use the following syntax:** <?php* ...** $a1 = new Auth("LDAP", array(* 'host' => 'localhost',* 'port' => '389',* 'version' => 3,* 'basedn' => 'o=netsols,c=de',* 'userattr' => 'uid'* 'binddn' => 'cn=admin,o=netsols,c=de',* 'bindpw' => 'password'));** $a2 = new Auth('LDAP', array(* 'url' => 'ldaps://ldap.netsols.de',* 'basedn' => 'o=netsols,c=de',* 'userscope' => 'one',* 'userdn' => 'ou=People',* 'groupdn' => 'ou=Groups',* 'groupfilter' => '(objectClass=posixGroup)',* 'memberattr' => 'memberUid',* 'memberisdn' => false,* 'group' => 'admin'* ));** $a3 = new Auth('LDAP', array(* 'host' => 'ldap.netsols.de',* 'port' => 389,* 'version' => 3,* 'referrals' => false,* 'basedn' => 'dc=netsols,dc=de',* 'binddn' => 'cn=Jan Wagner,cn=Users,dc=netsols,dc=de',* 'bindpw' => 'password',* 'userattr' => 'samAccountName',* 'userfilter' => '(objectClass=user)',* 'attributes' => array(''),* 'group' => 'testing',* 'groupattr' => 'samAccountName',* 'groupfilter' => '(objectClass=group)',* 'memberattr' => 'member',* 'memberisdn' => true,* 'groupdn' => 'cn=Users',* 'groupscope' => 'one',* 'debug' => true);** The parameter values have to correspond* to the ones for your LDAP server of course.** When talking to a Microsoft ActiveDirectory server you have to* use 'samaccountname' as the 'userattr' and follow special rules* to translate the ActiveDirectory directory names into 'basedn'.* The 'basedn' for the default 'Users' folder on an ActiveDirectory* server for the ActiveDirectory Domain (which is not related to* its DNS name) "win2000.example.org" would be:* "CN=Users, DC=win2000, DC=example, DC=org'* where every component of the domain name becomes a DC attribute* of its own. If you want to use a custom users folder you have to* replace "CN=Users" with a sequence of "OU" attributes that specify* the path to your custom folder in reverse order.* So the ActiveDirectory folder* "win2000.example.org\Custom\Accounts"* would become* "OU=Accounts, OU=Custom, DC=win2000, DC=example, DC=org'** It seems that binding anonymously to an Active Directory* is not allowed, so you have to set binddn and bindpw for* user searching.** LDAP Referrals need to be set to false for AD to work sometimes.** Example a3 shows a full blown and tested example for connection to* Windows 2000 Active Directory with group mebership checking** Note also that if you want an encrypted connection to an MS LDAP* server, then, on your webserver, you must specify* TLS_REQCERT never* in /etc/ldap/ldap.conf or in the webserver user's ~/.ldaprc (which* may or may not be read depending on your configuration).*** @category Authentication* @package Auth* @author Jan Wagner <wagner@netsols.de>* @author Adam Ashley <aashley@php.net>* @author Hugues Peeters <hugues.peeters@claroline.net>* @copyright 2001-2006 The PHP Group* @license http://www.php.net/license/3_01.txt PHP License 3.01* @version Release: 1.5.4 File: $Revision: 1.3 $* @link http://pear.php.net/package/Auth*/class Auth_Container_LDAP extends Auth_Container{// {{{ properties/*** Options for the class* @var array*/var $options = array();/*** Connection ID of LDAP Link* @var string*/var $conn_id = false;// }}}// {{{ Auth_Container_LDAP() [constructor]/*** Constructor of the container class** @param $params, associative hash with host,port,basedn and userattr key* @return object Returns an error object if something went wrong*/function Auth_Container_LDAP($params){if (false === extension_loaded('ldap')) {return PEAR::raiseError('Auth_Container_LDAP: LDAP Extension not loaded',41, PEAR_ERROR_DIE);}$this->_setDefaults();if (is_array($params)) {$this->_parseOptions($params);}}// }}}// {{{ _prepare()/*** Prepare LDAP connection** This function checks if we have already opened a connection to* the LDAP server. If that's not the case, a new connection is opened.** @access private* @return mixed True or a PEAR error object.*/function _prepare(){if (!$this->_isValidLink()) {$res = $this->_connect();if (PEAR::isError($res)) {return $res;}}return true;}// }}}// {{{ _connect()/*** Connect to the LDAP server using the global options** @access private* @return object Returns a PEAR error object if an error occurs.*/function _connect(){$this->log('Auth_Container_LDAP::_connect() called.', AUTH_LOG_DEBUG);// connectif (isset($this->options['url']) && $this->options['url'] != '') {$this->log('Connecting with URL', AUTH_LOG_DEBUG);$conn_params = array($this->options['url']);} else {$this->log('Connecting with host:port', AUTH_LOG_DEBUG);$conn_params = array($this->options['host'], $this->options['port']);}if (($this->conn_id = @call_user_func_array('ldap_connect', $conn_params)) === false) {$this->log('Connection to server failed.', AUTH_LOG_DEBUG);$this->log('LDAP ERROR: '.ldap_errno($this->conn_id).': '.ldap_error($this->conn_id), AUTH_LOG_DEBUG);return PEAR::raiseError('Auth_Container_LDAP: Could not connect to server.', 41);}$this->log('Successfully connected to server', AUTH_LOG_DEBUG);// switch LDAP versionif (is_numeric($this->options['version']) && $this->options['version'] > 2) {$this->log("Switching to LDAP version {$this->options['version']}", AUTH_LOG_DEBUG);@ldap_set_option($this->conn_id, LDAP_OPT_PROTOCOL_VERSION, $this->options['version']);// start TLS if availableif (isset($this->options['start_tls']) && $this->options['start_tls']) {$this->log("Starting TLS session", AUTH_LOG_DEBUG);if (@ldap_start_tls($this->conn_id) === false) {$this->log('Could not start TLS session', AUTH_LOG_DEBUG);$this->log('LDAP ERROR: '.ldap_errno($this->conn_id).': '.ldap_error($this->conn_id), AUTH_LOG_DEBUG);return PEAR::raiseError('Auth_Container_LDAP: Could not start tls.', 41);}}}// switch LDAP referralsif (is_bool($this->options['referrals'])) {$this->log("Switching LDAP referrals to " . (($this->options['referrals']) ? 'true' : 'false'), AUTH_LOG_DEBUG);if (@ldap_set_option($this->conn_id, LDAP_OPT_REFERRALS, $this->options['referrals']) === false) {$this->log('Could not change LDAP referrals options', AUTH_LOG_DEBUG);$this->log('LDAP ERROR: '.ldap_errno($this->conn_id).': '.ldap_error($this->conn_id), AUTH_LOG_DEBUG);}}// bind with credentials or anonymouslyif (strlen($this->options['binddn']) && strlen($this->options['bindpw'])) {$this->log('Binding with credentials', AUTH_LOG_DEBUG);$bind_params = array($this->conn_id, $this->options['binddn'], $this->options['bindpw']);} else {$this->log('Binding anonymously', AUTH_LOG_DEBUG);$bind_params = array($this->conn_id);}// bind for searchingif ((@call_user_func_array('ldap_bind', $bind_params)) === false) {$this->log('Bind failed', AUTH_LOG_DEBUG);$this->log('LDAP ERROR: '.ldap_errno($this->conn_id).': '.ldap_error($this->conn_id), AUTH_LOG_DEBUG);$this->_disconnect();return PEAR::raiseError("Auth_Container_LDAP: Could not bind to LDAP server.", 41);}$this->log('Binding was successful', AUTH_LOG_DEBUG);return true;}// }}}// {{{ _disconnect()/*** Disconnects (unbinds) from ldap server** @access private*/function _disconnect(){$this->log('Auth_Container_LDAP::_disconnect() called.', AUTH_LOG_DEBUG);if ($this->_isValidLink()) {$this->log('disconnecting from server');@ldap_unbind($this->conn_id);}}// }}}// {{{ _getBaseDN()/*** Tries to find Basedn via namingContext Attribute** @access private*/function _getBaseDN(){$this->log('Auth_Container_LDAP::_getBaseDN() called.', AUTH_LOG_DEBUG);$err = $this->_prepare();if ($err !== true) {return PEAR::raiseError($err->getMessage(), $err->getCode());}if ($this->options['basedn'] == "" && $this->_isValidLink()) {$this->log("basedn not set, searching via namingContexts.", AUTH_LOG_DEBUG);$result_id = @ldap_read($this->conn_id, "", "(objectclass=*)", array("namingContexts"));if (@ldap_count_entries($this->conn_id, $result_id) == 1) {$this->log("got result for namingContexts", AUTH_LOG_DEBUG);$entry_id = @ldap_first_entry($this->conn_id, $result_id);$attrs = @ldap_get_attributes($this->conn_id, $entry_id);$basedn = $attrs['namingContexts'][0];if ($basedn != "") {$this->log("result for namingContexts was $basedn", AUTH_LOG_DEBUG);$this->options['basedn'] = $basedn;}}@ldap_free_result($result_id);}// if base ist still not set, raise errorif ($this->options['basedn'] == "") {return PEAR::raiseError("Auth_Container_LDAP: LDAP search base not specified!", 41);}return true;}// }}}// {{{ _isValidLink()/*** determines whether there is a valid ldap conenction or not** @accessd private* @return boolean*/function _isValidLink(){if (is_resource($this->conn_id)) {if (get_resource_type($this->conn_id) == 'ldap link') {return true;}}return false;}// }}}// {{{ _setDefaults()/*** Set some default options** @access private*/function _setDefaults(){$this->options['url'] = '';$this->options['host'] = 'localhost';$this->options['port'] = '389';$this->options['version'] = 2;$this->options['referrals'] = true;$this->options['binddn'] = '';$this->options['bindpw'] = '';$this->options['basedn'] = '';$this->options['userdn'] = '';$this->options['userscope'] = 'sub';$this->options['userattr'] = 'uid';$this->options['userfilter'] = '(objectClass=posixAccount)';$this->options['attributes'] = array(''); // no attributes$this->options['attrformat'] = 'AUTH'; // returns attribute like other Auth containers$this->options['group'] = '';$this->options['groupdn'] = '';$this->options['groupscope'] = 'sub';$this->options['groupattr'] = 'cn';$this->options['groupfilter'] = '(objectClass=groupOfUniqueNames)';$this->options['memberattr'] = 'uniqueMember';$this->options['memberisdn'] = true;$this->options['start_tls'] = false;$this->options['debug'] = false;$this->options['try_all'] = false; // Try all user ids returned not just the first one}// }}}// {{{ _parseOptions()/*** Parse options passed to the container class** @access private* @param array*/function _parseOptions($array){$array = $this->_setV12OptionsToV13($array);foreach ($array as $key => $value) {if (array_key_exists($key, $this->options)) {if ($key == 'attributes') {if (is_array($value)) {$this->options[$key] = $value;} else {$this->options[$key] = explode(',', $value);}} else {$this->options[$key] = $value;}}}}// }}}// {{{ _setV12OptionsToV13()/*** Adapt deprecated options from Auth 1.2 LDAP to Auth 1.3 LDAP** @author Hugues Peeters <hugues.peeters@claroline.net>* @access private* @param array* @return array*/function _setV12OptionsToV13($array){if (isset($array['useroc']))$array['userfilter'] = "(objectClass=".$array['useroc'].")";if (isset($array['groupoc']))$array['groupfilter'] = "(objectClass=".$array['groupoc'].")";if (isset($array['scope']))$array['userscope'] = $array['scope'];return $array;}// }}}// {{{ _scope2function()/*** Get search function for scope** @param string scope* @return string ldap search function*/function _scope2function($scope){switch($scope) {case 'one':$function = 'ldap_list';break;case 'base':$function = 'ldap_read';break;default:$function = 'ldap_search';break;}return $function;}// }}}// {{{ fetchData()/*** Fetch data from LDAP server** Searches the LDAP server for the given username/password* combination. Escapes all LDAP meta characters in username* before performing the query.** @param string Username* @param string Password* @return boolean*/function fetchData($username, $password){$this->log('Auth_Container_LDAP::fetchData() called.', AUTH_LOG_DEBUG);$err = $this->_prepare();if ($err !== true) {return PEAR::raiseError($err->getMessage(), $err->getCode());}$err = $this->_getBaseDN();if ($err !== true) {return PEAR::raiseError($err->getMessage(), $err->getCode());}// UTF8 Encode username for LDAPv3if (@ldap_get_option($this->conn_id, LDAP_OPT_PROTOCOL_VERSION, $ver) && $ver == 3) {$this->log('UTF8 encoding username for LDAPv3', AUTH_LOG_DEBUG);$username = utf8_encode($username);}// make search filter$filter = sprintf('(&(%s=%s)%s)',$this->options['userattr'],$this->_quoteFilterString($username),$this->options['userfilter']);// make search base dn$search_basedn = $this->options['userdn'];if ($search_basedn != '' && substr($search_basedn, -1) != ',') {$search_basedn .= ',';}$search_basedn .= $this->options['basedn'];// attributes$searchAttributes = $this->options['attributes'];// make functions params array$func_params = array($this->conn_id, $search_basedn, $filter, $searchAttributes);// search function to use$func_name = $this->_scope2function($this->options['userscope']);$this->log("Searching with $func_name and filter $filter in $search_basedn", AUTH_LOG_DEBUG);// searchif (($result_id = @call_user_func_array($func_name, $func_params)) === false) {$this->log('User not found', AUTH_LOG_DEBUG);} elseif (@ldap_count_entries($this->conn_id, $result_id) >= 1) { // did we get some possible results?$this->log('User(s) found', AUTH_LOG_DEBUG);$first = true;$entry_id = null;do {// then get the user dnif ($first) {$entry_id = @ldap_first_entry($this->conn_id, $result_id);$first = false;} else {$entry_id = @ldap_next_entry($this->conn_id, $entry_id);if ($entry_id === false)break;}$user_dn = @ldap_get_dn($this->conn_id, $entry_id);// as the dn is not fetched as an attribute, we save it anywayif (is_array($searchAttributes) && in_array('dn', $searchAttributes)) {$this->log('Saving DN to AuthData', AUTH_LOG_DEBUG);$this->_auth_obj->setAuthData('dn', $user_dn);}// fetch attributesif ($attributes = @ldap_get_attributes($this->conn_id, $entry_id)) {if (is_array($attributes) && isset($attributes['count']) &&$attributes['count'] > 0) {// ldap_get_attributes() returns a specific multi dimensional array// format containing all the attributes and where each array starts// with a 'count' element providing the number of attributes in the// entry, or the number of values for attribute. For compatibility// reasons, it remains the default format returned by LDAP container// setAuthData().// The code below optionally returns attributes in another format,// more compliant with other Auth containers, where each attribute// element are directly set in the 'authData' list. This option is// enabled by setting 'attrformat' to// 'AUTH' in the 'options' array.// eg. $this->options['attrformat'] = 'AUTH'if ( strtoupper($this->options['attrformat']) == 'AUTH' ) {$this->log('Saving attributes to Auth data in AUTH format', AUTH_LOG_DEBUG);unset ($attributes['count']);foreach ($attributes as $attributeName => $attributeValue ) {if (is_int($attributeName)) continue;if (is_array($attributeValue) && isset($attributeValue['count'])) {unset ($attributeValue['count']);}if (count($attributeValue)<=1) $attributeValue = $attributeValue[0];$this->log('Storing additional field: '.$attributeName, AUTH_LOG_DEBUG);$this->_auth_obj->setAuthData($attributeName, $attributeValue);}}else{$this->log('Saving attributes to Auth data in LDAP format', AUTH_LOG_DEBUG);$this->_auth_obj->setAuthData('attributes', $attributes);}}}@ldap_free_result($result_id);// need to catch an empty password as openldap seems to return TRUE// if anonymous binding is allowedif ($password != "") {$this->log("Bind as $user_dn", AUTH_LOG_DEBUG);// try binding as this user with the supplied passwordif (@ldap_bind($this->conn_id, $user_dn, $password)) {$this->log('Bind successful', AUTH_LOG_DEBUG);// check group if appropiateif (strlen($this->options['group'])) {// decide whether memberattr value is a dn or the username$this->log('Checking group membership', AUTH_LOG_DEBUG);$return = $this->checkGroup(($this->options['memberisdn']) ? $user_dn : $username);$this->_disconnect();return $return;} else {$this->log('Authenticated', AUTH_LOG_DEBUG);$this->_disconnect();return true; // user authenticated} // checkGroup} // bind} // non-empty password} while ($this->options['try_all'] == true); // interate through entries} // get results// default$this->log('NOT authenticated!', AUTH_LOG_DEBUG);$this->_disconnect();return false;}// }}}// {{{ checkGroup()/*** Validate group membership** Searches the LDAP server for group membership of the* supplied username. Quotes all LDAP filter meta characters in* the user name before querying the LDAP server.** @param string Distinguished Name of the authenticated User* @return boolean*/function checkGroup($user){$this->log('Auth_Container_LDAP::checkGroup() called.', AUTH_LOG_DEBUG);$err = $this->_prepare();if ($err !== true) {return PEAR::raiseError($err->getMessage(), $err->getCode());}// make filter$filter = sprintf('(&(%s=%s)(%s=%s)%s)',$this->options['groupattr'],$this->options['group'],$this->options['memberattr'],$this->_quoteFilterString($user),$this->options['groupfilter']);// make search base dn$search_basedn = $this->options['groupdn'];if ($search_basedn != '' && substr($search_basedn, -1) != ',') {$search_basedn .= ',';}$search_basedn .= $this->options['basedn'];$func_params = array($this->conn_id, $search_basedn, $filter,array($this->options['memberattr']));$func_name = $this->_scope2function($this->options['groupscope']);$this->log("Searching with $func_name and filter $filter in $search_basedn", AUTH_LOG_DEBUG);// searchif (($result_id = @call_user_func_array($func_name, $func_params)) != false) {if (@ldap_count_entries($this->conn_id, $result_id) == 1) {@ldap_free_result($result_id);$this->log('User is member of group', AUTH_LOG_DEBUG);return true;}}// default$this->log('User is NOT member of group', AUTH_LOG_DEBUG);return false;}// }}}// {{{ _quoteFilterString()/*** Escapes LDAP filter special characters as defined in RFC 2254.** @access private* @param string Filter String*/function _quoteFilterString($filter_str){$metas = array( '\\', '*', '(', ')', "\x00");$quoted_metas = array('\\\\', '\*', '\(', '\)', "\\\x00");return str_replace($metas, $quoted_metas, $filter_str);}// }}}}?>