| // +----------------------------------------------------------------------+ // // $Id: LDAP.php,v 1.14 2003/06/02 16:55:10 mj Exp $ // require_once "Auth/Container.php"; 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 * 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 * scope: one, sub (default), or base * basedn: the base dn of your server * userdn: gets prepended to basedn when searching for user * userattr: the user attribute to search for (default: uid) * useroc: objectclass of user (for the search filter) * (default: posixAccount) * groupdn: gets prepended to basedn when searching for group * groupattr : the group attribute to search for (default: cn) * groupoc : objectclass of group (for the search filter) * (default: 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 * debug: Enable/Disable debugging output (default: false) * * To use this storage container, you have to use the following syntax: * * 'localhost', * 'port' => '389', * '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', * 'scope' => 'one', * 'userdn' => 'ou=People', * 'groupdn' => 'ou=Groups', * 'groupoc' => 'posixGroup', * 'memberattr' => 'memberUid', * 'memberisdn' => false, * 'group' => 'admin' * )); * * $a3 = new Auth('LDAP', array( * 'host' => 'ad.netsols.de', * 'basedn' => 'dc=netsols,dc=de', * 'userdn' => 'ou=Users', * 'binddn' => 'cn=Jan Wagner,ou=Users,dc=netsols,dc=de', * 'bindpw' => '*******', * 'userattr' => 'samAccountName', * 'useroc' => 'user', * '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, * * Example a3 shows a tested example for connenction to Windows 2000 * Active Directory * * @author Jan Wagner * @package Auth * @version $Revision: 1.14 $ */ class Auth_Container_LDAP extends Auth_Container { /** * Options for the class * @var array */ var $options = array(); /** * Connection ID of LDAP Link * @var string */ var $conn_id = false; /** * LDAP search function to use * @var string */ var $ldap_search_func; /** * 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) { $this->_setDefaults(); if (is_array($params)) { $this->_parseOptions($params); } } // }}} // {{{ _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() { // connect if (isset($this->options['url']) && $this->options['url'] != '') { $this->_debug('Connecting with URL', __LINE__); $conn_params = array($this->options['url']); } else { $this->_debug('Connecting with host:port', __LINE__); $conn_params = array($this->options['host'], $this->options['port']); } if(($this->conn_id = @call_user_func_array('ldap_connect', $conn_params)) === false) { return PEAR::raiseError('Auth_Container_LDAP: Could not connect to server.', 41, PEAR_ERROR_DIE); } $this->_debug('Successfully connected to server', __LINE__); // try switchig to LDAPv3 $ver = 0; if(@ldap_get_option($this->conn_id, LDAP_OPT_PROTOCOL_VERSION, $ver) && $ver >= 2) { $this->_debug('Switching to LDAPv3', __LINE__); @ldap_set_option($this->conn_id, LDAP_OPT_PROTOCOL_VERSION, 3); } // bind with credentials or anonymously if($this->options['binddn'] && $this->options['bindpw']) { $this->_debug('Binding with credentials', __LINE__); $bind_params = array($this->conn_id, $this->options['binddn'], $this->options['bindpw']); } else { $this->_debug('Binding anonymously', __LINE__); $bind_params = array($this->conn_id); } // bind for searching if ((@call_user_func_array('ldap_bind', $bind_params)) == false) { $this->_debug(); $this->_disconnect(); return PEAR::raiseError("Auth_Container_LDAP: Could not bind to LDAP server.", 41, PEAR_ERROR_DIE); } $this->_debug('Binding was successful', __LINE__); } /** * Disconnects (unbinds) from ldap server * * @access private */ function _disconnect() { if($this->_isValidLink()) { $this->_debug('disconnecting from server'); @ldap_unbind($this->conn_id); } } /** * Tries to find Basedn via namingContext Attribute * * @access private */ function _getBaseDN() { if ($this->options['basedn'] == "" && $this->_isValidLink()) { $this->_debug("basedn not set, searching via namingContexts.", __LINE__); $result_id = @ldap_read($this->conn_id, "", "(objectclass=*)", array("namingContexts")); if (ldap_count_entries($this->conn_id, $result_id) == 1) { $this->_debug("got result for namingContexts", __LINE__); $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->_debug("result for namingContexts was $basedn", __LINE__); $this->options['basedn'] = $basedn; } } ldap_free_result($result_id); } // if base ist still not set, raise error if ($this->options['basedn'] == "") { return PEAR::raiseError("Auth_Container_LDAP: LDAP search base not specified!", 41, PEAR_ERROR_DIE); } return true; } /** * 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; } /** * Set some default options * * @access private */ function _setDefaults() { $this->options['host'] = 'localhost'; $this->options['port'] = '389'; $this->options['binddn'] = ''; $this->options['bindpw'] = ''; $this->options['scope'] = 'sub'; $this->options['basedn'] = ''; $this->options['userdn'] = ''; $this->options['userattr'] = "uid"; $this->options['useroc'] = 'posixAccount'; $this->options['groupdn'] = ''; $this->options['groupattr'] = 'cn'; $this->options['groupoc'] = 'groupOfUniqueNames'; $this->options['memberattr'] = 'uniqueMember'; $this->options['memberisdn'] = true; $this->options['debug'] = false; } /** * Parse options passed to the container class * * @access private * @param array */ function _parseOptions($array) { foreach ($array as $key => $value) { $this->options[$key] = $value; } // get the according search function for selected scope switch($this->options['scope']) { case 'one': $this->ldap_search_func = 'ldap_list'; break; case 'base': $this->ldap_search_func = 'ldap_read'; break; default: $this->ldap_search_func = 'ldap_search'; break; } $this->_debug("LDAP search function will be: {$this->ldap_search_func}", __LINE__); } /** * Fetch data from LDAP server * * Searches the LDAP server for the given username/password * combination. * * @param string Username * @param string Password * @return boolean */ function fetchData($username, $password) { $this->_connect(); $this->_getBaseDN(); // make search filter $filter = sprintf('(&(objectClass=%s)(%s=%s))', $this->options['useroc'], $this->options['userattr'], $username); // make search base dn $search_basedn = $this->options['userdn']; if ($search_basedn != '' && substr($search_basedn, -1) != ',') { $search_basedn .= ','; } $search_basedn .= $this->options['basedn']; // make functions params array $func_params = array($this->conn_id, $search_basedn, $filter, array($this->options['userattr'])); $this->_debug("Searching with $filter in $search_basedn", __LINE__); // search if (($result_id = @call_user_func_array($this->ldap_search_func, $func_params)) == false) { $this->_debug('User not found', __LINE__); } elseif (ldap_count_entries($this->conn_id, $result_id) == 1) { // did we get just one entry? $this->_debug('User was found', __LINE__); // then get the user dn $entry_id = ldap_first_entry($this->conn_id, $result_id); $user_dn = ldap_get_dn($this->conn_id, $entry_id); ldap_free_result($result_id); // need to catch an empty password as openldap seems to return TRUE // if anonymous binding is allowed if ($password != "") { $this->_debug("Bind as $user_dn", __LINE__); // try binding as this user with the supplied password if (@ldap_bind($this->conn_id, $user_dn, $password)) { $this->_debug('Bind successful', __LINE__); // check group if appropiate if(isset($this->options['group'])) { // decide whether memberattr value is a dn or the username $this->_debug('Checking group membership', __LINE__); return $this->checkGroup(($this->options['memberisdn']) ? $user_dn : $username); } else { $this->_debug('Authenticated', __LINE__); $this->_disconnect(); return true; // user authenticated } // checkGroup } // bind } // non-empty password } // one entry // default $this->_debug('NOT authenticated!', __LINE__); $this->_disconnect(); return false; } /** * Validate group membership * * Searches the LDAP server for group membership of the * authenticated user * * @param string Distinguished Name of the authenticated User * @return boolean */ function checkGroup($user) { // make filter $filter = sprintf('(&(%s=%s)(objectClass=%s)(%s=%s))', $this->options['groupattr'], $this->options['group'], $this->options['groupoc'], $this->options['memberattr'], $user ); // 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'])); $this->_debug("Searching with $filter in $search_basedn", __LINE__); // search if(($result_id = @call_user_func_array($this->ldap_search_func, $func_params)) != false) { if(ldap_count_entries($this->conn_id, $result_id) == 1) { ldap_free_result($result_id); $this->_debug('User is member of group', __LINE__); $this->_disconnect(); return true; } } // default $this->_debug('User is NOT member of group', __LINE__); $this->_disconnect(); return false; } /** * Outputs debugging messages * * @access private * @param string Debugging Message * @param integer Line number */ function _debug($msg = '', $line = 0) { if($this->options['debug'] === true) { if($msg == '' && $this->_isValidLink()) { $msg = 'LDAP_Error: ' . @ldap_err2str(@ldap_errno($this->_conn_id)); } print("$line: $msg
"); } } } ?>